- Added lib/signal_reader.py with SignalGpsReader, SignalMeteoReader, and SignalDepthReader that use PropertiesChanged signal subscriptions instead of polling via GetValue(), reducing D-Bus overhead at steady state. - Each reader discovers its service dynamically, seeds its cache with a one-shot GetValue, then relies on signals for all subsequent updates. - Refactored dbus-tides, dbus-windy-station, dbus-no-foreign-land, dbus-lightning, and dbus-meteoblue-forecast to import from the shared library, removing ~600 lines of duplicated _unwrap() helpers and per-service GPS/meteo/depth reader classes. - Updated install.sh for all five services to deploy signal_reader.py to /data/lib/ on the target device. - Updated build-package.sh for all five services to bundle signal_reader.py into the .tar.gz package. - Updated README.md with the new lib/ entry in the project table and documented the shared D-Bus readers pattern. - Bumped version numbers in affected services (e.g. nfl_tracking 2.0.1). Made-with: Cursor
dbus-tides
A Venus OS service that provides real-time tide predictions by combining onboard depth-sensor observations with harmonic tidal models. It publishes tide data on D-Bus (and therefore MQTT) so that marine displays, MFDs, and other Venus OS consumers can show current and upcoming tide information.
Version: 1.0.19
How It Works
Overview
The service continuously reads the vessel's depth sounder, detects observed
high and low tide events, and blends them with a harmonic tidal model to
produce an accurate, location-aware tide prediction curve. The result is
published as a set of D-Bus paths under com.victronenergy.tides.
Depth Sensor (D-Bus)
│
▼
DepthRecorder Collect, average, persist 5-min depth samples
│
▼
TideDetector Detect observed high/low tide turning points
│
▼
TidePredictor Harmonic model prediction (NOAA stations or grid)
│
▼
TideMerger Calibrate predictions to depth under keel
│
▼
TideAdapter Local adaptation (timing offset, amplitude scaling)
│
▼
D-Bus / MQTT Publish predictions, curves, and event slots
Depth Collection (depth_recorder.py)
Raw depth readings are sampled every 10 seconds. Readings are averaged over
5-minute windows to suppress wave and wake noise, then stored in a SQLite
database (/data/dbus-tides/depth_history.db) along with GPS coordinates.
Up to 96 hours of history is retained.
When retrieving history for calibration or detection, records are filtered by position so that only data from the current anchorage (within the stationary radius) is used.
Tide Detection (tide_detector.py)
The smoothed depth history is processed to identify turning points (high and low tides). A two-stage approach is used:
- Median filter (kernel size 5) removes impulse noise from anchor swing.
- Triangular-weighted moving average (60-minute window) produces a smooth trend curve.
- Slope reversal detection identifies candidate extrema.
- Amplitude gating requires at least 0.4 m of depth change between
consecutive highs and lows (
MIN_TIDE_AMPLITUDE). - Confirmation requires a 0.2 m excursion from the candidate before the event is reported, preventing premature triggering on flat tides.
Harmonic Prediction (tide_harmonics.py, tide_predictor.py)
Tide heights are computed using the standard IHO/Schureman harmonic method with 37 tidal constituents:
h(t) = Z₀ + Σ fᵢ · Aᵢ · cos(Vᵢ(t) + uᵢ − Gᵢ)
Where:
- Z₀ — mean water level
- Aᵢ, Gᵢ — amplitude and phase lag for each constituent
- fᵢ, uᵢ — nodal amplitude and phase corrections (18.6-year lunar cycle)
- Vᵢ(t) — astronomical argument computed from Doodson numbers
Constituent list (37)
| Category | Constituents |
|---|---|
| Semi-diurnal | M2, S2, N2, K2, 2N2, MU2, NU2, L2, T2, R2, LAM2 |
| Diurnal | K1, O1, P1, Q1, J1, OO1, M1, 2Q1, RHO1 |
| Long-period | MF, MM, SSA, MSM, MS |
| Shallow-water | M3, M4, MS4, M6, M8, MN4, S4, S6, 2SM2, MKS2 |
| Terdiurnal | MK3, 2MK3 |
| Other | S1, SA |
The engine is pure Python with no external dependencies at runtime.
Extrema detection
Predicted tide events (highs and lows) are extracted from the harmonic curve using a multi-pass algorithm:
- Identify all local maxima and minima.
- Merge consecutive same-type extrema to enforce strict alternation.
- Iteratively remove the adjacent pair with the smallest height range until all remaining pairs exceed the minimum amplitude threshold.
This avoids the cascading-rejection problem that single-pass filtering causes when diurnal inequality produces small-range tidal cycles (common in the Caribbean, Gulf of Mexico, and other mixed-tide regions).
Station Selection (tide_predictor.py)
Constituent data can come from three sources, tried in priority order:
- Manual override — user specifies a NOAA station ID via
/Settings/StationOverride. - Best-fit scoring — if the vessel has accumulated at least 12 hours of depth history, nearby NOAA stations are hindcast and correlated against detrended observations. The station with the highest Pearson correlation (minimum 0.3) wins.
- Nearest station — falls back to the closest NOAA station within 50 nm.
- Coastal grid — if no nearby station exists, constituents are bilinearly interpolated from a pre-built 0.25° coastal grid (derived from the GOT4.10c global ocean tide model).
Up to 5 nearby NOAA stations are reported on /Tide/NearbyStations for the
UI to offer as manual selections.
Subordinate stations
NOAA subordinate stations (type S) do not have their own harmonic
constituents. Instead, predictions are derived from a reference harmonic
station by:
- Predicting the reference station's full curve.
- Applying time offsets (
time_high,time_low) interpolated between reference extrema. - Applying height correction factors (
height_high,height_low) as either ratios or additive offsets, interpolated between reference extrema.
Chart Depth Calibration (tide_merger.py)
Harmonic models predict heights relative to a tidal datum (typically mean sea level). The depth sounder reports depth under the keel. The chart depth offset bridges the two:
chart_depth = mean(observed_depth) − mean(predicted_tide_height)
This offset is computed from recent observations and converts model predictions into expected depth under the keel. Once calibrated, the predicted curve represents what the sounder should read at each point in time.
Position-Aware Recalibration
When the vessel moves, the service responds at three distance thresholds:
| Threshold | Distance | Action |
|---|---|---|
CHART_DEPTH_RECAL_METERS |
500 ft (152 m) | Recalibrate chart depth using the last 30 minutes of observations at the new position |
STATIONARY_RADIUS_METERS |
5 nm (9.3 km) | Depth history is filtered to the current anchorage |
MODEL_RERUN_RADIUS_METERS |
20 nm (37 km) | Re-interpolate tidal constituents and re-run the harmonic model |
On service restart, if no prior calibration position is known, calibration uses only recent observations (last 30 minutes) to avoid contamination from depth records at a previous anchorage.
A speed gate (2 knots) suppresses depth readings while underway, since vessel motion invalidates sounder accuracy.
Local Adaptation (tide_adapter.py)
Even with the best available constituents, local geography (bays, inlets, shallow banks) causes tides to arrive earlier or later and with different amplitude compared to the open-ocean model. The adapter estimates two correction factors from observed tide events matched against predictions:
- Δt — timing offset in seconds (positive = local tide arrives later)
- k — amplitude scale factor (1.0 = no scaling)
The corrected local prediction is:
local_height(t) = k · predicted_height(t − Δt)
Adaptation progresses through stages:
- Status 0 — no observed events matched yet
- Status 1 — timing correction only (at least 2 matched events)
- Status 2 — timing + amplitude correction (at least 1 high/low range pair)
Residual correction
After applying timing and amplitude adjustments, a smoothed residual (observed minus adapted model) is computed over a 1.5-hour window and added to the prediction. Beyond the last observation, the residual correction decays exponentially with a 6-hour half-life.
Dual-Curve Output
The service publishes two prediction curves:
- Station curve — raw harmonic model shifted to depth under keel
- Local curve — station curve with timing, amplitude, and residual corrections applied
The UI can display either or both. The "best" curve (local when adaptation
is active, otherwise station) is published on /Tide/Predictions.
D-Bus / MQTT Topics
All paths are published under service name com.victronenergy.tides.
On MQTT, paths map to topics prefixed with N/<portal_id>/tides/.
Service Management
| Path | Type | Description |
|---|---|---|
/Mgmt/ProcessName |
string | dbus-tides |
/Mgmt/ProcessVersion |
string | Service version |
/Mgmt/Connection |
string | local |
/DeviceInstance |
int | 0 |
/ProductId |
int | 0xA162 |
/ProductName |
string | Tide Prediction |
/FirmwareVersion |
string | Service version |
/Connected |
int | 1 |
Status
| Path | Type | Description |
|---|---|---|
/Status |
int | 0 Idle, 1 Calibrating, 2 Ready, 3 Error |
/ErrorMessage |
string | Error description (empty when status is not 3) |
/IsStationary |
int | 1 if vessel is within the stationary radius |
Depth
| Path | Type | Description |
|---|---|---|
/Depth/Current |
float | Latest smoothed depth in meters |
/Depth/History |
string | JSON array of {"ts": <unix>, "depth": <m>} (last 24 h) |
/Depth/FullHistory |
string | JSON array of all observed depth points |
Predictions
| Path | Type | Description |
|---|---|---|
/Tide/Predictions |
string | JSON array — best available prediction curve |
/Tide/Station/Predictions |
string | JSON array — raw station model curve |
/Tide/Local/Predictions |
string | JSON array — locally adapted curve |
Local Adaptation
| Path | Type | Description |
|---|---|---|
/Tide/Local/TimeOffset |
float | Timing offset Δt in seconds |
/Tide/Local/AmpScale |
float | Amplitude scale factor k |
/Tide/Local/MatchCount |
int | Number of matched observed/predicted events |
/Tide/Local/Status |
int | 0 no data, 1 time only, 2 time + amplitude |
Tide Event Slots
Eight event slots provide the next and previous two highs and two lows. Each slot has four sub-paths:
| Slot | Sub-paths |
|---|---|
NextHigh1, NextHigh2 |
/Time, /Depth, /ObsTime, /ObsDepth |
NextLow1, NextLow2 |
/Time, /Depth, /ObsTime, /ObsDepth |
PrevHigh1, PrevHigh2 |
/Time, /Depth, /ObsTime, /ObsDepth |
PrevLow1, PrevLow2 |
/Time, /Depth, /ObsTime, /ObsDepth |
Full path example: /Tide/NextHigh1/Time (Unix timestamp),
/Tide/NextHigh1/Depth (meters).
ObsTime and ObsDepth are populated when a detected observed event has
been matched to this predicted slot; otherwise they are null.
Model Metadata
| Path | Type | Description |
|---|---|---|
/Tide/LastModelRun |
int | Unix timestamp of the last prediction run |
/Tide/DataSource |
string | Source identifier, e.g. noaa:8722670, grid |
/Tide/ChartDepth |
float | Calibrated chart depth offset in meters |
/Tide/DatumOffset |
float | MSL above MLLW in meters |
/Tide/ModelLocation/Lat |
float | Latitude used for model |
/Tide/ModelLocation/Lon |
float | Longitude used for model |
Station Info
| Path | Type | Description |
|---|---|---|
/Tide/Station/Id |
string | NOAA station ID |
/Tide/Station/Name |
string | Station name |
/Tide/Station/Distance |
float | Distance in km |
/Tide/Station/Lat |
float | Station latitude |
/Tide/Station/Lon |
float | Station longitude |
/Tide/Station/Type |
string | R (reference/harmonic) or S (subordinate) |
/Tide/Station/RefId |
string | Reference station ID (subordinate stations only) |
/Tide/NearbyStations |
string | JSON array of nearby stations for UI selection |
Settings (writable)
| Path | Type | Default | Description |
|---|---|---|---|
/Settings/Enabled |
int | 1 |
0 disables the service |
/Settings/MinTideAmplitude |
float | 0.3 |
Minimum amplitude for observed tide detection |
/Settings/Units |
int | 0 |
Display unit preference |
/Settings/DatumOffset |
float | -1.0 |
Manual datum offset in meters (-1 = automatic) |
/Settings/StationOverride |
string | "" |
Manual NOAA station ID (empty = automatic selection) |
Installation
The service is designed for Venus OS (Victron Energy). It installs to
/data/dbus-tides/ and registers itself with the daemontools service
supervisor.
# Install
chmod +x install.sh
./install.sh
# Uninstall (preserves data in /data/dbus-tides/)
./uninstall.sh
The install script:
- Locates
velib_pythonand creates a symlink - Links the service directory into
/service/(or/opt/victronenergy/service/) - Creates
/var/log/dbus-tides/for multilog output - Adds an
rc.localentry so the service persists across firmware updates
Building
./build-package.sh # dbus-tides-1.0.19.tar.gz
./build-package.sh --version 1.1.0 # override version
./build-package.sh --output /tmp/release # custom output dir
Produces a .tar.gz package and a .sha256 checksum file.
Development Tools
Tools in the tools/ directory require the dependencies listed in
tools/requirements-dev.txt (pyTMD, numpy, scipy, pyproj,
h5netcdf, xarray).
| Tool | Purpose |
|---|---|
build_noaa_stations.py |
Fetch NOAA harmonic and subordinate station data from the CO-OPS API |
build_coastal_grid.py |
Build a 0.25° coastal constituent grid from pyTMD / GOT4.10c |
predict_tides.py |
Standalone tide prediction for a given lat/lon |
extract_constituents.py |
Extract constituents for a single position from pyTMD |
Configuration
All tunable parameters are in config.py. Key settings:
| Parameter | Default | Description |
|---|---|---|
DEPTH_SAMPLE_INTERVAL |
10 s | Depth sensor polling rate |
DEPTH_AVG_WINDOW |
300 s | Averaging window for raw readings |
DEPTH_HISTORY_HOURS |
96 h | History retention |
MIN_TIDE_AMPLITUDE |
0.4 m | Minimum observed tide range |
SPEED_THRESHOLD_MS |
1.03 m/s | Speed gate (≈ 2 knots) |
STATIONARY_RADIUS_METERS |
9260 m | Anchorage radius (5 nm) |
CHART_DEPTH_RECAL_METERS |
152 m | Movement to trigger recalibration |
MODEL_RERUN_RADIUS_METERS |
37040 m | Movement to re-run model (20 nm) |
PREDICTION_HOURS |
48 h | Prediction horizon |
PREDICTION_INTERVAL |
900 s | Prediction time step (15 min) |
NOAA_STATION_MAX_DISTANCE |
92600 m | Max station distance (50 nm) |
BESTFIT_MIN_HISTORY_HOURS |
12 h | History required for best-fit scoring |
ADAPTATION_MAX_MATCH_WINDOW |
10800 s | Max time difference for event matching |
RESIDUAL_DECAY_HOURS |
6 h | Residual correction decay half-life |
License
See the project repository for license details.