<?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: DMJ Jones</title>
    <description>The latest articles on DEV Community by DMJ Jones (@dmj_jones_8ec54a3564709a8).</description>
    <link>https://dev.to/dmj_jones_8ec54a3564709a8</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%2F1928613%2F5241db88-d55d-4692-a5b5-a058ad3a6ccf.jpg</url>
      <title>DEV Community: DMJ Jones</title>
      <link>https://dev.to/dmj_jones_8ec54a3564709a8</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dmj_jones_8ec54a3564709a8"/>
    <language>en</language>
    <item>
      <title>Your certificate inventory is exact. Your tickets are not.</title>
      <dc:creator>DMJ Jones</dc:creator>
      <pubDate>Sun, 20 Sep 2026 10:22:11 +0000</pubDate>
      <link>https://dev.to/dmj_jones_8ec54a3564709a8/your-certificate-inventory-is-exact-your-tickets-are-not-41gj</link>
      <guid>https://dev.to/dmj_jones_8ec54a3564709a8/your-certificate-inventory-is-exact-your-tickets-are-not-41gj</guid>
      <description>&lt;p&gt;Every certificate team has read this ticket:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We renewed the certificate yesterday and I can see the new one in the portal, but about half of our customers still get a warning.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two things are true at once here. Your inventory knows exactly what is deployed, down to the serial and the second of expiry. And the person who filed the ticket has told you almost nothing you can query on.&lt;/p&gt;

&lt;p&gt;The gap between those two sentences is where certificate triage actually happens, and it is the part nobody automates. I spent a week building a triage tool to see how much of that gap a model can close, and — more usefully — where it should not be allowed anywhere near.&lt;/p&gt;

&lt;h2&gt;
  
  
  The split
&lt;/h2&gt;

&lt;p&gt;The rule I started with, and never had cause to break: &lt;strong&gt;if the answer is computable, compute it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Expiry maths, chain building for a given client's trust store, SAN coverage including wildcards, whether a CAA record permits the CA your ACME client is using, which endpoints serve which serial — none of that belongs in a model. It is arithmetic and string matching, and a model can only make it less reliable.&lt;/p&gt;

&lt;p&gt;What code cannot do is read "about half of our customers" and know that means some nodes behind a load balancer still serve the old certificate. Or read "trust anchor for certification path not found, but the website works in Chrome on the same phone" and understand that the phrase &lt;em&gt;on the same phone&lt;/em&gt; is the whole diagnosis, because it rules out the trust store and points at a missing intermediate.&lt;/p&gt;

&lt;p&gt;So: code owns the facts, the model reads the prose, and a fixed policy in ordinary code decides what happens. The model never acts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't hand it the inventory
&lt;/h2&gt;

&lt;p&gt;The tempting design is to give the model the estate and ask which certificate the ticket is about. It does not survive contact with a real inventory.&lt;/p&gt;

&lt;p&gt;A synthetic estate of 500 certificates costs about 20,000 tokens to describe, which is most of a request before you have asked anything. At 10,000 certificates it does not fit at all. Accuracy also drops as you pad the input with irrelevant detail, which is the documented failure mode for this class of model and the intuitive one for any other.&lt;/p&gt;

&lt;p&gt;What works is the boring thing. Code extracts the CN or hostname — from the intake form if the reporter gave one, otherwise from the ticket text — and matches it against the inventory exactly: common name, SANs, wildcards, and every endpoint serving that host. Only the matches go to the model, capped at twelve. The other 488 certificates never leave the server.&lt;/p&gt;

&lt;p&gt;That cap is what makes the cost independent of the estate. Triaging a ticket against 500 certificates and against ten million costs the same, because the model sees the same handful either way.&lt;/p&gt;

&lt;h2&gt;
  
  
  When it cannot tell, it asks
&lt;/h2&gt;

&lt;p&gt;Plenty of tickets name no host at all. "Old iPhones stopped loading product images this morning." For those, one question to the model — which service in the catalogue is this about — is enough for code to do the lookup.&lt;/p&gt;

&lt;p&gt;But sometimes the honest answer is that nobody knows yet. A ticket about mTLS failures between two internal services mapped to a service with 81 certificates, one per pod. Twelve is the cap, 81 is well past it, and picking one would be theatre.&lt;/p&gt;

&lt;p&gt;So the tool asked the reporter for the exact CN, along with the error, where it fails and since when.&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%2Fur1zpa92uidmr4xqlp73.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%2Fur1zpa92uidmr4xqlp73.png" alt="Triage outcome: 81 certificates match, too many to guess from. Ask for the certificate's CN or hostname, the exact error, where it fails, and since when." width="800" height="253"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The answer came back as &lt;code&gt;orders-17&lt;/code&gt;. Code then found, in one exact lookup, a pod still serving a certificate that had expired three hours earlier — while the renewed one sat in the inventory, issued and never deployed. cert-manager had done its job. The pod never reloaded.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Asking "which CN?" costs one message. Guessing across 81 certificates costs an incident review.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What the model is actually for
&lt;/h2&gt;

&lt;p&gt;The model I used is Jev, TypeSafe's System One model, which does not generate text at all. You send it state and typed questions, and it returns typed answers with probabilities: one option from a set you defined, a position on a scale you defined, or the probability that a yes/no statement is true. There is nothing to parse and no prose to second-guess.&lt;/p&gt;

&lt;p&gt;Triage became two requests. The first gets the ticket and nothing else, and answers what the reporter is describing, how urgent it sounds, and which service it concerns. The second gets the matched certificates and the findings the automated checks produced, and asks one yes/no question per finding: would this problem cause the failure this person is describing?&lt;/p&gt;

&lt;p&gt;Keeping the inventory out of the first request is deliberate. It means the reading of the symptoms and the reading of the evidence are independent, so when they agree, the agreement is worth something. That cross-check is the part I would keep in any design, whatever model sits behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then I measured it against an ordinary LLM
&lt;/h2&gt;

&lt;p&gt;The obvious question: why not just call a normal model? So I built the same pipeline again on gemini-3.5-flash-lite — same lookup code, same questions, same policy, structured JSON output — and ran both over the same tickets.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Same 9 tickets, same code, same policy

Jev (jev-1.13.0)          8 of 9 causes   596 ms    $0.09 / 1,000 tickets
gemini-3.5-flash-lite     8 of 9 causes 1,420 ms    $0.94 / 1,000 tickets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Accuracy tied, and they failed on different tickets, which is about what nine samples can tell you. Ten times the cost and a bit over twice the latency is a real difference at volume, but it is not the interesting part.&lt;/p&gt;

&lt;p&gt;The interesting part was the probabilities. Asked "would this finding cause what the reporter describes?", the LLM answered &lt;strong&gt;1.00&lt;/strong&gt;. Every finding, every ticket, including a stale-certificate finding paired with a ticket about payment timeouts that had nothing to do with TLS. Jev gave that pair &lt;strong&gt;0.29&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The LLM did not act wrongly on it, but only because its self-reported cause confidence happened to land on 0.50 that time — and its confidences across the run were 0.50, 0.80, 0.85, 0.90, 0.95. Round numbers it wrote about itself.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A threshold is only as good as the number you put it on.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you are gating an automatic action on a probability, that is the whole ballgame. A model that says 1.00 to everything has not given you a probability. It has given you a shape that looks like one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would require before letting this act
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No probability moves a key.&lt;/strong&gt; Issuance, revocation and export stay behind RBAC and human approval. A model can rank, link and explain. It does not get to sign.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Narrow in code first.&lt;/strong&gt; A handful of candidates, never the estate. It is cheaper, more accurate, and the cost stops scaling with your inventory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask a person a specific question&lt;/strong&gt; when the narrowing is ambiguous. "Which CN?" beats a confident guess.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the probability, the thresholds and the model version into the audit log&lt;/strong&gt; next to the decision. A decision you cannot replay six months later is not auditable, and "the model said so" is not a root cause.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pin the model version.&lt;/strong&gt; Thresholds are tuned against specific behaviour, and an alias like &lt;code&gt;jev-latest&lt;/code&gt; moves underneath you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure wrong automatic actions, not accuracy.&lt;/strong&gt; Accuracy is a number for a slide. The operational question is how often the system did something it should not have.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where this goes
&lt;/h2&gt;

