<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Siddharth Pandalai</title>
    <description>The latest articles on DEV Community by Siddharth Pandalai (@darkpandawarrior).</description>
    <link>https://dev.to/darkpandawarrior</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4036798%2Fcbc9c397-ab12-4008-91ec-0deac4a666df.jpg</url>
      <title>DEV Community: Siddharth Pandalai</title>
      <link>https://dev.to/darkpandawarrior</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/darkpandawarrior"/>
    <language>en</language>
    <item>
      <title>CancellationException is not an assassin</title>
      <dc:creator>Siddharth Pandalai</dc:creator>
      <pubDate>Sun, 02 Aug 2026 07:30:50 +0000</pubDate>
      <link>https://dev.to/darkpandawarrior/cancellationexception-is-not-an-assassin-2220</link>
      <guid>https://dev.to/darkpandawarrior/cancellationexception-is-not-an-assassin-2220</guid>
      <description>&lt;p&gt;I cancelled a coroutine. It kept running for 8 more seconds. It just did not care.&lt;/p&gt;

&lt;p&gt;This was a search screen. You type, it fires a network request. Type again, it cancels the old request and fires a new one. Standard stuff. Except the old request kept running, and every so often it finished last and painted stale results right over the fresh ones. I had called cancel. I had watched it run in the debugger. The job was "cancelled." And still it went on.&lt;/p&gt;

&lt;p&gt;The culprit was one line I wrote myself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn96fv3l1f4osytd8fhdn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn96fv3l1f4osytd8fhdn.png" alt="The Messenger, drawn as a specimen plate: a hooded figure in an assassin's cloak, holding out a folded note stamped NOTICE. Labelled THE MESSENGER, CancellationException, exhibit 07." width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Cancellation is a message, not a bullet
&lt;/h2&gt;

&lt;p&gt;This is the part people miss about Kotlin coroutines. Cancelling one does not reach in and kill it. It cannot. The coroutine has to cooperate. So the machinery throws a &lt;code&gt;CancellationException&lt;/code&gt; up through your suspend calls, and that exception is the message: we are done, unwind, release your resources, stop.&lt;/p&gt;

&lt;p&gt;Your job is to let that message travel. Most cancellation bugs are really one bug: you stopped the message from traveling.&lt;/p&gt;

&lt;h2&gt;
  
  
  The line that ate the message
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// a suspend call&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"search failed"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks careful. It looks like good defensive code. It is a trap.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CancellationException&lt;/code&gt; extends &lt;code&gt;Exception&lt;/code&gt;. So when cancellation fires mid-request, this &lt;code&gt;catch (e: Exception)&lt;/code&gt; grabs it, logs "search failed", and carries on as if a network error happened. The coroutine never unwinds. The framework thinks you handled something. The work continues.&lt;/p&gt;

&lt;p&gt;You caught the messenger, logged that he looked upset, and locked him in a cell. The message he was carrying never got delivered.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fut7zrmha6p07vnlkcy9x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fut7zrmha6p07vnlkcy9x.png" alt="Diagram: a cancellation signal travelling up the suspend stack from api.search(query), through try, and dying at catch (e: Exception), which is marked " width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Four ways to stop doing this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Catch what you actually expect.&lt;/strong&gt; Ninety percent of the time you do not want &lt;code&gt;Exception&lt;/code&gt;. You want the specific thing that can go wrong.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;IOException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;showOfflineState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;CancellationException&lt;/code&gt; is not an &lt;code&gt;IOException&lt;/code&gt;, so it sails right past and does its job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. If you must catch broadly, rethrow cancellation first.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;CancellationException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;                       &lt;span class="c1"&gt;// let the message through&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"search failed"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Know that &lt;code&gt;runCatching&lt;/code&gt; has the same trap.&lt;/strong&gt; It is a lovely little helper and it catches &lt;code&gt;CancellationException&lt;/code&gt; too.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;runCatching&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// also swallows cancellation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you use it inside a coroutine, check for cancellation and rethrow, or do not use it there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. For cleanup that must run even while cancelling, use &lt;code&gt;NonCancellable&lt;/code&gt;.&lt;/strong&gt; A naked catch is the wrong tool for "always close this".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;withContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;NonCancellable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flushAndClose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;    &lt;span class="c1"&gt;// runs even though the coroutine is cancelling&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8v6wjfbzno4t0ol4lpx7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8v6wjfbzno4t0ol4lpx7.png" alt="Diagram: the same suspend stack, fixed. The cancellation signal now travels all the way up, past catch (e: CancellationException) which rethrows, past catch (e: IOException), and reaches " width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the language made it this way
&lt;/h2&gt;

&lt;p&gt;Structured concurrency is the reason. When a parent scope cancels, every child needs to wind down cleanly and report back up. That only works if cancellation propagates. If any link in the chain swallows the exception, the parent never learns the child stopped, and your careful tree of coroutines turns into orphans that run in the dark.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;CancellationException&lt;/code&gt; is not an error to be defended against. It is the one exception in the system that is doing exactly what you asked. It is the app shutting things down cleanly, on time, on request.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Cancellation is a conversation, not a kill switch. If you swallow the message, the work does not stop. It just stops telling you it is still running.&lt;/p&gt;

&lt;p&gt;Do not shoot the messenger.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;🌀 Iteration 2 of **The Loopdown&lt;/em&gt;&lt;em&gt;. field notes from an engineer who writes.&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Series: **The Coroutine Court&lt;/em&gt;* · Featuring: The Messenger*&lt;br&gt;
&lt;em&gt;📚 The full series: &lt;a href="https://github.com/darkpandawarrior/the-loopdown/blob/main/series/the-coroutine-court.md" rel="noopener noreferrer"&gt;The Coroutine Court&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow the loop → &lt;a href="https://www.linkedin.com/in/siddharth-pandalai" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://dev.to/darkpandawarrior"&gt;dev.to&lt;/a&gt; · &lt;a href="https://github.com/darkpandawarrior/the-loopdown" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kotlin</category>
      <category>coroutines</category>
      <category>cancellation</category>
      <category>structuredconcurrency</category>
    </item>
    <item>
      <title>Teaching a phone to disbelieve its own GPS</title>
      <dc:creator>Siddharth Pandalai</dc:creator>
      <pubDate>Sun, 19 Jul 2026 19:25:38 +0000</pubDate>
      <link>https://dev.to/darkpandawarrior/teaching-a-phone-to-disbelieve-its-own-gps-cip</link>
      <guid>https://dev.to/darkpandawarrior/teaching-a-phone-to-disbelieve-its-own-gps-cip</guid>
      <description>&lt;p&gt;The bug report came in on a Tuesday. One line:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Your app says I hit 400 kmph. I was at a red light."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;He wasn't lying. Neither was the app. The phone genuinely believed it, and that is the interesting part.&lt;/p&gt;

&lt;p&gt;I work on mileage tracking. If you have ever had an app quietly log your drives for expenses or taxes, that is the category. Trip accuracy is not a feature of the product. It is the product. When we started, ours sat around 50 percent, and almost every missing point traced back to the same thing: the phone lying to us with total confidence.&lt;/p&gt;

&lt;p&gt;Here is how we got it to 95, and the one idea underneath all of it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhg0gbc50lvsh9x4z2hix.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhg0gbc50lvsh9x4z2hix.png" alt="The Concussed Witness, drawn as a specimen plate: a satellite with a bandaged, tilted dish and dizzy crossed-out eyes, confidently broadcasting a rejected reading of 400 km/h. Labelled THE CONCUSSED WITNESS, LocationManager, exhibit 01." width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  GPS is a witness with a concussion
&lt;/h2&gt;

&lt;p&gt;We treat GPS like a source of truth. Out in the world, where people actually drive, it behaves more like a witness who took a hard knock to the head. Confident. Cooperative. Frequently wrong.&lt;/p&gt;

&lt;p&gt;Three places break it badly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tunnels.&lt;/strong&gt; The receiver loses its view of the sky. Instead of admitting that, it keeps reporting the last position it had, sometimes for 30 to 40 seconds. Then you come out the other end, it reacquires, and it snaps to your real location in a single jump. Take that jump, divide by the tiny time it took, and you get a parked car doing 400 kmph.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Urban canyons.&lt;/strong&gt; Signals bounce off glass towers before reaching you (multipath, if you want the word). The phone averages the reflections and places you a street or two over, very sure of itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parking garages.&lt;/strong&gt; Nothing, then noise, then nothing again.&lt;/p&gt;

&lt;p&gt;None of these are edge cases. They are Tuesday. So the question stopped being "how do we get better GPS" and became "how do we stop believing bad GPS."&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 1: catch the liars
&lt;/h2&gt;

&lt;p&gt;The cheapest win first. For any travel mode there is a physically possible envelope. A person walking does not teleport 200 meters in a second. A car does not accelerate like a missile. So before a reading touches the trip, check whether it implies something physics would not allow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nc"&gt;GpsFix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isImpossible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;GpsFix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Mode&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;meters&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;haversine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;seconds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;coerceAtLeast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;speed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;meters&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="n"&gt;seconds&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;speed&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;maxPlausibleSpeed&lt;/span&gt;   &lt;span class="c1"&gt;// walking, cycling, driving each differ&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it is impossible, drop it. This one guard killed the most embarrassing errors on its own, including the 400 kmph report. It is not clever. It just refuses to be gaslit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 2: dead reckoning, so a gap is not a hole
&lt;/h2&gt;

