{"id":50447705,"url":"https://github.com/testica/garmin-bicimad","last_synced_at":"2026-05-31T22:32:10.480Z","repository":{"id":361683838,"uuid":"1255328881","full_name":"testica/garmin-bicimad","owner":"testica","description":"Unofficial BiciMAD app for gamin","archived":false,"fork":false,"pushed_at":"2026-05-31T19:17:43.000Z","size":56,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-31T21:13:34.019Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Monkey C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/testica.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-31T17:38:29.000Z","updated_at":"2026-05-31T19:17:47.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/testica/garmin-bicimad","commit_stats":null,"previous_names":["testica/garmin-bicimad"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/testica/garmin-bicimad","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/testica%2Fgarmin-bicimad","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/testica%2Fgarmin-bicimad/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/testica%2Fgarmin-bicimad/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/testica%2Fgarmin-bicimad/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/testica","download_url":"https://codeload.github.com/testica/garmin-bicimad/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/testica%2Fgarmin-bicimad/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33752286,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-31T02:00:06.040Z","response_time":95,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-31T22:32:07.074Z","updated_at":"2026-05-31T22:32:10.466Z","avatar_url":"https://github.com/testica.png","language":"Monkey C","funding_links":[],"categories":[],"sub_categories":[],"readme":"# BiciMAD — Garmin Connect IQ Watch App\n\nA native Garmin smartwatch app for the **BiciMAD** bike-share system in Madrid.  \nFind nearby stations, check availability, unlock bikes by plate number, and view your trip history — all from your wrist.\n\n\u003e **Disclaimer:** This is an unofficial third-party app. BiciMAD and EMT Madrid are trademarks of Empresa Municipal de Transportes de Madrid S.A.\n\n---\n\n## Features\n\n| Feature | Description |\n|---------|-------------|\n| **Stations by GPS** | Find the nearest BiciMAD stations sorted by walking distance |\n| **Search by name** | Type a station name and get matching results |\n| **Unlock by plate** | Enter a bike's plate number — the app verifies it and physically unlocks the dock |\n| **Trip history** | See your last 5 trips and any active trip in progress |\n| **Secure login** | Authenticates with your BiciMAD account via the MPass API |\n| **Persistent session** | Token stored on-device — no need to login on every use |\n| **Bilingual** | Full English and Spanish support (auto-selected by device language) |\n\n---\n\n## Screenshots / Flow\n\n```\nMain Menu\n├── View Stations\n│   ├── Nearby (GPS) ──→ ProgressBar ──→ Station list (native Menu2)\n│   │                     \"Metro Callao\"\n│   │                     11/25 · 57m\n│   └── Search by Name ──→ TextPicker ──→ ProgressBar ──→ Station list\n├── Trips  (logged in only)\n│   ├── Active Trip ──→ Bike #, departure station, start time\n│   └── History     ──→ Last 5 trips: date, station, cost, duration\n├── Unlock Bike\n│   ├── Plate  ──→ TextPicker (type plate number)\n│   └── Unlock ──→ ProgressBar (verify) → Confirmation → ProgressBar (unlock) → Result\n└── Sign In / Sign Out\n    ├── Email    ──→ TextPicker (native keyboard via phone)\n    ├── Password ──→ TextPicker\n    └── Connect  →\n```\n\n---\n\n## Architecture\n\n### Watch App (Monkey C / Connect IQ)\n\nBuilt with Garmin's **Connect IQ SDK 3.2.0+** using native UI components throughout:\n\n| Component | Used for |\n|-----------|----------|\n| `WatchUi.Menu2` | Main menu, login form, search form, station results, trip history |\n| `WatchUi.TextPicker` | Email, password, plate number, station name search |\n| `WatchUi.Confirmation` | Unlock confirmation dialog |\n| `WatchUi.ProgressBar` | All loading states (GPS, network, unlock) |\n\n### Proxy Backend (Node.js / Cloudflare Workers)\n\nA lightweight proxy server that bridges the watch and the EMT Madrid API.\n\n**Why is a proxy needed?**\n1. **Network routing** — Garmin routes all `makeWebRequest()` calls through its own infrastructure, which cannot reach `apiemtpay.emtmadrid.es` directly (returns 404).\n2. **Response size** — The EMT stations API returns 632 stations (~296 KB); the Garmin watch buffer is ~32 KB.\n3. **DES cryptography** — The bike unlock flow requires a hashcode computed with DES encryption, reverse-engineered from the official APK.\n\n```\nGarmin Watch → Garmin Servers → Proxy (Cloudflare/Node.js) → EMT Madrid API\n```\n\n---\n\n## API Endpoints\n\nAll endpoints are under `/api`. Deploy the proxy to any Node.js host or Cloudflare Workers.\n\n---\n\n### `GET /api/stations`\n\nReturns BiciMAD stations sorted by proximity or filtered by name.  \nReduces the full 296 KB EMT response to \u003c5 KB for the watch.\n\n| Parameter | Type | Description |\n|-----------|------|-------------|\n| `filter` | `coordinates` \\| `name` | Search mode |\n| `value` | `lat,lon` or text | GPS coordinates or station name fragment |\n| `limit` | number | Max results (default 15, max 30) |\n\n```\nGET /api/stations?filter=coordinates\u0026value=40.4168,-3.7038\u0026limit=10\nGET /api/stations?filter=name\u0026value=callao\n```\n\n**Response:**\n```json\n[\n  { \"id\": \"1406\", \"name\": \"2 - Metro Callao\", \"bikes\": 11, \"slots\": 14, \"dist\": 57 },\n  { \"id\": \"1428\", \"name\": \"25A - Plaza de Celenque A\", \"bikes\": 6, \"slots\": 15, \"dist\": 179 }\n]\n```\n\n**Upstream:** `GET https://openapi.emtmadrid.es/v2/transport/bicimad/stations/`  \nUses an anonymous app token — no user account required.\n\n---\n\n### `GET /api/trips`\n\nReturns the user's trip history compacted from ~65 KB to ~7 KB.  \nA trip with `active: true` has no `dock` — the user is currently riding.\n\n| Parameter | Type | Description |\n|-----------|------|-------------|\n| `token` | string | User's `accessToken` |\n| `userId` | string | User's ID |\n\n```\nGET /api/trips?token=ACCESS_TOKEN\u0026userId=USER_ID\n```\n\n**Response:**\n```json\n{\n  \"code\": \"00\",\n  \"data\": [\n    {\n      \"id\": \"41437512\",\n      \"bike\": \"00015858\",\n      \"mins\": 16.2,\n      \"cost\": 0.50,\n      \"active\": false,\n      \"undock\": { \"name\": \"250 - Serrano - CSIC\", \"ts\": \"2026-05-29 20:32\" },\n      \"dock\":   { \"name\": \"17 - Plaza de Carlos Cambronero\", \"ts\": \"2026-05-29 20:48\" }\n    }\n  ]\n}\n```\n\n**Upstream:** `GET https://apiemtpay.emtmadrid.es/v1/bicimad/trips/`  \nHeaders: `accessToken`, `userId`, `mode: mPass`\n\n---\n\n### `GET /api/check`\n\nVerifies a bike by plate number and returns its current location.  \nUsed to show the user bike details before confirming an unlock.\n\n| Parameter | Type | Description |\n|-----------|------|-------------|\n| `plate` | string | Bike plate number (e.g. `14802`) |\n| `token` | string | User's `accessToken` |\n| `userId` | string | User's ID |\n\n```\nGET /api/check?plate=14802\u0026token=ACCESS_TOKEN\u0026userId=USER_ID\n```\n\n**Response:**\n```json\n{\n  \"code\": \"00\",\n  \"data\": { \"number\": \"14802\", \"docker\": \"802\", \"fleet\": 1, \"lat\": 40.4239, \"lon\": -3.7020 }\n}\n```\n\n`docker` = anchor/dock ID within the station. `fleet`: `1` = BiciMAD Classic, `2` = BiciMAD Go.\n\n**Upstream:** `GET https://apiemtpay.emtmadrid.es/v1/checkresource/bicimad/{plate}/`\n\n---\n\n### `GET /api/unlock`\n\n**Physically unlocks a specific bike** by plate number, starting a trip.  \nThis is the most complex endpoint — it replicates the full DES encryption flow from the official BiciMAD APK (reverse-engineered via `jadx` + `objdump`).\n\n\u003e **Note:** After a successful unlock (`code: 00`), the dock releases the bike. You have a few minutes to remove the bike before the dock re-locks it automatically.\n\n| Parameter | Type | Description |\n|-----------|------|-------------|\n| `plate` | string | Bike plate number |\n| `token` | string | User's `accessToken` |\n| `userId` | string | User's ID |\n| `lat` | float | User's latitude |\n| `lon` | float | User's longitude |\n\n```\nGET /api/unlock?plate=14802\u0026token=ACCESS_TOKEN\u0026userId=USER_ID\u0026lat=40.4239\u0026lon=-3.7020\n```\n\n**Response:**\n```json\n{ \"code\": \"00\", \"description\": \"RELEASE OK\", \"bike\": \"14802\", \"docker\": \"802\" }\n```\n\n**Full flow (3 steps):**\n\n**1. Verify bike**\n```\nGET https://apiemtpay.emtmadrid.es/v1/checkresource/bicimad/{plate}/\n```\nReturns `bikeNumber`, `docker`, GPS coordinates, fleet type.\n\n**2. Compute hashcode** — reverse-engineered from `QRService.cifrarHashcode()` in `EMTingSDK`:\n```\nplaintext = bikeNumber + \"#\" + docker + \"#\" + lon10 + \"#\" + lat10 + \"#U#\" + userId\npadded    = plaintext padded to multiple of 8 with \"#\"\nstep1     = \"B\" + DES_ECB(padded,  userId.toUpperCase()[0:8])   → HEX UPPERCASE\nstep1     = step1 padded to multiple of 8 with \"Z\"\nhashcode  = DES_ECB(step1, operatorId.toUpperCase()[0:8])       → BASE64\n```\nWhere `operatorId = \"b6cf40a4-6130-439f-9917-15654c79c22e\"` (from `Constants.OPERATOR_ID` in APK).  \nKeys use `Utils.getEightFirstChars()` = `.toUpperCase().substring(0, 8)`.\n\n**3. Sell ticket**\n```\nPUT https://apiemtpay.emtmadrid.es/v2/payment/qrcodesdk/sellticket/\nHeaders: accessToken, hashcode, latitude, longitude, userId, operatorId, ...\n```\nThe server validates the hashcode, signals the PBSC dock system, and releases the bike.\n\n\u003e **Discovery notes:**  \n\u003e - Must be `PUT` (method=2 in Volley), not `POST`  \n\u003e - Must use `v2` endpoint, not `v1` (`v1` returns `\"Not valid xClientId\"`)  \n\u003e - DES keys must be **UPPERCASE** (`Utils.getEightFirstChars` calls `.toUpperCase()`)  \n\u003e - `apiemtpay.emtmadrid.es` is blocked by Garmin's HTTP infrastructure — proxy is required\n\n---\n\n## Authentication\n\n### Anonymous token (stations)\n\nUsed by the proxy to fetch station data. No user account needed.\n\n```\nGET https://openapi.emtmadrid.es/v2/mobilitylabs/user/login/\nHeaders: X-ClientId, passKey  (app credentials, no email/password)\n```\n\n`X-ClientId` and `passKey` are extracted from the official BiciMAD APK via `libkeys.so` disassembly using `objdump`. They identify the app, not any individual user.\n\n### User token (trips and unlock)\n\nThe watch authenticates with the user's BiciMAD account:\n\n```\nGET https://openapi.emtmadrid.es/v2/mobilitylabs/user/login/\nHeaders: X-ClientId, passKey, email, password\n```\n\nThe `accessToken` is stored in `Application.Storage` and persists across restarts. It expires after 30 days and is checked on every app launch.\n\n\n\n## Setup\n\n### Watch App\n\n1. Install the [Garmin Connect IQ SDK](https://developer.garmin.com/connect-iq/sdk/)\n\n2. **Install [TinyMetrix](https://tinymetrix.com) barrel** — Required for analytics and crash reporting\n\n   **Option A: Using VSCode (Recommended)**\n\n   1. Open the project in VSCode with the Garmin Connect IQ extension\n   2. Press `Cmd+Shift+P` (Mac) or `Ctrl+Shift+P` (Windows/Linux)\n   3. Type and select `Monkey C: Configure Monkey Barrel`\n   4. Download the [TinyMetrix](https://tinymetrix.com) barrel from: https://tinymetrix.com/assets/binaries/tinymetrix-2.1.6.barrel\n   5. Select the downloaded `.barrel` file when prompted\n\n   **Option B: Manual installation**\n\n   1. Download the barrel: https://tinymetrix.com/assets/binaries/tinymetrix-2.1.6.barrel\n   2. Create a `barrels` directory in your project root if it doesn't exist\n   3. Copy `tinymetrix-2.1.6.barrel` to the `barrels/` folder\n   4. The barrel is already configured in `manifest.xml`:\n      ```xml\n      \u003ciq:barrels\u003e\n          \u003ciq:depends name=\"Tinymetrix\" version=\"2.1.6\"/\u003e\n      \u003c/iq:barrels\u003e\n      ```\n\n3. **Configure properties file**\n\n   Copy the example properties file:\n   ```bash\n   cp resources/properties.xml.example resources/properties.xml\n   ```\n\n   The example file includes a `MOCK_TOKEN` that works for development. For production, edit `resources/properties.xml` and replace `MOCK_TOKEN` with your actual [TinyMetrix](https://tinymetrix.com) token.\n\n### Proxy Backend\n\n```bash\ncd server\nnpm install\nnpm start        # runs on http://localhost:3000\n```\n\nSet the `PORT` environment variable to change the port.\n\n**Deploy to production** (Railway, Render, Fly.io, Cloudflare Workers, etc.):\n```monkey-c\n// BiciMadService.mc — update these URLs after deploying\nprivate const URL_PROXY  = \"https://your-proxy.example.com/api/stations\";\nprivate const URL_TRIPS  = \"https://your-proxy.example.com/api/trips\";\nprivate const URL_CHECK  = \"https://your-proxy.example.com/api/check\";\nprivate const URL_UNLOCK = \"https://your-proxy.example.com/api/unlock\";\n```\n\n### Update Station Coordinates\n\nThe app bundles static coordinates for all 632 stations (`source/StationsData.mc`) to avoid loading 296 KB over the watch's network buffer. Refresh when EMT adds new stations (~once a year):\n\n```bash\n./update_stations.sh\n```\n\nFetches fresh data from the [GBFS feed](https://madrid.publicbikesystem.net/customer/gbfs/v3.0/station_information) and regenerates `StationsData.mc`.\n\n---\n\n## Project Structure\n\n```\nbicimad/\n├── source/                      # Monkey C source (watch app)\n│   ├── bicimadApp.mc            # App entry point, token storage, session management\n│   ├── bicimadView.mc           # Main menu (Menu2) + delegate\n│   ├── BiciMadService.mc        # All API calls: login, stations, trips, check, unlock\n│   ├── StationsData.mc          # Static station coordinates (auto-generated, 632 stations)\n│   ├── StationListView.mc       # Station search: GPS proximity + name search\n│   ├── LoginView.mc             # Login form (Menu2 + TextPicker)\n│   ├── PlateSearchView.mc       # Unlock bike by plate: input → verify → confirm → result\n│   ├── TripsView.mc             # Trip history + active trip detail\n│   ├── ReservationView.mc       # Station booking flow\n│   └── PositionManager.mc       # GPS location handler\n├── resources/\n│   ├── strings/strings.xml      # Default strings (English fallback)\n│   ├── layouts/layout.xml       # Base layout\n│   └── menus/menu.xml           # Menu resources\n├── resources-eng/               # English UI strings\n│   └── strings/strings.xml\n├── resources-spa/               # Spanish UI strings\n│   └── strings/strings.xml\n├── server/                      # Proxy backend (Node.js)\n│   ├── index.js                 # Express server — all 4 API endpoints\n│   └── package.json\n├── manifest.xml                 # App manifest (permissions, 140+ target devices)\n├── monkey.jungle                # Build configuration\n└── update_stations.sh           # Refresh station coordinates from GBFS\n```\n\n---\n\n## License\n\nMIT — see [LICENSE](LICENSE) for details.\n\nThis project is not affiliated with, endorsed by, or connected to EMT Madrid or Garmin.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftestica%2Fgarmin-bicimad","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftestica%2Fgarmin-bicimad","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftestica%2Fgarmin-bicimad/lists"}