<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Iris Classon - In Love with Code</title><link>https://www.irisclasson.com/</link><description>Recent content on Iris Classon - In Love with Code</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Tue, 07 Apr 2026 12:00:00 +0200</lastBuildDate><atom:link href="https://www.irisclasson.com/index.xml" rel="self" type="application/rss+xml"/><item><title>Error CompileAppManifest Task Failed Unexpectedly</title><link>https://www.irisclasson.com/2026/04/07/error-compileappmanifest-task-failed-unexpectedly/</link><pubDate>Tue, 07 Apr 2026 12:00:00 +0200</pubDate><guid>https://www.irisclasson.com/2026/04/07/error-compileappmanifest-task-failed-unexpectedly/</guid><description>Iris Classon - In Love with Code https://www.irisclasson.com/2026/04/07/error-compileappmanifest-task-failed-unexpectedly/ -&lt;p>Hope you&amp;rsquo;ve had a lovely Easter! I caught the flu the week before, and am still recovering. Flu plus Easter = I was away for over a week. Which means by the time I came back and did a pull there had been quite a few changes in the work code base. After staring at the screen for two hours running various updates I finally got around to attempting a build, but got the following error:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>Xamarin.Shared.targets&lt;span style="color:#ff79c6">(&lt;/span>614,3&lt;span style="color:#ff79c6">)&lt;/span>: Error
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>MSB4018 : The &lt;span style="color:#f1fa8c">&amp;#34;CompileAppManifest&amp;#34;&lt;/span> task failed unexpectedly.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>System.NullReferenceException: Object reference not &lt;span style="color:#8be9fd;font-style:italic">set&lt;/span> to an instance of an object.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Xamarin.MacDev.Tasks.CompileAppManifest.SetXcodeValues&lt;span style="color:#ff79c6">(&lt;/span>PDictionary plist, IAppleSdk currentSDK&lt;span style="color:#ff79c6">)&lt;/span> in /Users/builder/azdo/_work/1/s/macios/msbuild/Xamarin.MacDev.Tasks/Tasks/CompileAppManifest.cs:line &lt;span style="color:#bd93f9">534&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Xamarin.MacDev.Tasks.CompileAppManifest.Compile&lt;span style="color:#ff79c6">(&lt;/span>PDictionary plist&lt;span style="color:#ff79c6">)&lt;/span> in /Users/builder/azdo/_work/1/s/macios/msbuild/Xamarin.MacDev.Tasks/Tasks/CompileAppManifest.cs:line &lt;span style="color:#bd93f9">338&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Xamarin.MacDev.Tasks.CompileAppManifest.Execute&lt;span style="color:#ff79c6">()&lt;/span> in /Users/builder/azdo/_work/1/s/macios/msbuild/Xamarin.MacDev.Tasks/Tasks/CompileAppManifest.cs:line &lt;span style="color:#bd93f9">166&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.Build.BackEnd.TaskExecutionHost.Execute&lt;span style="color:#ff79c6">()&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.Build.BackEnd.TaskBuilder.ExecuteInstantiatedTask&lt;span style="color:#ff79c6">(&lt;/span>TaskExecutionHost taskExecutionHost, TaskLoggingContext taskLoggingContext, TaskHost taskHost, ItemBucket bucket, TaskExecutionMode howToExecuteTask&lt;span style="color:#ff79c6">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;p>If you come across this, take a closer look at the error.&lt;/p>
&lt;p>The null reference exception is in the &lt;code>SetXcodeValues&lt;/code> method, which usually means you have a SDK mismatch. For me it was simply a missing workload update since I was updating my environment:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet workload update&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;p>If you get this error, make sure you have the right Xcode version for the MAUI version you are using.&lt;/p>
&lt;p>Hope this helps!&lt;/p>
&lt;h4 class="comment-header">Comments&lt;/h4>
&lt;h7>&lt;a href="mailto:IrisLovesCode@gmail.com?subject=Error%20CompileAppManifest%20Task%20Failed%20Unexpectedly">Leave a comment below, or by email.&lt;/a>&lt;/h7>
- https://www.irisclasson.com/2026/04/07/error-compileappmanifest-task-failed-unexpectedly/ -</description></item><item><title>Fourth Edition: A History of .NET Web Development</title><link>https://www.irisclasson.com/2026/03/31/fourth-edition-a-history-of-.net-web-development/</link><pubDate>Tue, 31 Mar 2026 00:00:00 +0000</pubDate><guid>https://www.irisclasson.com/2026/03/31/fourth-edition-a-history-of-.net-web-development/</guid><description>Iris Classon - In Love with Code https://www.irisclasson.com/2026/03/31/fourth-edition-a-history-of-.net-web-development/ -&lt;p>The fourth edition of my .NET web development history book is out today.&lt;/p>
&lt;picture>
&lt;source srcset="./2026/03/historyofnetwebdevelopment.JPG.webp" type="image/webp">
&lt;source srcset="./2026/03/historyofnetwebdevelopment.JPG" type="image/jpeg">
&lt;img src="./2026/03/historyofnetwebdevelopment.JPG" alt="Fourth Edition: A History of .NET Web Development"
title="Fourth Edition: A History of .NET Web Development" >
&lt;/picture>
&lt;p>&lt;a href="https://leanpub.com/historyofnetwebdevelopment-4thed">https://leanpub.com/historyofnetwebdevelopment-4thed&lt;/a>&lt;/p>
&lt;p>Since its inception in the early 2000s, the .NET web development platform has evolved significantly. Each release has introduced new capabilities, reshaped best practices, and responded to changing demands in how we build applications.&lt;/p>
&lt;p>Understanding that evolution helps make sense of where the platform is today and where it is heading.&lt;/p>
&lt;p>In this fourth edition, we follow the journey of .NET web development from its early foundations through to the modern ecosystem. We cover the shifts toward cloud-native architecture, improvements in performance and developer experience, and the growing role of AI-related abstractions in .NET 9, along with the continued refinements and simplifications introduced in .NET 10.&lt;/p>
&lt;p>If you&amp;rsquo;ve read a previous edition, this one picks up where it left off and brings the story up to date. If this is your first time, it starts from the beginning and covers the full arc.&lt;/p>
&lt;h4 class="comment-header">Comments&lt;/h4>
&lt;h7>&lt;a href="mailto:IrisLovesCode@gmail.com?subject=Fourth%20Edition%3a%20A%20History%20of%20.NET%20Web%20Development">Leave a comment below, or by email.&lt;/a>&lt;/h7>
- https://www.irisclasson.com/2026/03/31/fourth-edition-a-history-of-.net-web-development/ -</description></item><item><title>GitHub Copilot Model Training</title><link>https://www.irisclasson.com/2026/03/29/github-copilot-model-training/</link><pubDate>Sun, 29 Mar 2026 00:00:00 +0000</pubDate><guid>https://www.irisclasson.com/2026/03/29/github-copilot-model-training/</guid><description>Iris Classon - In Love with Code https://www.irisclasson.com/2026/03/29/github-copilot-model-training/ -&lt;p>A little reminder for those of you who haven&amp;rsquo;t logged in to GitHub for a while! On April 24th, GitHub Copilot will be using interaction data to train their model.&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Important update&lt;/strong>
On April 24 we&amp;rsquo;ll start using GitHub Copilot interaction data for AI model training unless you opt out. Review this update and manage your preferences in your GitHub account settings.&lt;/p>
&lt;/blockquote>
&lt;p>You can change the setting here: &lt;a href="https://github.com/settings/copilot/features">https://github.com/settings/copilot/features&lt;/a>&lt;/p>
&lt;p>Scroll down to privacy and choose to disable to opt out, if that&amp;rsquo;s your preference.&lt;/p>
&lt;p>More data, and data from real usage, equals smarter models and better replies as they explain in the announcement blog post: &lt;a href="https://github.blog/news-insights/company-news/updates-to-github-copilot-interaction-data-usage-policy/">https://github.blog/news-insights/company-news/updates-to-github-copilot-interaction-data-usage-policy/&lt;/a>&lt;/p>
&lt;p>But it&amp;rsquo;s equally important to let people opt out and make that option easy to find. Worth noting that interaction data from Copilot Business, Copilot Enterprise, or enterprise-owned repositories are not used.&lt;/p>
&lt;h4 class="comment-header">Comments&lt;/h4>
&lt;h7>&lt;a href="mailto:IrisLovesCode@gmail.com?subject=GitHub%20Copilot%20Model%20Training">Leave a comment below, or by email.&lt;/a>&lt;/h7>
- https://www.irisclasson.com/2026/03/29/github-copilot-model-training/ -</description></item><item><title>Expired Azure Credentials in GitHub Actions</title><link>https://www.irisclasson.com/2026/03/26/expired-azure-credentials-in-github-actions/</link><pubDate>Thu, 26 Mar 2026 00:00:00 +0000</pubDate><guid>https://www.irisclasson.com/2026/03/26/expired-azure-credentials-in-github-actions/</guid><description>Iris Classon - In Love with Code https://www.irisclasson.com/2026/03/26/expired-azure-credentials-in-github-actions/ -&lt;p>If your GitHub Actions workflow suddenly starts failing with an error like:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-text" data-lang="text">&lt;span style="display:flex;">&lt;span>AADSTS7000222: The provided client secret keys are expired&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;p>it usually means your Azure service principal secret has expired and needs to be rotated.&lt;/p>
&lt;p>Here is a quick guide to updating your Azure credentials and getting your pipeline working again.&lt;/p>
&lt;p>&lt;strong>The Azure credentials format&lt;/strong>&lt;/p>
&lt;p>In many GitHub workflows, Azure authentication is stored as a JSON secret (often called &lt;code>AZURE_CREDENTIALS&lt;/code>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">&amp;#34;clientId&amp;#34;&lt;/span>: &lt;span style="color:#f1fa8c">&amp;#34;xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">&amp;#34;clientSecret&amp;#34;&lt;/span>: &lt;span style="color:#f1fa8c">&amp;#34;your-secret-value&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">&amp;#34;subscriptionId&amp;#34;&lt;/span>: &lt;span style="color:#f1fa8c">&amp;#34;xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">&amp;#34;tenantId&amp;#34;&lt;/span>: &lt;span style="color:#f1fa8c">&amp;#34;xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;p>Each field maps directly to values in Azure. Where do you find these?&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Client ID&lt;/strong> is the same as Application ID&lt;/li>
&lt;li>&lt;strong>Client secret&lt;/strong> is the value of the secret you create&lt;/li>
&lt;li>&lt;strong>Subscription ID&lt;/strong>: Azure portal → Subscriptions → select your subscription&lt;/li>
&lt;li>&lt;strong>Tenant ID&lt;/strong> is the same as Directory ID&lt;/li>
&lt;/ul>
&lt;h4 class="comment-header">Comments&lt;/h4>
&lt;h7>&lt;a href="mailto:IrisLovesCode@gmail.com?subject=Expired%20Azure%20Credentials%20in%20GitHub%20Actions">Leave a comment below, or by email.&lt;/a>&lt;/h7>
- https://www.irisclasson.com/2026/03/26/expired-azure-credentials-in-github-actions/ -</description></item><item><title>Bluetooth Log Distance Path Loss Calculation</title><link>https://www.irisclasson.com/2026/03/03/bluetooth-log-distance-path-loss-calculation/</link><pubDate>Tue, 03 Mar 2026 00:00:00 +0000</pubDate><guid>https://www.irisclasson.com/2026/03/03/bluetooth-log-distance-path-loss-calculation/</guid><description>Iris Classon - In Love with Code https://www.irisclasson.com/2026/03/03/bluetooth-log-distance-path-loss-calculation/ -&lt;p>I realize I didn&amp;rsquo;t show the calculation in the previous blog post. That was partly intentional as it&amp;rsquo;s not a good idea. Even more so indoors, where you have more surfaces and obstacles.&lt;/p>
&lt;p>But, here it is, the C# implementation:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8be9fd;font-style:italic">public&lt;/span> &lt;span style="color:#8be9fd">double?&lt;/span> EstimateDistanceMeters(&lt;span style="color:#8be9fd">double&lt;/span> txPowerAt1Meter = -&lt;span style="color:#bd93f9">59&lt;/span>, &lt;span style="color:#8be9fd">double&lt;/span> pathLossExponent = &lt;span style="color:#bd93f9">2.4&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#6272a4">// _ema is the smoothed RSSI discussed in the previous blog post&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">if&lt;/span> (!_emaRssi.HasValue)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">return&lt;/span> &lt;span style="color:#ff79c6">null&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">return&lt;/span> Math.Pow(&lt;span style="color:#bd93f9">10.0&lt;/span>, (txPowerAt1Meter - _emaRssi.Value) / (&lt;span style="color:#bd93f9">10.0&lt;/span> * pathLossExponent));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;p>The path loss exponent describes how fast signal drops — approximately 2 in open space, and 2.2–2.4 indoors.&lt;/p>
&lt;p>The value &lt;code>-59&lt;/code> for &lt;code>txPowerAt1Meter&lt;/code> is a convention, not a law of physics. It comes from BLE beacon practice, especially Apple&amp;rsquo;s iBeacon spec, where manufacturers often define RSSI at 1 meter as approximately -59 dBm. So developers started using -59 as a default when they don&amp;rsquo;t have a calibrated value, and many devices ship with that as their default.&lt;/p>
&lt;p>Calibration, which I talked about in the previous post, would be a better way to get a more accurate value for the &lt;code>txPowerAt1Meter&lt;/code> parameter.&lt;/p>
&lt;h4 class="comment-header">Comments&lt;/h4>
&lt;h7>&lt;a href="mailto:IrisLovesCode@gmail.com?subject=Bluetooth%20Log%20Distance%20Path%20Loss%20Calculation">Leave a comment below, or by email.&lt;/a>&lt;/h7>
- https://www.irisclasson.com/2026/03/03/bluetooth-log-distance-path-loss-calculation/ -</description></item><item><title>File-Based Apps in .NET 10</title><link>https://www.irisclasson.com/2026/03/01/file-based-apps-in-.net-10/</link><pubDate>Sun, 01 Mar 2026 00:00:00 +0000</pubDate><guid>https://www.irisclasson.com/2026/03/01/file-based-apps-in-.net-10/</guid><description>Iris Classon - In Love with Code https://www.irisclasson.com/2026/03/01/file-based-apps-in-.net-10/ -&lt;p>One of my favourite improvements in recent .NET versions, and something that feels properly usable in .NET 10, is file-based apps. You can create a single &lt;code>.cs&lt;/code> file and run it directly. No project file, no solution, no folder structure, just code.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet run hello.cs&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;p>That&amp;rsquo;s it.&lt;/p>
&lt;h2 id="what-it-looks-like">What it looks like&lt;/h2>
&lt;p>A minimal file-based app is just a &lt;code>.cs&lt;/code> file with top-level statements:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8be9fd">var&lt;/span> name = args.FirstOrDefault() ?? &lt;span style="color:#f1fa8c">&amp;#34;world&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.WriteLine(&lt;span style="color:#f1fa8c">$&amp;#34;Hello, {name}!&amp;#34;&lt;/span>);&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;p>Save it as &lt;code>hello.cs&lt;/code>, run &lt;code>dotnet run hello.cs&lt;/code>, and you are done.&lt;/p>
&lt;p>If you need a NuGet package, you can reference it at the top of the file. For example, using &lt;code>System.CommandLine&lt;/code> to handle arguments:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>#:package System.CommandLine@&lt;span style="color:#bd93f9">2.0&lt;/span>.&lt;span style="color:#bd93f9">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff79c6">using&lt;/span> System.CommandLine;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8be9fd">var&lt;/span> nameOption = &lt;span style="color:#ff79c6">new&lt;/span> Option&amp;lt;&lt;span style="color:#8be9fd">string&lt;/span>&amp;gt;(&lt;span style="color:#f1fa8c">&amp;#34;--name&amp;#34;&lt;/span>, () =&amp;gt; &lt;span style="color:#f1fa8c">&amp;#34;world&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8be9fd">var&lt;/span> root = &lt;span style="color:#ff79c6">new&lt;/span> RootCommand
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> nameOption
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>root.SetHandler((name) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#f1fa8c">$&amp;#34;Hello, {name}!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}, nameOption);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff79c6">await&lt;/span> root.InvokeAsync(args);&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;p>There is no &lt;code>.csproj&lt;/code> to manage and no explicit restore step. The tooling takes care of it in the background.&lt;/p>
&lt;h2 id="why-it-matters">Why it matters&lt;/h2>
&lt;p>C# has always been a strong choice for small tools and automation, but the setup got in the way. Before writing any actual logic, you had already created a project, picked a template, and committed to a structure.&lt;/p>
&lt;p>Languages like Python and JavaScript never had that overhead. A script is just a file. You write it and run it.&lt;/p>
&lt;p>File-based apps move C# much closer to that experience. It becomes practical to reach for C# for quick scripts, small utilities, or experiments without switching languages or setting up a project. That lowers the barrier both for experienced developers who want speed and for beginners who just want to try something without understanding the full project system first.&lt;/p>
&lt;h2 id="key-benefits-include">Key benefits include&lt;/h2>
&lt;p>&lt;strong>Reduced boilerplate for simple applications&lt;/strong>
You can go from idea to running code without scaffolding a project. For small utilities, that removes most of the friction that used to come before the actual work.&lt;/p>
&lt;p>&lt;strong>Self-contained source files with embedded configuration&lt;/strong>
Dependencies and basic configuration can live alongside your code. That makes the file portable and easier to share, since everything you need is defined in one place.&lt;/p>
&lt;p>&lt;strong>Works well with modern publishing options&lt;/strong>
File-based apps fit naturally with things like single-file publishing and Native AOT. If you decide a script should become something you distribute, you are already close to a publishable shape.&lt;/p>
&lt;p>&lt;strong>Easy path to CLI tools&lt;/strong>
A small script can evolve into a proper command-line tool without a big rewrite. You can start simple and grow into a more structured project when it makes sense, instead of committing upfront.&lt;/p>
&lt;h2 id="when-to-use-it">When to use it&lt;/h2>
&lt;p>This is not a replacement for regular projects. As soon as you need multiple files, tests, or anything resembling real application structure, you still want a project.&lt;/p>
&lt;p>But for automation scripts, quick data processing, trying out an API, or teaching someone the basics of C#, a single file is often exactly the right tool.&lt;/p>
&lt;h4 class="comment-header">Comments&lt;/h4>
&lt;h7>&lt;a href="mailto:IrisLovesCode@gmail.com?subject=File-Based%20Apps%20in%20.NET%2010">Leave a comment below, or by email.&lt;/a>&lt;/h7>
- https://www.irisclasson.com/2026/03/01/file-based-apps-in-.net-10/ -</description></item><item><title>BLE Scanning on Android in .NET MAUI</title><link>https://www.irisclasson.com/2026/02/28/ble-scanning-on-android-in-.net-maui/</link><pubDate>Sat, 28 Feb 2026 00:00:00 +0000</pubDate><guid>https://www.irisclasson.com/2026/02/28/ble-scanning-on-android-in-.net-maui/</guid><description>Iris Classon - In Love with Code https://www.irisclasson.com/2026/02/28/ble-scanning-on-android-in-.net-maui/ -&lt;p>In my previous post I mentioned scan modes briefly, mostly in the context of why RSSI behaves the way it does on Android. A few people asked for a more complete example, so here is a MAUI-ready Android service you can drop in.&lt;/p>
&lt;p>&lt;code>BluetoothLeScanner&lt;/code> gets the scan settings, and &lt;code>ScanSettings.Builder().SetScanMode(ScanMode.LowLatency)&lt;/code> is the bit that enables low-latency scanning. In .NET for Android, those settings are passed into &lt;code>StartScan(...)&lt;/code>. Android&amp;rsquo;s BLE docs also recommend stopping scans promptly because continuous scanning is expensive on battery.&lt;/p>
&lt;h2 id="1-shared-interface">1. Shared interface&lt;/h2>
&lt;p>Create this somewhere in shared code, for example &lt;code>Services/IBleScanner.cs&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8be9fd;font-style:italic">public&lt;/span> &lt;span style="color:#ff79c6">interface&lt;/span> &lt;span style="color:#50fa7b">IBleScanner&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">event&lt;/span> EventHandler&amp;lt;BleDeviceDiscoveredEventArgs&amp;gt;? DeviceDiscovered;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd">bool&lt;/span> IsScanning { &lt;span style="color:#ff79c6">get&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Task StartScanningAsync(CancellationToken cancellationToken = &lt;span style="color:#ff79c6">default&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Task StopScanningAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8be9fd;font-style:italic">public&lt;/span> &lt;span style="color:#8be9fd;font-style:italic">sealed&lt;/span> &lt;span style="color:#ff79c6">class&lt;/span> &lt;span style="color:#50fa7b">BleDeviceDiscoveredEventArgs&lt;/span> : EventArgs
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd;font-style:italic">public&lt;/span> BleDeviceDiscoveredEventArgs(&lt;span style="color:#8be9fd">string&lt;/span> id, &lt;span style="color:#8be9fd">string?&lt;/span> name, &lt;span style="color:#8be9fd">int&lt;/span> rssi)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Id = id;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = name;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Rssi = rssi;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd;font-style:italic">public&lt;/span> &lt;span style="color:#8be9fd">string&lt;/span> Id { &lt;span style="color:#ff79c6">get&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd;font-style:italic">public&lt;/span> &lt;span style="color:#8be9fd">string?&lt;/span> Name { &lt;span style="color:#ff79c6">get&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd;font-style:italic">public&lt;/span> &lt;span style="color:#8be9fd">int&lt;/span> Rssi { &lt;span style="color:#ff79c6">get&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;h2 id="2-android-implementation">2. Android implementation&lt;/h2>
&lt;p>Put this in &lt;code>Platforms/Android/BleScanner.android.cs&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff79c6">using&lt;/span> Android.Bluetooth;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff79c6">using&lt;/span> Android.Bluetooth.LE;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff79c6">using&lt;/span> Android.Content;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff79c6">using&lt;/span> Microsoft.Maui.ApplicationModel;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff79c6">using&lt;/span> Application = Android.App.Application;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8be9fd;font-style:italic">public&lt;/span> &lt;span style="color:#8be9fd;font-style:italic">sealed&lt;/span> &lt;span style="color:#ff79c6">class&lt;/span> &lt;span style="color:#50fa7b">BleScanner&lt;/span> : Java.Lang.Object, IBleScanner
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd;font-style:italic">private&lt;/span> &lt;span style="color:#ff79c6">readonly&lt;/span> BluetoothManager? _bluetoothManager;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd;font-style:italic">private&lt;/span> BluetoothAdapter? _adapter;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd;font-style:italic">private&lt;/span> BluetoothLeScanner? _scanner;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd;font-style:italic">private&lt;/span> ScanCallbackImpl? _callback;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd;font-style:italic">public&lt;/span> &lt;span style="color:#ff79c6">event&lt;/span> EventHandler&amp;lt;BleDeviceDiscoveredEventArgs&amp;gt;? DeviceDiscovered;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd;font-style:italic">public&lt;/span> &lt;span style="color:#8be9fd">bool&lt;/span> IsScanning { &lt;span style="color:#ff79c6">get&lt;/span>; &lt;span style="color:#8be9fd;font-style:italic">private&lt;/span> &lt;span style="color:#ff79c6">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd;font-style:italic">public&lt;/span> BleScanner()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _bluetoothManager =
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Application.Context.GetSystemService(Context.BluetoothService) &lt;span style="color:#ff79c6">as&lt;/span> BluetoothManager;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _adapter = _bluetoothManager?.Adapter;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _scanner = _adapter?.BluetoothLeScanner;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd;font-style:italic">public&lt;/span> Task StartScanningAsync(CancellationToken cancellationToken = &lt;span style="color:#ff79c6">default&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> EnsureBluetoothReady();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">if&lt;/span> (IsScanning)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">return&lt;/span> Task.CompletedTask;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _callback = &lt;span style="color:#ff79c6">new&lt;/span> ScanCallbackImpl(args =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MainThread.BeginInvokeOnMainThread(() =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> DeviceDiscovered?.Invoke(&lt;span style="color:#ff79c6">this&lt;/span>, args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd">var&lt;/span> settings = &lt;span style="color:#ff79c6">new&lt;/span> ScanSettings.Builder()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .SetScanMode(Android.Bluetooth.LE.ScanMode.LowLatency)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd">var&lt;/span> filters = &lt;span style="color:#ff79c6">new&lt;/span> List&amp;lt;ScanFilter&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _scanner!.StartScan(filters, settings, _callback);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IsScanning = &lt;span style="color:#ff79c6">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">if&lt;/span> (cancellationToken.CanBeCanceled)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cancellationToken.Register(() =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _ = StopScanningAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">return&lt;/span> Task.CompletedTask;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd;font-style:italic">public&lt;/span> Task StopScanningAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">if&lt;/span> (!IsScanning || _scanner == &lt;span style="color:#ff79c6">null&lt;/span> || _callback == &lt;span style="color:#ff79c6">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">return&lt;/span> Task.CompletedTask;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _scanner.StopScan(_callback);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IsScanning = &lt;span style="color:#ff79c6">false&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _callback = &lt;span style="color:#ff79c6">null&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">return&lt;/span> Task.CompletedTask;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd;font-style:italic">private&lt;/span> &lt;span style="color:#ff79c6">void&lt;/span> EnsureBluetoothReady()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _adapter ??= _bluetoothManager?.Adapter;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _scanner ??= _adapter?.BluetoothLeScanner;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">if&lt;/span> (_bluetoothManager == &lt;span style="color:#ff79c6">null&lt;/span> || _adapter == &lt;span style="color:#ff79c6">null&lt;/span> || _scanner == &lt;span style="color:#ff79c6">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">throw&lt;/span> &lt;span style="color:#ff79c6">new&lt;/span> InvalidOperationException(&lt;span style="color:#f1fa8c">&amp;#34;Bluetooth LE scanning is not available on this device.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">if&lt;/span> (!_adapter.IsEnabled)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">throw&lt;/span> &lt;span style="color:#ff79c6">new&lt;/span> InvalidOperationException(&lt;span style="color:#f1fa8c">&amp;#34;Bluetooth is turned off.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd;font-style:italic">private&lt;/span> &lt;span style="color:#8be9fd;font-style:italic">sealed&lt;/span> &lt;span style="color:#ff79c6">class&lt;/span> &lt;span style="color:#50fa7b">ScanCallbackImpl&lt;/span> : ScanCallback
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd;font-style:italic">private&lt;/span> &lt;span style="color:#ff79c6">readonly&lt;/span> Action&amp;lt;BleDeviceDiscoveredEventArgs&amp;gt; _onDeviceDiscovered;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd;font-style:italic">public&lt;/span> ScanCallbackImpl(Action&amp;lt;BleDeviceDiscoveredEventArgs&amp;gt; onDeviceDiscovered)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _onDeviceDiscovered = onDeviceDiscovered;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd;font-style:italic">public&lt;/span> &lt;span style="color:#8be9fd;font-style:italic">override&lt;/span> &lt;span style="color:#ff79c6">void&lt;/span> OnScanResult(ScanCallbackType callbackType, ScanResult? result)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">if&lt;/span> (result?.Device == &lt;span style="color:#ff79c6">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">return&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd">var&lt;/span> args = &lt;span style="color:#ff79c6">new&lt;/span> BleDeviceDiscoveredEventArgs(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> id: result.Device.Address ?? &lt;span style="color:#8be9fd">string&lt;/span>.Empty,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> name: result.Device.Name,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> rssi: result.Rssi);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _onDeviceDiscovered(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd;font-style:italic">public&lt;/span> &lt;span style="color:#8be9fd;font-style:italic">override&lt;/span> &lt;span style="color:#ff79c6">void&lt;/span> OnBatchScanResults(IList&amp;lt;ScanResult&amp;gt;? results)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">if&lt;/span> (results == &lt;span style="color:#ff79c6">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">return&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">foreach&lt;/span> (&lt;span style="color:#8be9fd">var&lt;/span> result &lt;span style="color:#ff79c6">in&lt;/span> results)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">if&lt;/span> (result?.Device == &lt;span style="color:#ff79c6">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">continue&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd">var&lt;/span> args = &lt;span style="color:#ff79c6">new&lt;/span> BleDeviceDiscoveredEventArgs(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> id: result.Device.Address ?? &lt;span style="color:#8be9fd">string&lt;/span>.Empty,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> name: result.Device.Name,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> rssi: result.Rssi);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _onDeviceDiscovered(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd;font-style:italic">public&lt;/span> &lt;span style="color:#8be9fd;font-style:italic">override&lt;/span> &lt;span style="color:#ff79c6">void&lt;/span> OnScanFailed(ScanFailure errorCode)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> System.Diagnostics.Debug.WriteLine(&lt;span style="color:#f1fa8c">$&amp;#34;BLE scan failed: {errorCode}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;h2 id="3-register-it-in-mauiprogramcs">3. Register it in MauiProgram.cs&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.Services.AddSingleton&amp;lt;IBleScanner, BleScanner&amp;gt;();&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;h2 id="4-use-it-from-a-page-or-view-model">4. Use it from a page or view model&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8be9fd;font-style:italic">public&lt;/span> &lt;span style="color:#8be9fd;font-style:italic">partial&lt;/span> &lt;span style="color:#ff79c6">class&lt;/span> &lt;span style="color:#50fa7b">MainPage&lt;/span> : ContentPage
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd;font-style:italic">private&lt;/span> &lt;span style="color:#ff79c6">readonly&lt;/span> IBleScanner _bleScanner;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd;font-style:italic">public&lt;/span> MainPage(IBleScanner bleScanner)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> InitializeComponent();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _bleScanner = bleScanner;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _bleScanner.DeviceDiscovered += OnDeviceDiscovered;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd;font-style:italic">private&lt;/span> &lt;span style="color:#8be9fd;font-style:italic">async&lt;/span> &lt;span style="color:#ff79c6">void&lt;/span> OnStartScanClicked(&lt;span style="color:#8be9fd">object&lt;/span> sender, EventArgs e)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">await&lt;/span> _bleScanner.StartScanningAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd;font-style:italic">private&lt;/span> &lt;span style="color:#8be9fd;font-style:italic">async&lt;/span> &lt;span style="color:#ff79c6">void&lt;/span> OnStopScanClicked(&lt;span style="color:#8be9fd">object&lt;/span> sender, EventArgs e)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">await&lt;/span> _bleScanner.StopScanningAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd;font-style:italic">private&lt;/span> &lt;span style="color:#ff79c6">void&lt;/span> OnDeviceDiscovered(&lt;span style="color:#8be9fd">object?&lt;/span> sender, BleDeviceDiscoveredEventArgs e)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> System.Diagnostics.Debug.WriteLine(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f1fa8c">$&amp;#34;Found {e.Name ?? &amp;#34;&lt;/span>(unknown)&lt;span style="color:#f1fa8c">&amp;#34;} [{e.Id}] RSSI={e.Rssi}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;h2 id="5-android-permissions">5. Android permissions&lt;/h2>
&lt;p>Modern Android BLE scanning requires Bluetooth scan permissions, and older Android versions may also require location permission for scan results to work correctly.&lt;/p>
&lt;p>In &lt;code>Platforms/Android/AndroidManifest.xml&lt;/code> you will usually need:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-xml" data-lang="xml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff79c6">&amp;lt;uses-permission&lt;/span> &lt;span style="color:#50fa7b">android:name=&lt;/span>&lt;span style="color:#f1fa8c">&amp;#34;android.permission.BLUETOOTH&amp;#34;&lt;/span> &lt;span style="color:#ff79c6">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff79c6">&amp;lt;uses-permission&lt;/span> &lt;span style="color:#50fa7b">android:name=&lt;/span>&lt;span style="color:#f1fa8c">&amp;#34;android.permission.BLUETOOTH_ADMIN&amp;#34;&lt;/span> &lt;span style="color:#ff79c6">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff79c6">&amp;lt;uses-permission&lt;/span> &lt;span style="color:#50fa7b">android:name=&lt;/span>&lt;span style="color:#f1fa8c">&amp;#34;android.permission.BLUETOOTH_SCAN&amp;#34;&lt;/span> &lt;span style="color:#ff79c6">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff79c6">&amp;lt;uses-permission&lt;/span> &lt;span style="color:#50fa7b">android:name=&lt;/span>&lt;span style="color:#f1fa8c">&amp;#34;android.permission.BLUETOOTH_CONNECT&amp;#34;&lt;/span> &lt;span style="color:#ff79c6">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff79c6">&amp;lt;uses-permission&lt;/span> &lt;span style="color:#50fa7b">android:name=&lt;/span>&lt;span style="color:#f1fa8c">&amp;#34;android.permission.ACCESS_FINE_LOCATION&amp;#34;&lt;/span> &lt;span style="color:#ff79c6">/&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;p>And on Android 12+, you also need to request the runtime permissions before scanning.&lt;/p>
&lt;h2 id="side-note">Side note&lt;/h2>
&lt;p>&lt;code>LowLatency&lt;/code> is the most aggressive scan mode Android exposes, but it still does not guarantee perfectly continuous callbacks because behavior can vary by Android version, device, and power policy. &lt;code>BluetoothLeScanner&lt;/code> is the correct Android API to use for scan operations, and &lt;code>ScanSettings&lt;/code> is where you configure the scan mode.&lt;/p>
&lt;h4 class="comment-header">Comments&lt;/h4>
&lt;h7>&lt;a href="mailto:IrisLovesCode@gmail.com?subject=BLE%20Scanning%20on%20Android%20in%20.NET%20MAUI">Leave a comment below, or by email.&lt;/a>&lt;/h7>
- https://www.irisclasson.com/2026/02/28/ble-scanning-on-android-in-.net-maui/ -</description></item><item><title>Why BLE RSSI Spikes on Android</title><link>https://www.irisclasson.com/2026/02/27/why-ble-rssi-spikes-on-android/</link><pubDate>Fri, 27 Feb 2026 00:00:00 +0000</pubDate><guid>https://www.irisclasson.com/2026/02/27/why-ble-rssi-spikes-on-android/</guid><description>Iris Classon - In Love with Code https://www.irisclasson.com/2026/02/27/why-ble-rssi-spikes-on-android/ -&lt;p>Working on the BLE emulator, I kept noticing that RSSI readings on Android were jumpier than on iOS. Not just a little — noticeably more erratic, even when nothing was moving and the environment hadn&amp;rsquo;t changed.&lt;/p>
&lt;p>It took me a while to understand why. The answer is scan modes and duty cycling.&lt;/p>
&lt;p>&lt;strong>Scan modes&lt;/strong>&lt;/p>
&lt;p>When you start a BLE scan on Android you can choose how aggressively the radio listens:&lt;/p>
&lt;ul>
&lt;li>&lt;code>SCAN_MODE_LOW_POWER&lt;/code> — short bursts with long gaps. This is the default.&lt;/li>
&lt;li>&lt;code>SCAN_MODE_BALANCED&lt;/code> — moderate frequency&lt;/li>
&lt;li>&lt;code>SCAN_MODE_LOW_LATENCY&lt;/code> — as close to continuous as you can get&lt;/li>
&lt;/ul>
&lt;p>The problem with anything other than &lt;code>LOW_LATENCY&lt;/code> is that the scanner goes quiet between windows. BLE devices advertise at their own intervals — often 100 to 1000 ms — and Android simply misses packets while the scan window is closed. When scanning resumes, you get whatever advertisement happens to arrive next. Because you missed the intermediate packets, that value can differ significantly from the last one you saw, and that shows up as a spike.&lt;/p>
&lt;p>You set it like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8be9fd">var&lt;/span> settings = &lt;span style="color:#ff79c6">new&lt;/span> ScanSettings.Builder()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .SetScanMode(ScanMode.LowLatency)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Build();&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;p>&lt;strong>Duty cycling&lt;/strong>&lt;/p>
&lt;p>Even with &lt;code>LOW_LATENCY&lt;/code>, Android may still apply internal duty cycling depending on device and OS version. You don&amp;rsquo;t control this, and missed packets are simply lost — they are not queued.&lt;/p>
&lt;p>What you do see is irregular timing between callbacks. Samples arrive unevenly, and because RSSI varies naturally due to multipath, antenna orientation, and nearby obstacles, uneven sampling makes the signal look more erratic than it actually is.&lt;/p>
&lt;p>RSSI is inherently noisy even under ideal conditions — scan behavior amplifies that noise rather than causing it. iOS is smoother here. CoreBluetooth handles scan scheduling internally and tends to produce more consistent readings. Android gives you more control, but also more noise.&lt;/p>
&lt;p>&lt;strong>Why it matters&lt;/strong>&lt;/p>
&lt;p>If you read my previous post on RSSI proximity estimation, you might recognize why the outlier rejection and the consecutive-hits guard were necessary. I didn&amp;rsquo;t write them because I had read a spec. I wrote them because raw Android readings were doing strange things and those two additions made behavior noticeably more stable.&lt;/p>
&lt;p>Duty cycling is a large part of why.&lt;/p>
&lt;p>The guards are not a workaround for bad code. They are a response to how the Android Bluetooth stack actually behaves.&lt;/p>
&lt;p>&lt;strong>If you need better readings&lt;/strong>&lt;/p>
&lt;p>A few things that help:&lt;/p>
&lt;ul>
&lt;li>Use &lt;code>SCAN_MODE_LOW_LATENCY&lt;/code> if battery is not a concern&lt;/li>
&lt;li>Increase your smoothing &lt;code>alpha&lt;/code> slightly on Android compared to iOS&lt;/li>
&lt;li>Be more aggressive with outlier rejection — a 15 dBm threshold is reasonable, but you may want to go lower&lt;/li>
&lt;/ul>
&lt;p>For my emulator it was enough to just know that the jumpiness was expected, and not a sign that something else was wrong.&lt;/p>
&lt;h4 class="comment-header">Comments&lt;/h4>
&lt;h7>&lt;a href="mailto:IrisLovesCode@gmail.com?subject=Why%20BLE%20RSSI%20Spikes%20on%20Android">Leave a comment below, or by email.&lt;/a>&lt;/h7>
- https://www.irisclasson.com/2026/02/27/why-ble-rssi-spikes-on-android/ -</description></item><item><title>Approximating BLE Proximity with RSSI in .NET on iOS</title><link>https://www.irisclasson.com/2026/02/20/approximating-ble-proximity-with-rssi-in-.net-on-ios/</link><pubDate>Fri, 20 Feb 2026 00:00:00 +0000</pubDate><guid>https://www.irisclasson.com/2026/02/20/approximating-ble-proximity-with-rssi-in-.net-on-ios/</guid><description>Iris Classon - In Love with Code https://www.irisclasson.com/2026/02/20/approximating-ble-proximity-with-rssi-in-.net-on-ios/ -&lt;p>I&amp;rsquo;ve continued working on my hobby project, a Bluetooth device emulator, and at some point I had what felt like a brilliant idea. I wanted to map out nearby Bluetooth devices from the receiving end and get a sense of where they were located.&lt;/p>
&lt;p>That turns out not to be a straightforward problem. Bluetooth doesn&amp;rsquo;t tell you where something is. What you get is signal strength, and distance is just one of many factors that affect it.&lt;/p>
&lt;p>Still, it felt like there had to be a way. I started playing around with this mostly for fun, knowing that I wouldn&amp;rsquo;t be able to produce a reliable distance estimate good enough for real-world use. There are simply too many variables that influence the signal.&lt;/p>
&lt;p>But for the sake of curiosity, I wanted to see how far I could get.&lt;/p>
&lt;p>&lt;strong>RSSI&lt;/strong>&lt;/p>
&lt;p>RSSI, Received Signal Strength Indicator, is a measure of how strong a received radio signal is. In BLE it is expressed in negative dBm values:&lt;/p>
&lt;ul>
&lt;li>closer to 0 means stronger signal&lt;/li>
&lt;li>more negative means weaker signal&lt;/li>
&lt;/ul>
&lt;p>So -50 is stronger than -80.&lt;/p>
&lt;p>The important detail is that RSSI is not a distance. It is affected by distance, but also by a number of other factors that can easily dominate the reading.&lt;/p>
&lt;p>&lt;strong>What affects RSSI the most&lt;/strong>&lt;/p>
&lt;p>&lt;em>Obstacles and materials&lt;/em>&lt;/p>
&lt;p>Walls, metal, and even people can significantly reduce signal strength. The human body is mostly water, which absorbs 2.4 GHz signals very effectively. Simply putting a phone in your pocket can change RSSI noticeably.&lt;/p>
&lt;p>&lt;em>Indoor reflections and multipath&lt;/em>&lt;/p>
&lt;p>Indoors, signals bounce off surfaces and arrive via multiple paths. Some reinforce each other, others cancel out. This creates rapid fluctuations in RSSI even when nothing is moving.&lt;/p>
&lt;p>&lt;em>Distance and transmit power&lt;/em>&lt;/p>
&lt;p>Distance still matters, and so does transmit power, but they are only part of the picture. Two identical distances can produce very different RSSI values depending on the environment.&lt;/p>
&lt;p>&lt;strong>The distance formula&lt;/strong>&lt;/p>
&lt;p>There is a well-known formula to estimate distance:&lt;/p>
&lt;pre tabindex="0">&lt;code>distance ≈ 10 ^ ((TxPowerAt1m - RSSI) / (10 * n))
&lt;/code>&lt;/pre>&lt;p>It works in theory, but in practice it depends on two values you rarely know precisely:&lt;/p>
&lt;ul>
&lt;li>RSSI at exactly 1 meter for your specific devices&lt;/li>
&lt;li>the environment factor &lt;code>n&lt;/code>, which changes depending on walls, furniture, and people&lt;/li>
&lt;/ul>
&lt;p>You can calibrate these values, but even then the estimate will drift as soon as conditions change.&lt;/p>
&lt;p>The result is that distance calculations tend to look precise but behave inconsistently.&lt;/p>
&lt;p>&lt;strong>The math behind it&lt;/strong>&lt;/p>
&lt;p>The formula is based on a simplified radio propagation model called the log-distance path loss model, which in turn is derived from the Friis transmission equation.&lt;/p>
&lt;p>The key idea is that signal strength does not decrease linearly with distance. It decreases logarithmically as the signal spreads out.&lt;/p>
&lt;p>In practice, engineers express signal strength in decibels, which turns the relationship into a linear equation:&lt;/p>
&lt;pre tabindex="0">&lt;code>RSSI = TxPowerAt1m - 10 * n * log10(d)
&lt;/code>&lt;/pre>&lt;p>Where:&lt;/p>
&lt;ul>
&lt;li>&lt;code>d&lt;/code> is distance&lt;/li>
&lt;li>&lt;code>TxPowerAt1m&lt;/code> is the expected RSSI at 1 meter&lt;/li>
&lt;li>&lt;code>n&lt;/code> is the path-loss exponent&lt;/li>
&lt;/ul>
&lt;p>If you rearrange that equation to solve for distance, you get:&lt;/p>
&lt;pre tabindex="0">&lt;code>d = 10 ^ ((TxPowerAt1m - RSSI) / (10 * n))
&lt;/code>&lt;/pre>&lt;p>That is the formula we started with.&lt;/p>
&lt;p>What it is really doing is estimating how far away a device would be if the signal behaved in a smooth, predictable way. Indoors, that assumption rarely holds.&lt;/p>
&lt;p>&lt;strong>Proximity, not distance&lt;/strong>&lt;/p>
&lt;p>Instead of asking &amp;ldquo;how many meters away is this device,&amp;rdquo; a more reliable question is: is it very close, nearby, or far away?&lt;/p>
&lt;p>That maps much better to what RSSI can actually tell you.&lt;/p>
&lt;p>A simple set of buckets might look like this:&lt;/p>
&lt;ul>
&lt;li>&lt;code>&amp;gt;= -60&lt;/code> → immediate&lt;/li>
&lt;li>&lt;code>-60 to -75&lt;/code> → near&lt;/li>
&lt;li>&lt;code>-75 to -88&lt;/code> → far&lt;/li>
&lt;li>&lt;code>&amp;lt; -88&lt;/code> → unknown&lt;/li>
&lt;/ul>
&lt;p>These values are not universal. You will need to tune them based on your environment and hardware.&lt;/p>
&lt;p>&lt;strong>Working with Core Bluetooth in .NET&lt;/strong>&lt;/p>
&lt;p>When scanning, you get RSSI from the discovery callback. When connected, you explicitly request updates.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8be9fd;font-style:italic">public&lt;/span> &lt;span style="color:#8be9fd;font-style:italic">override&lt;/span> &lt;span style="color:#ff79c6">void&lt;/span> DiscoveredPeripheral(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CBCentralManager central,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CBPeripheral peripheral,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> NSDictionary advertisementData,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> NSNumber RSSI)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">if&lt;/span> (RSSI != &lt;span style="color:#ff79c6">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd">var&lt;/span> &lt;span style="color:#ff79c6">value&lt;/span> = RSSI.Int32Value;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#6272a4">// feed into estimator&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;p>For connected devices:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>peripheral.ReadRSSI();&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;p>And handle the callback:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8be9fd;font-style:italic">public&lt;/span> &lt;span style="color:#8be9fd;font-style:italic">override&lt;/span> &lt;span style="color:#ff79c6">void&lt;/span> RssiRead(CBPeripheral peripheral, NSNumber rssi, NSError error)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">if&lt;/span> (error != &lt;span style="color:#ff79c6">null&lt;/span> || rssi == &lt;span style="color:#ff79c6">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">return&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd">var&lt;/span> &lt;span style="color:#ff79c6">value&lt;/span> = rssi.Int32Value;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#6272a4">// feed into estimator&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;p>&lt;strong>Smoothing RSSI&lt;/strong>&lt;/p>
&lt;p>Raw RSSI is too unstable to use directly. You need to smooth it.&lt;/p>
&lt;p>A simple and effective approach is an exponential moving average:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>_ema = alpha * rssi + (&lt;span style="color:#bd93f9">1&lt;/span> - alpha) * _ema;&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;p>Where &lt;code>alpha&lt;/code> is typically between 0.2 and 0.3.&lt;/p>
&lt;p>You should also ignore obvious outliers. If a value suddenly jumps by 15 dB compared to your current average, it is often just noise.&lt;/p>
&lt;p>Instead of switching proximity immediately, require a few consecutive readings before changing state. This stabilizes the experience significantly.&lt;/p>
&lt;p>&lt;strong>A practical estimator&lt;/strong>&lt;/p>
&lt;p>Putting it together:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8be9fd;font-style:italic">public&lt;/span> &lt;span style="color:#8be9fd;font-style:italic">enum&lt;/span> BleProximity
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Unknown,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Immediate,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Near,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Far
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8be9fd;font-style:italic">public&lt;/span> &lt;span style="color:#8be9fd;font-style:italic">sealed&lt;/span> &lt;span style="color:#ff79c6">class&lt;/span> &lt;span style="color:#50fa7b">BleRssiEstimator&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd;font-style:italic">private&lt;/span> &lt;span style="color:#8be9fd">double?&lt;/span> _ema;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd;font-style:italic">private&lt;/span> &lt;span style="color:#ff79c6">readonly&lt;/span> &lt;span style="color:#8be9fd">double&lt;/span> _alpha = &lt;span style="color:#bd93f9">0.25&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd;font-style:italic">private&lt;/span> BleProximity _current = BleProximity.Unknown;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd;font-style:italic">private&lt;/span> BleProximity _candidate;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd;font-style:italic">private&lt;/span> &lt;span style="color:#8be9fd">int&lt;/span> _candidateHits;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd;font-style:italic">public&lt;/span> &lt;span style="color:#ff79c6">void&lt;/span> AddSample(&lt;span style="color:#8be9fd">int&lt;/span> rssi)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">if&lt;/span> (rssi &amp;gt;= &lt;span style="color:#bd93f9">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">return&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">if&lt;/span> (_ema.HasValue &amp;amp;&amp;amp; Math.Abs(rssi - _ema.Value) &amp;gt; &lt;span style="color:#bd93f9">15&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">return&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _ema = _ema.HasValue
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ? _alpha * rssi + (&lt;span style="color:#bd93f9">1&lt;/span> - _alpha) * _ema.Value
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> : rssi;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> UpdateProximity();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd;font-style:italic">public&lt;/span> BleProximity Current =&amp;gt; _current;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd;font-style:italic">private&lt;/span> &lt;span style="color:#ff79c6">void&lt;/span> UpdateProximity()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">if&lt;/span> (!_ema.HasValue)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">return&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd">var&lt;/span> next = Classify(_ema.Value);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">if&lt;/span> (next == _current)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _candidateHits = &lt;span style="color:#bd93f9">0&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">return&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">if&lt;/span> (next != _candidate)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _candidate = next;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _candidateHits = &lt;span style="color:#bd93f9">1&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">return&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">if&lt;/span> (++_candidateHits &amp;gt;= &lt;span style="color:#bd93f9">3&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _current = next;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _candidateHits = &lt;span style="color:#bd93f9">0&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd;font-style:italic">private&lt;/span> &lt;span style="color:#8be9fd;font-style:italic">static&lt;/span> BleProximity Classify(&lt;span style="color:#8be9fd">double&lt;/span> rssi)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">if&lt;/span> (rssi &amp;gt;= -&lt;span style="color:#bd93f9">60&lt;/span>) &lt;span style="color:#ff79c6">return&lt;/span> BleProximity.Immediate;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">if&lt;/span> (rssi &amp;gt;= -&lt;span style="color:#bd93f9">75&lt;/span>) &lt;span style="color:#ff79c6">return&lt;/span> BleProximity.Near;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">if&lt;/span> (rssi &amp;gt;= -&lt;span style="color:#bd93f9">88&lt;/span>) &lt;span style="color:#ff79c6">return&lt;/span> BleProximity.Far;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">return&lt;/span> BleProximity.Unknown;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>
&lt;p>&lt;strong>Calibration matters&lt;/strong>&lt;/p>
&lt;p>If you want reasonable behavior, you need to calibrate:&lt;/p>
&lt;ol>
&lt;li>Place devices 1 meter apart&lt;/li>
&lt;li>Measure RSSI for 10–20 seconds&lt;/li>
&lt;li>Average the result&lt;/li>
&lt;li>Adjust your thresholds based on real-world testing&lt;/li>
&lt;/ol>
&lt;p>Even small changes in placement or environment can shift results noticeably, so calibration is not a one-time task.&lt;/p>
&lt;p>RSSI works best when you:&lt;/p>
&lt;ul>
&lt;li>treat it as a relative signal, not a precise measurement&lt;/li>
&lt;li>smooth aggressively&lt;/li>
&lt;li>use proximity buckets instead of meters&lt;/li>
&lt;/ul>
&lt;h4 class="comment-header">Comments&lt;/h4>
&lt;h7>&lt;a href="mailto:IrisLovesCode@gmail.com?subject=Approximating%20BLE%20Proximity%20with%20RSSI%20in%20.NET%20on%20iOS">Leave a comment below, or by email.&lt;/a>&lt;/h7>
- https://www.irisclasson.com/2026/02/20/approximating-ble-proximity-with-rssi-in-.net-on-ios/ -</description></item><item><title>Why Your BLE Manufacturer Data Disappears on macOS (.NET + CoreBluetooth)</title><link>https://www.irisclasson.com/2026/02/05/why-your-ble-manufacturer-data-disappears-on-macos-.net--corebluetooth/</link><pubDate>Thu, 05 Feb 2026 00:00:00 +0000</pubDate><guid>https://www.irisclasson.com/2026/02/05/why-your-ble-manufacturer-data-disappears-on-macos-.net--corebluetooth/</guid><description>Iris Classon - In Love with Code https://www.irisclasson.com/2026/02/05/why-your-ble-manufacturer-data-disappears-on-macos-.net--corebluetooth/ -&lt;p>If you&amp;rsquo;ve ended up here, I am sorry. I also hope you have not spent as much time as I did trying to figure out why the manufacturer data is always empty on the receiving end.&lt;/p>
&lt;p>I have been working on a .NET MAUI BLE device emulator and realized, after an embarrassing amount of debugging, that Apple does not let us include more than a local name and service UUIDs when advertising with Core Bluetooth.&lt;/p>
&lt;p>The supported keys are:&lt;/p>
&lt;ul>
&lt;li>&lt;code>CBAdvertisementDataLocalNameKey&lt;/code>&lt;/li>
&lt;li>&lt;code>CBAdvertisementDataServiceUUIDsKey&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>Which is unfortunate, because I needed manufacturer data for my emulator devices. More precisely, I needed a way to send more data without connecting.&lt;/p>
&lt;p>Apple&amp;rsquo;s approach is simple. Advertise just enough to connect, then exchange real data after connection. I would have saved myself a lot of time if I had internalized that earlier.&lt;/p>
&lt;p>&lt;a href="https://developer.apple.com/documentation/corebluetooth/cbperipheralmanager/startadvertising(_:)">https://developer.apple.com/documentation/corebluetooth/cbperipheralmanager/startadvertising(_:)&lt;/a>&lt;/p>
&lt;p>My assumption was straightforward. If I can read manufacturer data when scanning, I should be able to send manufacturer data when advertising.&lt;/p>
&lt;p>Nope. That symmetry does not exist here.&lt;/p>
&lt;p>So, workarounds it is.&lt;/p>
&lt;h2 id="option-1-use-gatt-for-the-real-data">Option 1: Use GATT for the real data&lt;/h2>
&lt;p>Advertise just enough to be discovered, then use GATT for the actual data.&lt;/p>
&lt;p>That means advertising a single service UUID, optionally adding a short local name, and then reading a characteristic after connecting. That characteristic can contain whatever you need, like serial number, firmware version, device role, owner tag, or location.&lt;/p>
&lt;h2 id="option-2-encode-identity-in-the-service-uuid">Option 2: Encode identity in the service UUID&lt;/h2>
&lt;p>Another option is to encode identity into the service UUID itself.&lt;/p>
&lt;p>For example, you can use a fixed base UUID and let the last bytes represent device type, instance, or region. It is not pretty, but it is reliable on Apple platforms.&lt;/p>
&lt;h2 id="option-3-use-the-local-name">Option 3: Use the local name&lt;/h2>
&lt;p>You can also use the local name to carry a bit of extra information.&lt;/p>
&lt;p>Something like &lt;code>DEbtn1&lt;/code>, where &lt;code>DE&lt;/code> stands for Device Emulator and &lt;code>btn1&lt;/code> identifies the device. You do not have many bytes to work with, but it is often enough to distinguish your devices in a crowded BLE environment.&lt;/p>
&lt;p>If you have a smarter approach, I would love to hear it. For now, I am combining all three strategies in my emulator.&lt;/p>
&lt;p>There will likely be more posts like this as I continue exploring CoreBluetooth and working with Bluetooth in .NET and .NET MAUI.&lt;/p>
&lt;h4 class="comment-header">Comments&lt;/h4>
&lt;h7>&lt;a href="mailto:IrisLovesCode@gmail.com?subject=Why%20Your%20BLE%20Manufacturer%20Data%20Disappears%20on%20macOS%20%28.NET%20%2b%20CoreBluetooth%29">Leave a comment below, or by email.&lt;/a>&lt;/h7>
- https://www.irisclasson.com/2026/02/05/why-your-ble-manufacturer-data-disappears-on-macos-.net--corebluetooth/ -</description></item></channel></rss>