Skip to content

organvm/netmode

Repository files navigation

netmode

A laptop that manages itself across metered and free connections — so you never have to think about which link you're on.

CI shellcheck version macOS

netmode is a single-machine multi-link manager for macOS. It meters every connection a laptop uses, scores them, and switches between them automatically — with one overriding rule it enforces on its own:

At home, ride the metered phone hotspot and never touch the Starlink (Starlink is reserved for the TV). Away, use anything — free Wi-Fi first, then the phone, then Starlink. You unplug, walk around, leave, come back — and it just follows.

The whole point is captured in one user sentence that drove the design:

"Why do I need to be thinking about this still? Why isn't this seamless?"

netmode's job is to make that question go away.


Table of contents


What it does

  • Meters each link separately — the phone hotspot and Starlink have different caps and different billing reset days; "free" Wi-Fi (café, friend's place) is tracked for information but never billed.
  • Knows where it is. A learned Wi-Fi fingerprint plus live signals tell it home from away — no GPS, no cloud, no setup.
  • Knows if the phone is here. USB tether, hotspot SSID visibility, and Bluetooth proximity together answer "is my phone in the room?" — so it never clings to a hotspot that walked out the door.
  • Switches on network change, not on a timer. A launchd WatchPaths agent fires the moment macOS rewrites its network config — no always-on daemon.
  • Keeps the hotspot alive while you work — a tiny gated heartbeat defeats iOS's ~90 s idle-disconnect, but only when the phone is your link and you're at the Mac.
  • Surfaces everything — a text status, a doctor diagnostic, a JSON API, and a local web dashboard.

Quick start

# 1. clone into place (this repo IS ~/Library/Application Support/netmeter)
git clone git@github.com:4444J99/netmode.git "$HOME/Library/Application Support/netmeter"
cd "$HOME/Library/Application Support/netmeter"

# 2. create your private config from the example (real SSIDs live here; it's git-ignored)
cp config.example config
$EDITOR config

# 3. prove the logic is sound (no network changes are made)
bash netmode.sh selftest

# 4. see where you stand
bash netmode.sh           # live status + "next move"
bash netmode.sh doctor    # full diagnostic

# 5. make it seamless — fire on every network change (no daemon)
bash netmode.sh trigger install

# optional: a 5-minute safety tick + the hotspot keep-alive
bash netmode.sh keepalive on

A convenience netmode alias:

alias netmode='bash "$HOME/Library/Application Support/netmeter/netmode.sh"'

The seamless rule

seamless is the default mode. It dispatches on location:

Where Behavior
home Ride the iPhone (USB or its Wi-Fi). Remove Starlink from preferred networks so macOS won't auto-join it. Never Starlink — even over cap, even if the phone leaves (it holds offline and notifies instead).
away The cascade: free/known Wi-Fi → iPhone hotspot → Starlink. Anything goes.
unknown Conservative auto behavior until a signal arrives.

Because netmode rewrites macOS's own preferred-network order to match, the OS does the right thing between ticks too. That's the seamless part.

Manual override when sensing is wrong:

netmode here     # pin "home"  for a few hours
netmode out      # pin "away"  for a few hours  (e.g. a café where sensing lags)

Commands

netmode                 live status + "next move"
netmode watch [secs]    auto-refreshing status
netmode why             explain the current decision + the inputs behind it
netmode doctor          full diagnostic (links, scores, location, schedule, service order)
netmode report          usage + projection
netmode export          usage history as CSV
netmode setusage L GB   anchor link L (phone|starlink) to a provider-reported GB number
netmode here | out      pin location home/away (overrides sensing for a few hours)
netmode overnight ...   set/clear the overnight Starlink window (see Modes)
netmode keepalive ...   on | off | status — hotspot idle-drop heartbeat
netmode trigger ...     install | uninstall the network-change LaunchAgent (no daemon)
netmode dish [--raw|--json]   read-only Starlink dish stats (on the Starlink LAN)
netmode menubar install drop a SwiftBar/xbar plugin (opt-in, no daemon)
netmode json            machine status (used by the dashboard)
netmode selftest        assert decision logic & invariants (no network changes)
netmode ui              open the dashboard (http://127.0.0.1:8765)
netmode version         print the version

Modes

Mode Behavior
seamless default — location-aware (see above)
auto burn the phone first, auto-fall to Starlink at the cap
failover cable = phone, unplug = Starlink (instant)
anchor Starlink first even when the cable's plugged (saves the phone)
solo phone only, never Starlink (Wi-Fi hotspot ok; offline if the phone's gone)
ladder phone-USB → phone-Wi-Fi → Starlink

Overnight anchor — a home-only, time-gated sub-rule of seamless (not a mode swap), so it can never strand you when you're traveling:

netmode overnight 0100 0700   # let backups ride Starlink overnight at home
netmode overnight off         # disable
netmode overnight             # show

Metering

Caps are shared across devices, so the provider's number is ground truth:

netmode setusage starlink 803   # anchor Starlink to the figure on the bill/app
netmode setusage phone 84
netmode report                  # usage + projection per link

Each metered link tracks its own cycle; "free" Wi-Fi is logged but never billed.

How it senses home vs away

Two pure decision functions fused from cheap signals (see docs/ARCHITECTURE.md):

  • Phone presencetethered | near | gone from USB link, hotspot SSID visibility, and Bluetooth proximity.
  • Locationhome | away | unknown from Starlink visibility, an ambient Wi-Fi fingerprint learned automatically at home, and the current connection class — with asymmetric hysteresis (leaving home, which re-permits Starlink, demands more proof than returning) and a strong-away fast-path (a healthy foreign scan leaves home in one sample, so you're never stranded at a café).

Architecture

            ┌──────────── sensing (pure fns + cheap IO) ────────────┐
            │  phone_present:  usb · ssid · bluetooth                 │
            │  location:       starlink-vis · fingerprint · conn      │
            └───────────────────────────┬───────────────────────────┘
                                         ▼
                       seamless policy  (home | away | unknown)
                                         ▼
        apply: service order · preferred-network rewrite · single-flight switch
                                         ▲
            fired by:  WatchPaths net-change LaunchAgent  +  5-min tick

Everything lives in netmode.sh (engine) and netui.py (dashboard). Runtime state and your real config stay on disk, git-ignored. No second process, no cloud. See docs/ARCHITECTURE.md and docs/DESIGN-PRINCIPLES.md.

Requirements

  • macOS (uses route, networksetup, ipconfig, system_profiler, ioreg, launchctl)
  • bash, python3 (dashboard + JSON helpers)
  • grpcurl (optional) — only for netmode dish read-only Starlink stats
  • No sudo. No Wi-Fi password is ever written to disk.

Privacy

This repository is private by design — netmode is bespoke personal infrastructure, and your real SSIDs, learned fingerprint, and usage live only in git-ignored files. Before ever making a fork public, run scripts/sanitize-for-public.sh and read SECURITY.md.

Contributing

It's a single-owner project, but it's run like a real one — feature branches, conventional commits, CI, and PRs. See CONTRIBUTING.md.

License

See LICENSE. Personal project; not licensed for redistribution.

About

Seamless multi-link manager for a macOS laptop across metered (iPhone hotspot, Starlink) and free connections — location-aware, no daemon.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages