an open source Park Smarter alternative. Works with IPS parking meters.
Find a file
Erik 148e1635d3
All checks were successful
build-apk / build (push) Successful in 10m38s
v0.6.0: city parking-map overlay + local time tracking, no IPS API
Adds the City of Sandpoint's printed "Downtown & Waterfront Public Parking"
map as a georeferenced overlay, and lets you track your time on any of its
areas without ever touching the ParkSmarter/IPS API.

Georeferencing (tools/citymap/)
- The PDF carries no geo metadata, so the page->WebMercator affine is
  recovered by fitting the drawing to OSM street centrelines.
- pdftocairo writes stroked street segments with per-path matrix()
  transforms in local coords while filled lots are absolute; both are
  handled. The five legend swatches share the real geometry's colours and
  are identified by stroke-width and position, then dropped.
- 49 areas, fitted to RMS 4.1 m (X) / 3.5 m (Y). On-street segments land a
  mean 4.0 m from the nearest OSM road. sp-039/040 sit further out because
  they are angled bays along the old rail corridor, on no named road at all.
- Sandpoint's grid jogs 38 m between N 2nd Ave and S 2nd Ave; the page shows
  the same jog at the fitted scale, which independently confirms the fit.

App
- Map tab: "City map" layer in the legend's colours, tappable.
- "Park here" pins the car from GPS and auto-detects the containing area
  (40 m snap). With no fix it asks you to tap the spot instead, so the pin
  never depends on GPS working.
- The pin lives in its own storage key, not inside the session: pinning the
  car without starting a timer must survive backing out of the screen.
- Durations cap at the posted limit — a 2-hour space is not offered a
  4-hour timer. Lots and no-limit spots get the long options.
- Reuses the existing foreground-service countdown. The second notification
  button reads "+1 hr" for a city area rather than "Extend": there is
  nothing to buy, so it edits the local timer and says so.
- Account -> Align city map: nudge/scale/rotate the whole overlay against a
  live GPS fix. Save-on-phone needs no admin token, since the person who can
  see the misalignment is the one standing on the street.

Server
- parking_areas + map_overlay tables, public read, admin replace-all. The
  areas come from one source document, so replacement is wholesale rather
  than an upsert.

Dropped geometryCenter from the geo module: on the real data it returns a
point in the water for the crescent City Beach lot and mid-block for
L-shaped runs. Nothing used it.

Tests: 8 geometry tests in app/, 5 area/overlay tests in server/.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 03:28:18 +00:00
.forgejo/workflows CI: pre-install NDK+CMake with retry (SDK mirror served a corrupt CMake zip) 2026-07-13 18:42:56 -07:00
app v0.6.0: city parking-map overlay + local time tracking, no IPS API 2026-08-13 03:28:18 +00:00
docs docs: RE report + correct the privacy writeup from live analysis 2026-07-16 19:11:26 +00:00
parksmarter-client v0.2.4: fix countdown notification not appearing + wrong time-remaining 2026-07-15 21:55:45 +00:00
scripts CI: dedicated cached runner for this repo (deploy script + volume caches) 2026-07-08 11:05:35 -07:00
server v0.6.0: city parking-map overlay + local time tracking, no IPS API 2026-08-13 03:28:18 +00:00
tools/citymap v0.6.0: city parking-map overlay + local time tracking, no IPS API 2026-08-13 03:28:18 +00:00
.gitignore Release signing fix + Obtainium install guide + publish script 2026-07-07 13:27:19 -07:00
INSTALL.md docs: make the Obtainium add link a real clickable link 2026-07-07 14:02:28 -07:00
package-lock.json v0.6.0: city parking-map overlay + local time tracking, no IPS API 2026-08-13 03:28:18 +00:00
package.json Initial commit: parksmarter-client + BigBrainParking app 2026-07-06 08:33:47 -07:00
README.md v0.6.0: city parking-map overlay + local time tracking, no IPS API 2026-08-13 03:28:18 +00:00

BigBrainParking — big-brain driver

BigBrainParking

An unofficial, de-Googled client for the ParkSmarter (IPS Group) parking system, built to run on GrapheneOS with UnifiedPush notifications and distributed via Obtainium — no Google Play Services, no Firebase, no app store.

Install it (users)

BigBrainParking installs and updates through Obtainium — no Google Play, no account. Works on GrapheneOS. On your phone (with Obtainium installed), tap:

Add BigBrainParking to Obtainium

or follow INSTALL.md for the manual steps.

Monorepo layout

bigbrainparking/
├── parksmarter-client/   # Zero-dep TypeScript API client (reverse-engineered, live-verified)
├── app/                  # Expo / React Native app (BigBrainParking)
└── .gitea/workflows/     # CI: build signed APK -> publish release for Obtainium
  • parksmarter-client — the API layer. All ~40 endpoints, the custom-header auth model, and response models (most verified against production). Runs anywhere; the app imports it directly. See its own README for the API details.
  • app — the phone app. React Native (Expo prebuild), MapLibre maps, VisionCamera QR scanning, expo-secure-store token storage, local session-expiry reminders, and UnifiedPush wiring.

Features

