Skip to content

NotAFood/corelocation_python

Repository files navigation

corelocation-socket

A macOS background daemon that reads your location via CoreLocation and serves it over a Unix domain socket. Comes with a Python client library.

Architecture

┌──────────────────────────┐
│  corelocation-socket.app │
│                          │
│  CoreLocation ──► JSON   │
│                   │      │
│            Unix Socket   │
└───────────────┬──────────┘
                │
    ~/Library/Application Support/
      corelocation-socket/location.sock
                │
        ┌───────┴───────┐
        │  Python code  │
        └───────────────┘

The daemon is packaged as a .app bundle (not a bare CLI) because macOS Ventura and later silently deny location access to non-bundled executables. The bundle contains an Info.plist with NSLocationUsageDescription, which is the only location key recognized on macOS (the WhenInUse/Always variants are iOS-only and ignored).

Installation

# Build, bundle, sign, install, and start the daemon
./scripts/install.sh

This will:

  1. Compile the Swift binary via SPM (swift build -c release)
  2. Package it into corelocation-socket.app with ad-hoc code signing
  3. Copy the app to ~/Applications/
  4. Install a launchd user agent that starts on login and restarts on crash
  5. Load the agent immediately

On first launch, macOS will prompt you to allow location access.

Uninstall

launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.user.corelocation-socket.plist
rm ~/Library/LaunchAgents/com.user.corelocation-socket.plist
rm -rf ~/Applications/corelocation-socket.app

Socket Protocol

The server communicates using JSON Lines (one JSON object per line, delimited by \n) over a Unix domain socket at:

~/Library/Application Support/corelocation-socket/location.sock

The protocol is inspired by gpsd's JSON protocol but stripped down for desktop use.

Message Types

TPV (Time-Position-Velocity) — location data from the daemon:

{"class":"TPV","lat":37.8712,"lon":-122.2628,"horizontal_accuracy":35,"timestamp":"2026-02-25T23:55:05.693Z","mode":2}
Field Type Description
class string Always "TPV"
lat float Latitude in decimal degrees
lon float Longitude in decimal degrees
horizontal_accuracy float Accuracy radius in meters
timestamp string ISO 8601 timestamp of the fix
mode int 0 = no fix, 2 = 2D fix (Wi-Fi/cell)

Mac desktops typically report mode: 2 (Wi-Fi-based positioning). There is no GPS hardware, so altitude/speed/course are not included.

POLL — request the current location:

{"class":"POLL"}

Send this to get an immediate response with the last known location. The server replies with a single TPV message (or an ERROR if no fix is available yet).

ERROR — returned when something goes wrong:

{"class":"ERROR","message":"No location data available"}

Behavior

  • On connect: The server accepts the connection and waits for input. It does not send a banner or greeting.
  • POLL request: Client sends {"class":"POLL"}\n. Server replies with one TPV or ERROR line.
  • Streaming: Connected clients receive broadcast TPV messages whenever CoreLocation delivers a new fix. On a typical Mac, this is roughly every 5 minutes via Wi-Fi positioning. The client does not need to send anything to receive broadcasts — just stay connected.
  • Multiple clients: The server supports concurrent connections. Each client independently receives broadcasts and can send POLL requests.
  • Disconnect: Clients can close the connection at any time. The server cleans up automatically.

Raw socket example

# One-shot query
echo '{"class":"POLL"}' | socat - UNIX-CONNECT:"$HOME/Library/Application Support/corelocation-socket/location.sock"

# Stream updates (stays connected)
socat - UNIX-CONNECT:"$HOME/Library/Application Support/corelocation-socket/location.sock"

Python Client

Install the client library:

pip install -e .

One-shot location

from corelocation_client import get_location

loc = get_location()
print(f"{loc['lat']}, {loc['lon']}{loc['horizontal_accuracy']}m)")

get_location() opens a connection, sends POLL, reads the response, and closes. It raises ConnectionError if the daemon isn't running, or if no location fix is available yet.

Streaming

import asyncio
from corelocation_client import stream_locations

async def main():
    async for loc in stream_locations():
        print(f"{loc['lat']}, {loc['lon']}")

asyncio.run(main())

stream_locations() holds the connection open and yields each TPV as it arrives. It sends an initial POLL on connect, then listens for broadcasts.

API Reference

get_location(socket_path: str | None = None) -> LocationData

Synchronous. Returns the current location or raises ConnectionError / TimeoutError.

async stream_locations(socket_path: str | None = None) -> AsyncIterator[LocationData]

Async generator. Yields LocationData dicts as updates arrive. Skips malformed messages automatically.

class LocationData(TypedDict):
    lat: float
    lon: float
    horizontal_accuracy: float
    timestamp: str   # ISO 8601
    mode: int        # 0 = no fix, 2 = 2D fix

Both functions default to ~/Library/Application Support/corelocation-socket/location.sock. Pass socket_path to override.

Daemon Management

# Check if running
launchctl list | grep corelocation

# View logs
tail -f /tmp/corelocation-socket.log

# Restart
launchctl kickstart -k gui/$(id -u)/com.user.corelocation-socket

# Stop
launchctl bootout gui/$(id -u)/com.user.corelocation-socket

The daemon is configured with KeepAlive: true, so launchd will restart it if it crashes.

Troubleshooting

"No location data available" — CoreLocation hasn't delivered a fix yet. This is normal for the first few seconds after startup. Retry shortly.

ConnectionError: Failed to connect — The daemon isn't running. Check launchctl list | grep corelocation and the log file.

Location permission denied — Open System Settings → Privacy & Security → Location Services. Find corelocation-socket and enable it. You may need to restart the daemon afterward.

Stale socket file — If the daemon crashed without cleanup, the socket file may be left behind. The daemon removes it on startup, but you can manually delete it:

rm ~/Library/Application\ Support/corelocation-socket/location.sock

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published