&lt;p&gt;Ticket triage was the demo, because a ticket is the clearest case of prose meeting exact data. The same shape fits three more jobs I care about rather more, and they are next for &lt;a href="https://certpilot.mervinjones.dev/" rel="noopener noreferrer"&gt;CertPilot&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Discovery.&lt;/strong&gt; CT logs surface a certificate for one of your domains that the inventory cannot match by name. Is it a team you do not know about, a SaaS vendor operating legitimately, or a lookalike worth an alert?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Renewal failures.&lt;/strong&gt; A durable queue with retries is correct design and wrong behaviour for a CAA misconfiguration — it will retry twenty times and burn the CA's rate limit. Code reads the structured ACME error; something else has to read the prose the CA sends back and decide retry, back off, or hand over.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollout safety.&lt;/strong&gt; During a deployment wave, the signal that you broke an old client usually arrives as a support ticket, not a failed probe. Link fresh tickets to the hosts just deployed, and hold the wave.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The demo runs on a synthetic estate with invented tickets, so treat the numbers as a demonstration rather than a benchmark: fourteen labelled tickets, nine of them in the LLM comparison because of a free-tier quota. What is real is the model traffic, the latency and the cost, and every request and response is on screen.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://certradar-smoky.vercel.app/" rel="noopener noreferrer"&gt;CertRadar is here&lt;/a&gt; if you want to click through it. Every step shows the exact JSON that went to the model, the exact JSON that came back, and what the decision cost.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>devops</category>
      <category>jev</category>
    </item>
    <item>
      <title>I co-engineered a macOS Soundcore headphone controller with Claude over a weekend</title>
      <dc:creator>DMJ Jones</dc:creator>
      <pubDate>Mon, 14 Sep 2026 19:54:11 +0000</pubDate>
      <link>https://dev.to/dmj_jones_8ec54a3564709a8/i-co-engineered-a-macos-soundcore-headphone-controller-with-claude-over-a-weekend-216g</link>
      <guid>https://dev.to/dmj_jones_8ec54a3564709a8/i-co-engineered-a-macos-soundcore-headphone-controller-with-claude-over-a-weekend-216g</guid>
      <description>&lt;p&gt;My Soundcore Space 2 pairs with my MacBook and sounds exactly as it should. But&lt;br&gt;
the moment I want to turn noise cancelling off, or pull the bass back, I have to&lt;br&gt;
pick up my phone and open the Soundcore app — because Anker only ships that app&lt;br&gt;
for Android and iOS.&lt;/p&gt;

&lt;p&gt;The headphones are right there. The Mac is right there. The controls are on a&lt;br&gt;
third device.&lt;/p&gt;

&lt;p&gt;So over a single weekend, I decided to pair-program and co-engineer a solution with&lt;br&gt;
Claude to see if we could reverse-engineer the protocol and build&lt;br&gt;
&lt;a href="https://github.com/mervin008/soundcorebridge" rel="noopener noreferrer"&gt;SoundcoreBridge&lt;/a&gt;: a native macOS menu bar app&lt;br&gt;
and CLI that speaks the same protocol. And it actually worked.&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%2Fwcdbcd75fti6uho72fjm.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%2Fwcdbcd75fti6uho72fjm.png" alt="The SoundcoreBridge panel" width="800" height="924"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the story of how that weekend went, what co-engineering low-level hardware&lt;br&gt;
code with an LLM looks like in practice, and the two occasions I confidently&lt;br&gt;
concluded macOS was broken when the bug was mine.&lt;/p&gt;
&lt;h2&gt;
  
  
  Finding the door
&lt;/h2&gt;

&lt;p&gt;Bluetooth devices advertise their services over SDP. Dumping the Space 2's&lt;br&gt;
records shows the usual audio profiles — A2DP, AVRCP, handsfree — and then some&lt;br&gt;
more interesting entries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ch 30   PaulWang-Spp   0cf12d31-fac3-4553-bd80-d6832e7d1402   &amp;lt;- vendor control
ch 17   RFCOMM COM     df21fe2c-2515-4fdb-8886-f12c4d67927c
ch 12   TOTA           0x1101                                  &amp;lt;- firmware OTA
ch 13   BESOTA         66666666-...                            &amp;lt;- chipset OTA
ch 16   IOSSPP         00000000-deca-fade-deca-deafdecacaff     &amp;lt;- Apple iAP2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That first UUID shares its prefix (&lt;code&gt;0cf12d31-fac3-4553-bd80-&lt;/code&gt;) with the&lt;br&gt;
documented Soundcore control UUID; the suffix is per-model. Channel 30 is the&lt;br&gt;
door.&lt;/p&gt;

&lt;p&gt;Channels 12 and 13 flash firmware. Those are hard-blocked in my code by service&lt;br&gt;
identity, and I'd suggest anyone doing this does the same before writing a&lt;br&gt;
single byte.&lt;/p&gt;
&lt;h2&gt;
  
  
  The frame format
&lt;/h2&gt;

&lt;p&gt;Prior art from other Soundcore projects gave me the shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;08 EE 00 00 | 00 | 06 81 | 10 00 | 00 5F 02 00 00 01 | EF
└─ magic ─┘  seq  └ cmd ┘ └ len ┘ └───  payload  ──┘  checksum
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two-byte command, little-endian total length, and a checksum that's just the sum&lt;br&gt;
of every preceding byte truncated to 8 bits. Device replies swap the magic to&lt;br&gt;
&lt;code&gt;09 FF 00 00&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Before touching hardware I implemented the codec and checked it against six&lt;br&gt;
packets documented for other models. All six reproduced byte-for-byte. That&lt;br&gt;
five-minute investment meant that later, when nothing worked, I could rule out&lt;br&gt;
the encoder completely.&lt;/p&gt;
&lt;h2&gt;
  
  
  Two days of "macOS blocks this"
&lt;/h2&gt;

&lt;p&gt;Then I tried to open the channel, and nothing happened.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;openRFCOMMChannelAsync&lt;/code&gt; returned success and the delegate callback never fired.&lt;br&gt;
The synchronous variant returned &lt;code&gt;kIOReturnError&lt;/code&gt; immediately. I tried channel&lt;br&gt;
30, then 17, then channel 1 (handsfree — definitely exists), then channel 2&lt;br&gt;
(nothing advertised). Identical failure every time.&lt;/p&gt;

&lt;p&gt;So I ran what I thought was a decisive control experiment: I tried opening&lt;br&gt;
RFCOMM to a &lt;strong&gt;completely different device&lt;/strong&gt; — an Android phone, on standard&lt;br&gt;
services like OBEX and phonebook access. Same failure.&lt;/p&gt;

&lt;p&gt;Two devices, seven channels, standard services, from both a bare binary and a&lt;br&gt;
signed &lt;code&gt;.app&lt;/code&gt;. &lt;code&gt;bluetoothd&lt;/code&gt; logs showed no connect attempt ever reaching the air.&lt;br&gt;
I checked TCC (granted), App Sandbox (not sandboxed), the baseband link&lt;br&gt;
(connected). I wrote it up as a platform limitation and started designing a&lt;br&gt;
Raspberry Pi bridge.&lt;/p&gt;

&lt;p&gt;I was wrong.&lt;/p&gt;

&lt;p&gt;Claude — who I was pair-programming with throughout the weekend — pushed back on my&lt;br&gt;
theory and surfaced &lt;a href="https://github.com/AmitRajput-Dev/SonyBridge" rel="noopener noreferrer"&gt;SonyBridge&lt;/a&gt;, an&lt;br&gt;
open-source project doing exactly this on macOS. It exists. It works. That single&lt;br&gt;
pointer broke my confirmation bias. I read its implementation, reproduced its&lt;br&gt;
approach in a standalone spike with Claude, and the channel opened.&lt;/p&gt;

