{"id":573,"date":"2026-10-03T18:48:14","date_gmt":"2026-10-03T17:48:14","guid":{"rendered":"https:\/\/www.fabidouille.com\/?p=573"},"modified":"2026-10-03T19:38:46","modified_gmt":"2026-10-03T18:38:46","slug":"aa-proxy-pi-zero","status":"publish","type":"post","link":"https:\/\/www.fabidouille.com\/?p=573","title":{"rendered":"Z\u00e9ro de Conduite"},"content":{"rendered":"<p>The wireless Android Auto dongle of my 2015 Mazda 2 died. Instead of buying a new one, I replaced it with a Raspberry Pi Zero W running aa-proxy-rs, an open source project.<\/p>\n<p>It does the same job as the dongle, with one big difference: the Pi sits in the middle of the conversation between the phone and the car, and it can read and change everything they say to each other. Remove the tap limits, change the screen size, fake data&#8230; That much control in a board this small is a little scary. It even convinced Waze that I was always driving at 0 km\/h.<\/p>\n<p>And because it is in the middle, the car also became a source of data: I now log my trips and the Android Auto crashes, in a car with no internet.<\/p>\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" src=\"https:\/\/www.fabidouille.com\/wp-content\/uploads\/2026\/10\/pizero.jpg\" alt=\"The Raspberry Pi Zero W in a white 3D-printed case, with two micro-USB cables: one for power, one for data to the car\"\/><figcaption>The whole Android Auto bridge: a Pi Zero W in a 3D-printed case, and two cables. One for power, one for data.<\/figcaption><\/figure>\n<h2 class=\"wp-block-heading\">The setup<\/h2>\n<ul class=\"wp-block-list\">\n<li>Raspberry Pi Zero W v1.1: BCM2835, one ARM11 core at 1 GHz, 512 MB RAM, 2.4 GHz Wi-Fi, Bluetooth 4.2<\/li>\n<li>aa-proxy-rs, an open source Android Auto proxy written in Rust (image <code>rpi0w-sdcard.img.xz<\/code>, v0.22.0 then v1.0.0)<\/li>\n<li>the car: Mazda 2 (2015) with the Android Auto retrofit, Mazda Connect firmware 74.00.324<\/li>\n<li>the phone: a Realme GT 6<\/li>\n<\/ul>\n<p>The phone talks to the Pi over Bluetooth and Wi-Fi. The Pi talks to the car over USB, in gadget mode: for the car, the Pi is the phone. For the phone, the Pi is the car. This is a man-in-the-middle (MITM) setup, with its own TLS certificates.<\/p>\n<p>One trap before anything else: the Pi Zero W has two micro-USB ports. The one on the edge, marked \u00ab\u00a0USB\u00a0\u00bb, is the data port, it goes to the car. \u00ab\u00a0PWR\u00a0\u00bb is power only. And many cheap USB cables have no data wires at all.<\/p>\n<h2 class=\"wp-block-heading\">Three problems before the first drive<\/h2>\n<p>1. The old microSD card. FAT errors in dmesg, so a full read test:<\/p>\n<pre class=\"wp-block-code\"><code>dd: error reading '\/dev\/sde': Input\/output error\n118456320 bytes (118 MB, 113 MiB) copied, 143,775 s, 824 kB\/s<\/code><\/pre>\n<p>A hard I\/O error after 118 MB of 7.3 GB. Dead card. New card.<\/p>\n<p>2. A boot loop. The image uses two root partitions (A\/B: <code>mmcblk0p2<\/code> and <code>mmcblk0p3<\/code>, chosen by U-Boot). My first config changes went to only one of them, and <code>\/etc\/shadow<\/code> was replaced by a file with one single line (127 bytes instead of the 207 of the original). Fix: same <code>config.toml<\/code> on both partitions, and patch the one line in <code>\/etc\/shadow<\/code> instead of replacing the file. The Pi booted, the Wi-Fi access point came up, the web UI answered on <code>http:\/\/10.0.0.1<\/code>.<\/p>\n<p>3. In the car: \u00ab\u00a0Communication error 8 &#8211; Your car&rsquo;s software didn&rsquo;t pass Android Auto security checks\u00a0\u00bb. This is Android Auto&rsquo;s TLS error. In MITM mode, aa-proxy-rs shows its own certificates, and the source code says which one each side checks:<\/p>\n<pre class=\"wp-block-code\"><code>let prefix = match proxy_type {\n    ProxyType::HeadUnit     =&gt; \"md\",   \/\/ TLS server: the phone connects to us -&gt; md_cert.pem\n    ProxyType::MobileDevice =&gt; \"hu\",   \/\/ TLS client: we connect to the car     -&gt; hu_cert.pem\n};<\/code><\/pre>\n<p>The phone checks <code>md_cert.pem<\/code>. The well-known community certificate I had expired in August 2022, and today&rsquo;s Android Auto checks the date. The aa-proxy community shares valid ones. New certificate, no more error 8. And a date to watch: these certificates expire too.<\/p>\n<h2 class=\"wp-block-heading\">Waze at 0 km\/h<\/h2>\n<p>First real drives: Waze always said 0 km\/h. I took the session logs from the Pi&rsquo;s SD card and counted: 220 <code>speed_e3<\/code> values out of 220 were exactly zero, while the latitude and longitude were moving. My car never drove so slowly.<\/p>\n<p>The firmware? No: the setting was there 3 weeks before the firmware update. The phone? No. The cause was in the proxy itself, in <code>mitm.rs<\/code>. The <code>video_in_motion<\/code> option makes Android Auto believe the car is parked, so video works while driving. To do that, it sets to zero the speed, the bearing, the accelerometer, the gyroscope, the compass and the RPM. It fakes a parked car so well that Waze believes it too.<\/p>\n<p>I did not even want video while driving. What I wanted was no tap restriction. But the same file showed a second trap: <code>remove_tap_restriction<\/code> also removes the speed sensor, unless <code>collect_speed<\/code> is on. And the companion app&rsquo;s own help text says it clearly: <code>collect_speed<\/code> \u00ab\u00a0by design disables <code>remove_tap_restriction<\/code>\u00ab\u00a0. So it is one or the other: the speed in Waze, or the tap trick. I chose the speed. The fix, four lines of config, no new binary:<\/p>\n<pre class=\"wp-block-code\"><code>video_in_motion = false          # the cause: fakes a parked car\nremove_tap_restriction = true    # still on, but see the next line\ncollect_speed = true             # keeps the speed; by design disables remove_tap_restriction\ndisable_driving_status = true    # keeps the \"no driving restriction\" part of video_in_motion<\/code><\/pre>\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" src=\"https:\/\/www.fabidouille.com\/wp-content\/uploads\/2026\/10\/settings.png\" alt=\"The aa-proxy companion app on the phone, connected to the Pi: video_in_motion switched off, remove_tap_restriction on, collect_speed on, whose help text says it disables remove_tap_restriction by design\"\/><figcaption>The live settings, read from the Pi with the aa-proxy companion app.<\/figcaption><\/figure>\n<p>Waze knows my speed again. (aa-proxy-rs v1.0.0 also added <code>level_video_in_motion = \"low\"<\/code>, which only fakes the gear lever, PARK, and keeps the speed.)<\/p>\n<p>While the Pi was on my desk, I also changed the CPU governor from <code>ondemand<\/code> to <code>performance<\/code>, with a small init script so it stays after reboot. One ARM11 core has no margin.<\/p>\n<h2 class=\"wp-block-heading\">Logging trips without internet<\/h2>\n<p>I wanted to log my trips. But while driving, the Pi is the Wi-Fi access point for the phone: it has no internet. So the phone does the work:<\/p>\n<pre class=\"wp-block-code\"><code>phone plugin (GPS, SQLite queue)\n    --HTTPS + token--&gt; my server: small API --&gt; SQLite + VictoriaMetrics --&gt; Grafana\nhome server, every 15 min: cuts the points into trips --&gt; commits a trip log to git\nGoogle Find Hub tag in the car --&gt; read every 5 min --&gt; same API<\/code><\/pre>\n<ul class=\"wp-block-list\">\n<li>A small aa-proxy companion plugin on the phone saves GPS points in a local SQLite queue, and sends them in batches. A batch is deleted only when the server answers OK. No network in the mountains? The queue just gets longer.<\/li>\n<li>On the server, a small API stores the points and pushes them to VictoriaMetrics. Every 15 minutes, another script cuts them into trips and commits a trip log to a git repository.<\/li>\n<li>For the position when the car is parked, a Google Find Hub tag. There is no official API, so a community tool (GoogleFindMyTools) decrypts its position.<\/li>\n<\/ul>\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" src=\"https:\/\/www.fabidouille.com\/wp-content\/uploads\/2026\/10\/trip-app.png\" alt=\"The trip logger plugin on the phone: buttons to save, stop, send now, disable battery optimisation and allow location always, then the status: logging on, service running, background location yes, in the Mazda no, server configured yes, 0 points waiting, 48676 points recorded\"\/><figcaption>The phone plugin (in French): 48,676 points recorded, 0 waiting.<\/figcaption><\/figure>\n<p>Two PromQL traps found on the way, useful for any Grafana user:<\/p>\n<ul class=\"wp-block-list\">\n<li><code>max - min<\/code> over 365 days on irregular manual readings gave half the real value (the oldest point in the window was only 6 months old). <code>deriv()<\/code> fits a line through the points and gives the right answer.<\/li>\n<li><code>deriv()<\/code> returns a result with no labels. Multiply it by a series with a label (<code>grade=\"SP95\"<\/code>), and PromQL finds no match and returns nothing. No error, just an empty panel. Fix: <code>sum()<\/code> on both sides.<\/li>\n<\/ul>\n<h2 class=\"wp-block-heading\">A crash recorder for Android Auto<\/h2>\n<p>Sometimes Android Auto restarts while driving. To know why, the Pi needed a memory. A small shell script on the Pi:<\/p>\n<ul class=\"wp-block-list\">\n<li>writes a small JSON line for each Pi boot (told apart with the kernel <code>boot_id<\/code>), each Android Auto session event, and the Pi temperature<\/li>\n<li>at boot, creates a marker file that only a clean shutdown removes: if the file is still there at the next boot, the last shutdown was not clean. Plus the watchdog boot status from sysfs. (This replaces pstore\/ramoops, which needs a device tree change: not on an A\/B device that lives in a car.)<\/li>\n<li>a busybox web server on the Pi&rsquo;s Wi-Fi serves these lines, and the phone sends them to my server with the GPS points<\/li>\n<\/ul>\n<p>It costs almost nothing (0.05 % of the CPU). And it already showed something: even with the latest aa-proxy-rs, the USB link to the car sometimes fails to start, 4 times in one day. Not solved yet.<\/p>\n<h2 class=\"wp-block-heading\">Next: an OBD2 reader<\/h2>\n<p>All the data above comes from the phone. The car itself says nothing. So I ordered a Vgate iCar Pro 2S, a small Bluetooth OBD2 reader (ELM327 compatible), on eBay. The plan:<\/p>\n<ul class=\"wp-block-list\">\n<li>real fuel consumption, from the engine air flow sensor (MAF): <code>fuel L\/h = MAF \u00d7 3600 \/ (14.7 \u00d7 745)<\/code> for petrol, instead of a guessed 5.8 L\/100 km<\/li>\n<li>real speed and odometer for Android Auto: aa-proxy-rs can send them to the phone, but only if it has a source<\/li>\n<li>live gauges in Android Auto with AATorque, already installed and waiting for a reader<\/li>\n<li>RPM, MAF and coolant temperature next to the GPS points in the trip log<\/li>\n<\/ul>\n<p>The reader will talk to the phone, not to the Pi: the Pi&rsquo;s only Bluetooth chip is busy with Android Auto. And read only: no writing to the car&rsquo;s computer.<\/p>\n<h2 class=\"wp-block-heading\">How AI was used<\/h2>\n<p>The long technical work was done with AI coding agents (Claude Code): the SD card test, the A\/B boot repair, the certificate check, counting the 220 zeros in the logs, reading <code>mitm.rs<\/code>, and writing the phone plugin, the server and the crash recorder. My part: the Pi in the car, the drives, and saying \u00ab\u00a0no, Waze is wrong, I am not parked\u00a0\u00bb.<\/p>\n<p>Note: I don&rsquo;t have much time for the blog these days, so this post was written with the help of an AI, from my own notes, then read and corrected by me. Better to share the content this way than not at all.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>A Pi Zero W with aa-proxy-rs replaces a dead Android Auto dongle. Then Waze says 0 km\/h: the bug was in the proxy config. Plus trip logging and a crash recorder for a car with no internet.<\/p>\n","protected":false},"author":0,"featured_media":577,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-573","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-non-classe"],"_links":{"self":[{"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=\/wp\/v2\/posts\/573","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=573"}],"version-history":[{"count":6,"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=\/wp\/v2\/posts\/573\/revisions"}],"predecessor-version":[{"id":587,"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=\/wp\/v2\/posts\/573\/revisions\/587"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=\/wp\/v2\/media\/577"}],"wp:attachment":[{"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=573"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=573"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=573"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}