&lt;p&gt;Rejecting bad points leaves you with gaps, and a tunnel is a big one. If you freeze until GPS comes back, the trip gets a straight line cutting across three streets.&lt;/p&gt;

&lt;p&gt;So when GPS drops out, stop waiting for it and estimate. Take the last good heading and speed, read the accelerometer, and carry the position forward yourself. Old sailors did this by feel, no stars, no landmarks, just direction and time and a good guess. It drifts, so you cannot lean on it for long. But a tunnel is short, and "roughly right for 40 seconds" beats "confidently wrong" or "a hole in the map."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;deadReckon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;imu&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Imu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Float&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nc"&gt;Position&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;speed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;speed&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;imu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;forwardAccel&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;
    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;distance&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;speed&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;movedBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;heading&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;heading&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;imu&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;turnRate&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trick is knowing when to trust it and when to hand control back to GPS the moment GPS is worth trusting again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 3: fuse, do not just pick
&lt;/h2&gt;

&lt;p&gt;Which brings us to the real fix. Most of the accuracy came from stopping the "believe whoever spoke last" habit and weighing every input by how much it deserves belief right now.&lt;/p&gt;

&lt;p&gt;A clean, consistent GPS fix pulls the estimate hard toward itself. A jittery one barely moves it. The dead-reckoned guess fills the space between. Nobody gets a veto. Everybody gets a vote, weighted by confidence.&lt;/p&gt;

&lt;p&gt;A Kalman filter is the textbook home for this, and if you reach for one, good. But the win is not the filter. The win is the mindset: hold several noisy opinions at once, and lean toward the one that looks trustworthy this second.&lt;/p&gt;

&lt;h2&gt;
  
  
  The unglamorous half
&lt;/h2&gt;

&lt;p&gt;None of this matters if the tracker is dead. On Android, the moment the screen sleeps, the system starts looking for background work to kill, and aggressive OEM skins kill harder. We ran the pipeline in a foreground service with a small floating bubble, partly so the user could see it working, mostly so the OS would leave it alone. A tracker that gets killed is 0 percent accurate no matter how good the math is. I spent nearly as long on staying alive as on the algorithm.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnaa01updei7tu9pcv5b8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnaa01updei7tu9pcv5b8.png" alt="The three fixes as numbered cards: catch the liars, drop a reading that needs rocket acceleration; dead reckoning, estimate from the IMU and last good heading; fuse do not pick, weight each signal by how much you trust it now." width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually took us from 50 to 95
&lt;/h2&gt;

&lt;p&gt;Not one silver bullet. Spike detection removed the garbage. Dead reckoning covered the gaps. Fusion made the whole thing coherent. Staying alive made it real. Each one bought a chunk, and they compounded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Steal this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Treat every external input as a witness who might be concussed, not a source of truth.&lt;/li&gt;
&lt;li&gt;Give physics a veto. The cheapest correctness check is "could this even happen."&lt;/li&gt;
&lt;li&gt;When a signal drops, degrade to a rough estimate instead of freezing or guessing wildly.&lt;/li&gt;
&lt;li&gt;Weight inputs by live confidence, not recency.&lt;/li&gt;
&lt;li&gt;Keep the thing alive first. Perfect logic in a killed process is worth nothing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The one line I kept after all of it: good systems are not the ones with perfect inputs, because nobody gets perfect inputs. Good systems assume their inputs will lie, and plan for the day they do.&lt;/p&gt;

&lt;p&gt;Trust, but verify. Especially your own sensors.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;🌀 Iteration 1 of **The Loopdown&lt;/em&gt;&lt;em&gt;. field notes from an engineer who writes.&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Series: **Sensors Who Lie&lt;/em&gt;* · Featuring: The Concussed Witness*&lt;br&gt;
&lt;em&gt;📚 The full series: &lt;a href="https://github.com/darkpandawarrior/the-loopdown/blob/main/series/sensors-who-lie.md" rel="noopener noreferrer"&gt;Sensors Who Lie&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow the loop → &lt;a href="https://www.linkedin.com/in/siddharth-pandalai" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://dev.to/darkpandawarrior"&gt;dev.to&lt;/a&gt; · &lt;a href="https://github.com/darkpandawarrior/the-loopdown" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>location</category>
      <category>sensorfusion</category>
      <category>deadreckoning</category>
    </item>
  </channel>
</rss>