&lt;p&gt;Diffing my spike against my app turned up three things:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The first &lt;code&gt;openRFCOMMChannelAsync&lt;/code&gt; in a process always fails.&lt;/strong&gt; It appears&lt;br&gt;
to lazily initialise IOBluetooth's run-loop source, and the delegate never fires&lt;br&gt;
for that attempt. A retry succeeds. My spike had accidentally made two throwaway&lt;br&gt;
attempts against the phone before the one that worked — which is also why my&lt;br&gt;
"control experiment" failed identically on both devices. It was measuring my own&lt;br&gt;
bug twice and I read it as corroboration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. IOBluetooth delivers delegate callbacks on the run loop of the thread that&lt;br&gt;
opened the channel&lt;/strong&gt; — not on a dispatch queue. I was awaiting a Swift&lt;br&gt;
continuation, which never sees them. You have to pump the run loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;pump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;TimeInterval&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;until&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addingTimeInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="kt"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;RunLoop&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;until&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addingTimeInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;until&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;}&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 is also why CoreBluetooth worked fine for me all along while IOBluetooth&lt;br&gt;
never did — CoreBluetooth uses a dispatch queue. I had that evidence and read it&lt;br&gt;
backwards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Never close a failed attempt.&lt;/strong&gt; The close lands asynchronously and tears&lt;br&gt;
down whichever channel succeeds next.&lt;/p&gt;
&lt;h2&gt;
  
  
  Reads work. Writes vanish.
&lt;/h2&gt;

&lt;p&gt;With the channel open, &lt;code&gt;01:01&lt;/code&gt; returns 103 bytes, and there's readable ASCII in&lt;br&gt;
there straight away — &lt;code&gt;01.59&lt;/code&gt;, &lt;code&gt;1402&lt;/code&gt;, and the headset's own MAC. Firmware,&lt;br&gt;
model, serial.&lt;/p&gt;

&lt;p&gt;Writes were another matter. I sent what I was sure was the noise-cancelling&lt;br&gt;
command and got nothing. No error. No NAK. No state change. The device&lt;br&gt;
acknowledged reads perfectly and acted as though writes had never arrived.&lt;/p&gt;

&lt;p&gt;I burned a lot of time guessing command bytes. What actually solved it was&lt;br&gt;
capturing the Android app's own traffic: enable &lt;strong&gt;Bluetooth HCI snoop log&lt;/strong&gt; in&lt;br&gt;
developer options, drive the app, pull a bug report, and scan the capture for&lt;br&gt;
the frame magic.&lt;/p&gt;

&lt;p&gt;The capture showed a handshake the app performs on connect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;01:01                    device info
05:01  payload 01        capability table
05:81  (no payload) x2
05:81  payload 01
05:81  (no payload)
18:85  payload 01
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Until that runs, the device discards every write in silence. That's not&lt;br&gt;
documented anywhere I could find, and I don't think I'd ever have guessed it.&lt;/p&gt;

&lt;p&gt;The same capture caught something subtler. The sound-mode write is &lt;code&gt;06:81&lt;/code&gt; with&lt;br&gt;
payload &lt;code&gt;[mode, level, 02, 00, 00, 01]&lt;/code&gt;. I'd been building that payload by&lt;br&gt;
reading the current block back and modifying it — but the &lt;strong&gt;read returns &lt;code&gt;FF&lt;/code&gt; in&lt;br&gt;
byte 2 where the write requires &lt;code&gt;02&lt;/code&gt;&lt;/strong&gt;. Echoing back what you read gets you&lt;br&gt;
silently ignored. That single byte was most of a day.&lt;/p&gt;
&lt;h2&gt;
  
  
  Mapping the rest by diffing
&lt;/h2&gt;

&lt;p&gt;Once writes worked, the remaining fields fell out of a simple loop: hold the&lt;br&gt;
channel open from the Mac, poll the state blob, change one setting on the phone,&lt;br&gt;
and diff the bytes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CHANGED 01:01
  byte[71] (0x47): 02 → 01
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the ANC mode byte. Same method found the EQ preset at byte 23, the eight&lt;br&gt;
band values at 25–32, and — after an embarrassing detour — the ANC strength in&lt;br&gt;
the &lt;strong&gt;high nibble of byte 72&lt;/strong&gt;, not byte 70 as I first assumed. Byte 70 is a&lt;br&gt;
constant &lt;code&gt;05&lt;/code&gt;: the number of levels supported, not the level selected.&lt;/p&gt;

&lt;p&gt;Battery is byte 0, and it reads 0–9 rather than a percentage. That's Apple's&lt;br&gt;
accessory convention (&lt;code&gt;AT+IPHONEACCEV&lt;/code&gt;): percent is &lt;code&gt;(level + 1) × 10&lt;/code&gt;. I only&lt;br&gt;
worked that out because the raw byte said &lt;code&gt;5&lt;/code&gt; while both macOS and the phone app&lt;br&gt;
said 60%.&lt;/p&gt;
&lt;h2&gt;
  
  
  The official app has a bug