Feature Status Notes
Phone + password login wired tokens in OS keystore
Map of nearby meters (clickable, zoom, live GPS) wired MapLibre + OpenFreeMap tiles (no key)
City parking map overlay (2h/3h/4h/no-limit/lots) wired the city's printed map, georeferenced — never touches the IPS API
"Park here" pin + time tracking on any city area wired GPS or hand-placed pin, auto-detects the area, local countdown
Last-lot + My-location search wired opens on your last session's lot; GPS sent only via explicit "My location"
QR kiosk scan → meter lookup wired on-device VisionCamera
Save / share kiosks wired local (no server favorites API exists)
Active / past sessions wired list views + tap for full receipt
Start a paid session works confirmed live end-to-end (real $0.10 DL-zone charge); declines surfaced
Ongoing parking countdown wired foreground service; ticks down, End / Extend buttons, survives reboot
Session-expiry reminders wired local on-device notifications — no server, no push
UnifiedPush (ntfy) optional not needed for reminders; stub for future server-initiated msgs

The city parking map

The City map layer on the Map tab is the City of Sandpoint's printed Downtown & Waterfront Public Parking map, georeferenced and drawn in the same colours as the legend: 2-hour free, 3-hour, 4-hour, no time limit, and the paid city lots. 49 areas in all.

None of it touches ParkSmarter. The areas live in the local database (bundled with the app, refreshed from the zone-labels server, cached on-device), the countdown is the phone's own clock, and the notification is the same foreground service every other session uses. So tracking your time on a city spot works with no account, no signal, no payment, and in Anonymous Mode. Two ways to start:

  • Park here — pins your car from GPS and works out which area you're in. No GPS fix (garage, indoors, radio off)? It asks you to tap the spot instead and pins that. The pin stays on the map until you end the session, because "where did I leave the car" is half the point.
  • Tap a coloured segment — pick the block directly, no pin needed.

Either way you choose how long to track, capped at the posted limit (a 2-hour space won't offer to run a 4-hour timer — that's just scheduling a ticket). The ongoing notification's second button reads +1 hr here rather than Extend: there is nothing to buy, so it edits the local timer and says so.

The georeference was fitted to OpenStreetMap street centrelines and lands within ~4 m (see tools/citymap/ to regenerate it from a new edition of the PDF). Because a few metres is the difference between two sides of a street, Account → Align city map lets you nudge the whole overlay against a live GPS fix and save it — on the phone, or published to the server for every device if you hold the admin token.

Privacy

BigBrainParking sends your location to ParkSmarter only when you explicitly tap "My location" and search; it opens on your last parking lot instead of your GPS, and ships no analytics/tracking (no Segment/Amplitude/Firebase/Sentry, no ad-ID, no Google services). For a plain-language comparison with the official app — which auto-sends your GPS on map open and bundles that tracking stack — see docs/OFFICIAL_APP_PRIVACY.md. What reaches the API and what never does is also spelled out in the app's About page.

Build & run (dev)

Requires Node 20, JDK 17, Android SDK, and a GrapheneOS device (or any Android device) with USB debugging.

npm install                                   # installs both workspaces
npm run build --workspace parksmarter-client  # compile the client
cd app
npx expo prebuild --platform android          # generate native project
npx expo run:android                          # build + install a dev client

The app talks to prod (apiv2.parksmarter.com) by default — change extra.psEnvironment in app/app.json to point elsewhere.

Notifications on GrapheneOS

Everything here is entirely on-device — no server, no push, no FCM, no Play Services — so it works fully offline. UnifiedPush (ntfy) is wired only as an optional, no-op stub for any future server-initiated messages; nothing time-based needs it.

The ongoing parking countdown. Whenever a session is active — a paid one you bought or a free check-in — a persistent notification shows the time left and ticks down, with End and Extend buttons. It is held up by a real foreground service (modules/bbp-notify, type specialUse), which is what makes it stick on GrapheneOS the way ntfy's does: it survives the app being killed, can't be swiped away, and a BOOT_COMPLETED receiver brings it back after a reboot. The service's life is exactly the session's life — it stops itself, removing the notification, on End, when the meter runs out, or whenever there's no session to show.

The countdown itself costs no battery: the end time is handed to Android as a chronometer, and the system redraws the ticking text with the app closed and no timer of its own.

  • End stops tracking and clears the notification. On a free check-in that genuinely ends it. On a paid session it only stops the display — ParkSmarter has no stop-session endpoint, so time you already bought keeps running at the meter either way.
  • Extend (labeled Pay on a free check-in) opens the purchase screen for that exact zone. The active session is stored locally with its zone, so this works offline, in Anonymous Mode, and after a reboot. Extending doesn't end anything until the purchase actually goes through.

Expiry reminders fire a configurable lead time (default 15 min) before the end, via AlarmManager. Configure them in Account → Notifications, where a "Send a test reminder" button lets you confirm they fire on your phone; the same screen has a toggle for the ongoing countdown.

Distribution via Obtainium (self-hosted)

Tag a release and CI builds a signed APK and publishes it to this repo's releases; your phone's Obtainium tracks the repo and offers updates. See docs/DISTRIBUTION.md for the full server + CI + keystore setup.