&lt;/h2&gt;

&lt;p&gt;While verifying the strength levels I found something I didn't expect.&lt;/p&gt;

&lt;p&gt;I set level 4 from the Mac, reconnected the phone, and watched the exchange. The&lt;br&gt;
app asked the headset for its sound mode. The headset replied truthfully:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;11:46:10  app  → 06:01
11:46:10  dev  → 00 4F FF 00 00 01     (level 4)
          app displays: 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It queries, receives the correct answer, and renders its own cached value&lt;br&gt;
instead. So if you change anything from another host, Anker's app lies to you&lt;br&gt;
about the current state. Mine reads it from the device.&lt;/p&gt;
&lt;h2&gt;
  
  
  Making it work for more than one model
&lt;/h2&gt;

&lt;p&gt;The frame format is identical across Soundcore devices. What differs is where&lt;br&gt;
fields sit in the state blob, which value means which sound mode, and how many&lt;br&gt;
EQ bands there are. So a device is a data structure, not new protocol code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;space2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;DeviceProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;modelCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"1402"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Soundcore Space 2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;batteryOffset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;ancModeOffset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;71&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;ancLevelOffset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;72&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;eqPresetOffset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;eqBandsStart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;eqBandCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;features&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;battery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;soundMode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ancLevel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;equaliser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;customEQ&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;Unknown models resolve to a read-only profile: battery and firmware, no writes.&lt;br&gt;
I own exactly one Soundcore device, so that's the only one whose offsets I've&lt;br&gt;
confirmed. Shipping guessed offsets to someone else's headphones isn't a risk&lt;br&gt;
worth taking for the sake of a longer compatibility table.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell myself at the start
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Verify by reading back, not by absence of an error.&lt;/strong&gt; Every real bug in this&lt;br&gt;
project was silent. The device never once told me I was wrong — it just ignored&lt;br&gt;
me. &lt;code&gt;confirmed: byte[71] = 0x00&lt;/code&gt; is worth more than any number of successful&lt;br&gt;
writes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check that your control experiment isn't measuring the same bug.&lt;/strong&gt; Mine tested&lt;br&gt;
a second device and gave me false confidence, because the defect was in the code&lt;br&gt;
common to both paths.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prior art is the cheapest debugging tool there is.&lt;/strong&gt; I'd written off the whole&lt;br&gt;
platform. One link to a project that already did it turned two days of wrong&lt;br&gt;
conclusions around in an hour.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ground your AI pairing in real packet captures.&lt;/strong&gt; Claude excelled at spotting&lt;br&gt;
byte patterns, generating Swift codecs, and writing offline tests, but only&lt;br&gt;
because every prompt was grounded in real hex dumps and HCI logs. Without ground-truth&lt;br&gt;
captures, pair-programming on hardware quickly turns into mutual guesswork.&lt;/p&gt;




&lt;p&gt;SoundcoreBridge is MIT, Swift, macOS 13+. The full protocol map — state blob&lt;br&gt;
layout, handshake, all 22 EQ presets with band values, and the IOBluetooth traps&lt;br&gt;
— is in the repo, so if you'd rather write your own client than use mine, the&lt;br&gt;
hard part is already done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/mervin008/soundcorebridge" rel="noopener noreferrer"&gt;github.com/mervin008/soundcorebridge&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Not affiliated with Anker or Soundcore. Firmware-update channels are blocked in&lt;br&gt;
code; the protocol work is interoperability reverse engineering on hardware I own.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>swift</category>
      <category>ai</category>
      <category>bluetooth</category>
      <category>reverseengineering</category>
    </item>
    <item>
      <title>Ai apps at i3</title>
      <dc:creator>DMJ Jones</dc:creator>
      <pubDate>Fri, 06 Dec 2024 15:16:30 +0000</pubDate>
      <link>https://dev.to/dmj_jones_8ec54a3564709a8/ai-apps-at-i3-3bo</link>
      <guid>https://dev.to/dmj_jones_8ec54a3564709a8/ai-apps-at-i3-3bo</guid>
      <description></description>
      <category>ai</category>
    </item>
  </channel>
</rss>
