<?xml version="1.0" encoding="UTF-8"?><feed xmlns="http://www.w3.org/2005/Atom">
  <title>Serious Computer Business</title>
  <id>https://octet-stream.net/b/scb/</id>
  <updated>2026-08-17T17:43:38+10:00</updated>
  <link href="https://octet-stream.net/b/scb/"></link>
  <author>
    <name>Thomas Karpiniec</name>
    <email>tom.karpiniec@outlook.com</email>
  </author>
  <entry>
    <title>Hold on to your hats, HaLow is here</title>
    <updated>2026-07-22T22:08:54+10:00</updated>
    <id>urn:uuid:7a28591a-8e7d-4ed9-968e-3a4b9c9997d4</id>
    <content type="html">&lt;p&gt;&#xA;I’ve been waiting a long time for &lt;a href=&#34;https://en.wikipedia.org/wiki/IEEE_802.11ah&#34;&gt;WiFi HaLow (802.11ah)&lt;/a&gt; to show up. For the unaware, this is a standardised variant of WiFi using the 900 MHz ISM band to provide range over 1 km with a corresponding lower throughput—up to tens of Mbps.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;The standard was published in 2017 but for a long time the only available hardware was pretty specialised and I stopped paying attention. Manufacturing has increased and around 18 months ago Heltec started selling a properly convenient USB/ethernet bridge device, the &lt;a href=&#34;https://heltec.org/project/ht-hd01/&#34;&gt;HT-HD01&lt;/a&gt;. Now that I know they exist, I have ordered four of them for mesh experiments. These units use a chip &lt;a href=&#34;https://www.morsemicro.com/chips/&#34;&gt;designed by Morse Micro&lt;/a&gt;, which I am proud to discover is an Australian company&lt;a id=&#34;footnote-1-ref&#34; href=&#34;#footnote-1&#34;&gt;[1]&lt;/a&gt;.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;So what’s the big deal? Well, general consumers have had a pretty annoying range of communication technologies to choose from. We have regular fast WiFi, of course, with its tens-of-metres range. 2.4 GHz goes a bit farther than 5 GHz but competition for channels is fierce and microwave ovens are a natural predator. Bluetooth 5 Low Energy has its wonderful Coded PHY which lets you trade off bandwidth to get up to 1 km range, but it’s really very slow and you’ll be lucky to find an API for it outside of Android. Then there’s &lt;a href=&#34;https://en.wikipedia.org/wiki/LoRa&#34;&gt;LoRa&lt;/a&gt;, which can work up to around 10 km, again with very low throughput. I’ll have more to say about LoRa shortly.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;We’ve always been missing the happy middle. Now HaLow is here and that’s exactly what it offers. What can we do with it?&#xA;&lt;/p&gt;&#xA;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Cover large properties with internet access. Or stick an AP on your roof/balcony and enjoy connectivity on your walk.&#xA;&lt;li&gt;Offsite backups.&#xA;&lt;li&gt;24/7 LAN party with your mate who lives a block away.&#xA;&lt;li&gt;Community WiFi projects (this is the one I’m eyeing off).&#xA;&lt;li&gt;Probably lots more!&#xA;&lt;/ul&gt;&#xA;&#xA;&lt;p&gt;&#xA;Of course, we’re not strictly limited to what we can buy off the shelf. There’s always amateur radio, the main downside of which is that &lt;a href=&#34;https://xkcd.com/191/&#34;&gt;you can only talk to other radio amateurs&lt;/a&gt;. For community/mesh networking this has some upsides—amateurs are generally well-behaved and the legal requirement to identify stations gives you out-of-band ways to deal with bad actors, which means you don’t have to solve every aspect of abuse handling in the protocol itself.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;One thing that’s particularly cool about the HT-HD01 is that it supports &lt;a href=&#34;https://en.wikipedia.org/wiki/IEEE_802.11s&#34;&gt;802.11s&lt;/a&gt;, a standard protocol for mesh networking. Here, stations can work together to form an encrypted mesh network. With the appropriate configuration your end devices can see a flat Ethernet segment, as if they were connected to the same switch, and the mesh routing is handled for you behind the scenes. That’s pretty neat.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;Still, not everybody is satisfied with 802.11s as a multihop router. One of the most popular mesh-routing protocol families is called &lt;a href=&#34;https://en.wikipedia.org/wiki/B.A.T.M.A.N.&#34;&gt;BATMAN&lt;/a&gt;, which is implemented in the Linux kernel as &lt;code&gt;batman-adv&lt;/code&gt;. Using something like &lt;a href=&#34;https://openmanet.github.io/docs/&#34;&gt;OpenMANET&lt;/a&gt; it happens that you can use 802.11s for all of its underlying connectivity and encryption but disable just the mesh forwarding, stacking &lt;code&gt;batman-adv&lt;/code&gt; on top instead to cover that aspect.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;I am curious to see how well these could work for a community WiFi deployment. It’s a bit of a harder challenge to get going. Even if the advertised 1–2 km range is realistic you need significant density to get a usable mesh. Within that usable mesh, though, suddenly so much is possible. Not just IRC: actual web pages, forums, email, images, PDFs, even VoIP or games if the latency is good enough. This is the kind of speed where it’s no longer just a poor backup to the “real internet”. If you rig it up in a sufficiently reliable and convenient manner it could be your &lt;em&gt;first&lt;/em&gt; choice for local communications, not just something you reach for when Discord stops working.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;And that’s my main gripe with &lt;a href=&#34;https://en.wikipedia.org/wiki/MeshCore&#34;&gt;MeshCore&lt;/a&gt;/&lt;a href=&#34;https://en.wikipedia.org/wiki/Meshtastic&#34;&gt;Meshtastic&lt;/a&gt;, which are based on LoRa. They work for a low-volume text-based group chat containing only short messages. On the popular MeshCore “Mid” preset a 100-byte packet &lt;a href=&#34;https://zindello.com.au/finding-the-sweet-spot-meshcore-lora-settings-in-the-australian-900mhz-band/&#34;&gt;takes 0.55 seconds of airtime&lt;/a&gt;. That’s fine for what it is but it doesn’t really supplant anything. Good for an emergency or bushwalking; an idle curiosity when life is proceeding normally.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;My other gripe with LoRa is that it’s a proprietary modulation scheme. The devices may be inexpensive but at the end of the day it’s using a patented and licensed technology. I would prefer to use an open standard where possible.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;So I am looking forward very much to playing with these devices and seeing what they can do. If you live in Hobart and feel interested in doing some HaLow meshing, drop me an email.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;hr&gt;&#xA;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;footnote-1&#34;&gt;I was moved to take a peek at their &lt;a href=&#34;https://www.morsemicro.com/careers/&#34;&gt;careers page&lt;/a&gt; but, alas, all of their technical roles are on-site up in Sydney. That’s not happening. &lt;a href=&#34;#footnote-1-ref&#34;&gt;↩︎&lt;/a&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&#xA;</content>
    <link href="https://octet-stream.net/b/scb/2026-07-22-hold-on-to-your-hats-halow-is-here.html" rel="alternate"></link>
    <author>
      <name>Thomas Karpiniec</name>
      <email>tom.karpiniec@outlook.com</email>
    </author>
  </entry>
  <entry>
    <title>Automatic JavaVM and Context detection using Rust on Android</title>
    <updated>2026-07-23T21:35:22+10:00</updated>
    <id>urn:uuid:9b092c22-3002-43fc-a551-d39b12c6de8a</id>
    <content type="html">&lt;p&gt;&#xA;When you’re running Rust code in the context of an Android app, if you want to work with Android-specific functionality over JNI you tend to need three things:&#xA;&lt;/p&gt;&#xA;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;A reference to the running Java virtual machine.&#xA;&lt;li&gt;A reference to a suitable &lt;code&gt;Context&lt;/code&gt;, often the &lt;code&gt;ApplicationContext&lt;/code&gt;.&#xA;&lt;li&gt;Some helper code on the Java side to provide APIs with required callbacks, and so on.&#xA;&lt;/ol&gt;&#xA;&#xA;&lt;p&gt;&#xA;My cross-platform crate &lt;a href=&#34;https://crates.io/crates/netwatcher&#34;&gt;netwatcher&lt;/a&gt; has to do all of these when it finds itself running on an Android. It handles items 1 and 2 in a very straightforward manner: it exposes &lt;a href=&#34;https://github.com/thombles/netwatcher/blob/6ee4b450671853392a258cff8639086cd4e3b925/src/android.rs#L18-L51&#34;&gt;a public function&lt;/a&gt; where you pass in both of them. It’s up to the developer to do the appropriate plumbing to call through from Java or Kotlin, passing along their &lt;code&gt;Context&lt;/code&gt; object. For item 3, I was happy to discover (that is, copy from someone else) a fiddly-but-dependable technique involving injecting DEX bytecode at runtime, &lt;a href=&#34;/b/scb/2025-08-03-injecting-java-from-native-libraries-on-android.html&#34;&gt;which I wrote about previously&lt;/a&gt;.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;As a crate developer, the holy grail is to get all of these runtime dependencies automatically without the end user having to do anything other than put the crate in &lt;code&gt;Cargo.toml&lt;/code&gt; and use the API normally. They will be sad if their code suddenly breaks weirdly when they cross-compile it to Android. The difficulty level is increased by being a mere dependency within a larger Rust binary—if we’re just one piece of the final &lt;code&gt;.so&lt;/code&gt; then we can’t/shouldn’t hook into &lt;code&gt;JNI_OnLoad&lt;/code&gt; (without coordination through the final library), which is otherwise a reliable way to get the &lt;code&gt;JavaVM*&lt;/code&gt; pointer.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;The other day I was following some links between issues on GitHub and I noticed that the Matrix Rust SDK has apparently solved items 1 and 2, making them entirely automatic (&lt;a href=&#34;https://github.com/matrix-org/matrix-rust-sdk/pull/6306&#34;&gt;PR #1&lt;/a&gt;, &lt;a href=&#34;https://github.com/matrix-org/matrix-rust-sdk/pull/6370&#34;&gt;PR #2&lt;/a&gt;). I was intrigued and went on a deep dive to see how this worked. What I found is… well it’s pretty out there. For now I’ve decided to leave netwatcher the way it is, even if it’s more annoying for the developer.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;The hacks involved are pretty impressive so here I want to dissect how they work and briefly discuss the risks. To be clear, I’m not trying to put anyone on blast for writing or using these techniques. It’s a noble cause and if you get it wrong it’s probably just going to crash.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;Let’s begin with item 2 since it’s more straightforward. There is a class in Android called &lt;code&gt;ActivityThread&lt;/code&gt; which you won’t find in the documentation. It has a static method &lt;code&gt;currentActivityThread()&lt;/code&gt;, and when you get the instance back you can call &lt;code&gt;getApplication()&lt;/code&gt;. This returns the &lt;code&gt;Application&lt;/code&gt; that you can use as a &lt;code&gt;Context&lt;/code&gt;. JNI can give you a reference to the class and help you call the methods. Too easy.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;&lt;a href=&#34;https://github.com/matrix-org/matrix-rust-sdk/blob/c49ad735c21f81573adca020ad0d8e302bd657f8/bindings/matrix-sdk-ffi/src/platform/android_platform.rs#L64-L77&#34;&gt;From the Matrix Rust PR&lt;/a&gt;:&#xA;&lt;/p&gt;&#xA;&#xA;&lt;pre style=&#34;background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#8f5902;font-style:italic&#34;&gt;// Get the current activity thread&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#8f5902;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#204a87;font-weight:bold&#34;&gt;let&lt;/span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#000&#34;&gt;activity_thread&lt;/span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#ce5c00;font-weight:bold&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#000&#34;&gt;env&lt;/span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt;    &lt;/span&gt;&lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#000&#34;&gt;call_static_method&lt;/span&gt;&lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt;        &lt;/span&gt;&lt;span style=&#34;color:#4e9a06&#34;&gt;&amp;#34;android/app/ActivityThread&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;,&lt;/span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt;        &lt;/span&gt;&lt;span style=&#34;color:#4e9a06&#34;&gt;&amp;#34;currentActivityThread&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;,&lt;/span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt;        &lt;/span&gt;&lt;span style=&#34;color:#4e9a06&#34;&gt;&amp;#34;()Landroid/app/ActivityThread;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;,&lt;/span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt;        &lt;/span&gt;&lt;span style=&#34;color:#ce5c00;font-weight:bold&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;[],&lt;/span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt;    &lt;/span&gt;&lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;)&lt;/span&gt;&lt;span style=&#34;color:#ce5c00;font-weight:bold&#34;&gt;?&lt;/span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt;    &lt;/span&gt;&lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#000&#34;&gt;l&lt;/span&gt;&lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;()&lt;/span&gt;&lt;span style=&#34;color:#ce5c00;font-weight:bold&#34;&gt;?&lt;/span&gt;&lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;;&lt;/span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#8f5902;font-style:italic&#34;&gt;// Then get the application context&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#8f5902;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#204a87;font-weight:bold&#34;&gt;let&lt;/span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#000&#34;&gt;context&lt;/span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#ce5c00;font-weight:bold&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#000&#34;&gt;env&lt;/span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt;    &lt;/span&gt;&lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#000&#34;&gt;call_method&lt;/span&gt;&lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#000&#34;&gt;activity_thread&lt;/span&gt;&lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;,&lt;/span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#4e9a06&#34;&gt;&amp;#34;getApplication&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;,&lt;/span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#4e9a06&#34;&gt;&amp;#34;()Landroid/app/Application;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;,&lt;/span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#ce5c00;font-weight:bold&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;[])&lt;/span&gt;&lt;span style=&#34;color:#ce5c00;font-weight:bold&#34;&gt;?&lt;/span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt;    &lt;/span&gt;&lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#000&#34;&gt;l&lt;/span&gt;&lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;()&lt;/span&gt;&lt;span style=&#34;color:#ce5c00;font-weight:bold&#34;&gt;?&lt;/span&gt;&lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;;&lt;/span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&#xA;&#xA;&lt;p&gt;&#xA;The problem is that &lt;code&gt;ActivityThread&lt;/code&gt; and its methods aren’t officially part of the Android SDK and there are &lt;a href=&#34;https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces&#34;&gt;policies around using non-SDK interfaces&lt;/a&gt;. When I look up &lt;code&gt;currentActivityThread()&lt;/code&gt; and &lt;code&gt;getApplication()&lt;/code&gt; in their spreadsheet they are both marked &lt;code&gt;unsupported&lt;/code&gt;. This means Android turns a blind eye: the call will be permitted, but they’re allowed to break how this works at any time for any reason.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;Going by &lt;a href=&#34;https://github.com/search?q=Landroid%2Fapp%2FActivityThread&amp;amp;type=code&#34;&gt;a quick GitHub search&lt;/a&gt; this technique gets a fair amount of use but it still gives me pause. I don’t want to suddenly have to rearchitect my library because a new version of Android changes how &lt;code&gt;ActivityThread&lt;/code&gt; works, or handle bug reports from people who are running strange OEM Android distributions where up is down and down is up. For now I intend to stick with the supported API.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;For item 1 we must turn to the crate &lt;a href=&#34;https://crates.io/crates/jvm-getter&#34;&gt;jvm-getter&lt;/a&gt;, which is where Matrix has ended up. This crate helpfully begins its README with a warning icon and suggests alternatives.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;The game here is to locate and call a function called &lt;code&gt;JNI_GetCreatedJavaVMs&lt;/code&gt;. This is &lt;a href=&#34;https://docs.oracle.com/en/java/javase/11/docs/specs/jni/invocation.html#jni_getcreatedjavavms&#34;&gt;an official JNI invocation function&lt;/a&gt; that gives you a list of &lt;code&gt;JavaVM*&lt;/code&gt; pointers. In an Android app you would expect there to be just one VM, so you can pass it a 1-length array and now you have the pointer you need. This function is relatively unusual in JNI because it’s free-standing, an exported symbol you can look up. For most JNI work you would instead start with a &lt;code&gt;JavaVM*&lt;/code&gt; or a &lt;code&gt;JNIEnv*&lt;/code&gt; and use its function table.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;This function has always been exported. What has changed over time is whether it can be discovered by application NDK code. Up to API level 23, you could use &lt;code&gt;dlopen&lt;/code&gt; and &lt;code&gt;dlsym&lt;/code&gt; to look it up by name and then call it. In API levels 24–30 linker namespaces were introduced and &lt;code&gt;JNI_GetCreatedJavaVMs&lt;/code&gt; was no longer accessible, so &lt;code&gt;dlsym&lt;/code&gt; fails. Then from API level 31 onward, Android made &lt;code&gt;libnativehelper.so&lt;/code&gt; a public NDK library and this JNI function became available to &lt;code&gt;dlsym&lt;/code&gt; officially.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;This quirk bit Matrix: originally they used libloading (a cross-platform wrapper around the &lt;code&gt;dlopen&lt;/code&gt;/&lt;code&gt;dlsym&lt;/code&gt; technique) to look up the symbol and call it (PR #1). A &lt;a href=&#34;https://github.com/matrix-org/matrix-rust-sdk/issues/6368&#34;&gt;bug report came in&lt;/a&gt; that it crashes on Android 11 and they switched to using the &lt;code&gt;jvm-getter&lt;/code&gt; crate, which offered a workaround when &lt;code&gt;dlsym&lt;/code&gt; fails.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;So, that workaround. Buckle up, kids. &lt;a href=&#34;https://github.com/gobley/jvm-getter/blob/main/jvm-getter/src/android.rs&#34;&gt;What it does&lt;/a&gt; is this:&#xA;&lt;/p&gt;&#xA;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Use a system property to learn the name of the Android runtime library, typically &lt;code&gt;libart.so&lt;/code&gt;.&#xA;&lt;li&gt;Use &lt;a href=&#34;https://www.man7.org/linux/man-pages/man3/dl_iterate_phdr.3.html&#34;&gt;&lt;code&gt;dl_iterate_phdr&lt;/code&gt;&lt;/a&gt; to walk all the shared objects that have been loaded into the current process. For each one, this invokes a callback.&#xA;&lt;li&gt;In the callback, look for the one corresponding to &lt;code&gt;libart.so&lt;/code&gt;. When you see it, record the base address at which it’s been loaded into memory, and the full path on disk to the ELF file.&#xA;&lt;li&gt;Read the contents of the ELF file into memory.&#xA;&lt;li&gt;Use the &lt;a href=&#34;https://crates.io/crates/goblin&#34;&gt;&lt;code&gt;goblin&lt;/code&gt;&lt;/a&gt; crate to parse the ELF file, find the symbol table, and locate the symbol whose name matches &lt;code&gt;JNI_GetCreatedJavaVMs&lt;/code&gt;.&#xA;&lt;li&gt;Take that symbol’s relative address.&#xA;&lt;li&gt;Add the symbol’s relative address to the base address of &lt;code&gt;libart.so&lt;/code&gt; which was recorded earlier.&#xA;&lt;li&gt;Cast this to a function pointer and return it to the caller, which then calls it to find the JavaVM.&#xA;&lt;/ol&gt;&#xA;&#xA;&lt;p&gt;&#xA;Now &lt;em&gt;that’s&lt;/em&gt; persistence. Notice that the linker namespaces aren’t an isolation boundary. If you can find the address, you can still call it.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;While this is plainly nuts, on balance it doesn’t seem too bad. There’s nothing wrong with locating &lt;code&gt;JNI_GetCreatedJavaVMs&lt;/code&gt; with &lt;code&gt;dlopen&lt;/code&gt; and &lt;code&gt;dlsym&lt;/code&gt;—Android explicitly makes it available to find on most modern versions and this will be a reliable method for item 1 going forward. The tricky part is the gap in API levels where the symbol isn’t readily available, if you don’t want to drop support for older devices yet.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;The thing about the ELF-parsing workaround is that all the relevant Android versions are &lt;em&gt;in the past&lt;/em&gt;. They’re old phones or tablets that aren’t getting updates. &lt;code&gt;libart.so&lt;/code&gt; isn’t going to wake up one day with stripped symbols&lt;a id=&#34;footnote-1-ref&#34; href=&#34;#footnote-1&#34;&gt;[1]&lt;/a&gt;. If this workaround turns out to work well across a range of device types then it’s probably going to &lt;em&gt;keep&lt;/em&gt; working.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;Unfortunately, solving item 1 alone isn’t enough to simplify my public API. If I’m not prepared to use an unsupported method to get the &lt;code&gt;Context&lt;/code&gt; then I need a configuration method anyway, and it’s just as easy to grab the &lt;code&gt;JavaVM*&lt;/code&gt; at the same time.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;We just need a slightly better way to get an application context from the NDK and everything will be perfect…&#xA;&lt;/p&gt;&#xA;&#xA;&lt;hr&gt;&#xA;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;footnote-1&#34;&gt;I suspect &lt;code&gt;jvm-getter&lt;/code&gt; could be made more resilient here by searching &lt;a href=&#34;https://docs.rs/goblin/0.10.7/goblin/elf/struct.Elf.html#structfield.dynsyms&#34;&gt;&lt;code&gt;dynsyms&lt;/code&gt;&lt;/a&gt; instead of &lt;code&gt;syms&lt;/code&gt;. Since the &lt;code&gt;JNI_GetCreatedJavaVMs&lt;/code&gt; implementation has to be exported, it must be named and cannot be stripped from this particular symbol table. &lt;a href=&#34;#footnote-1-ref&#34;&gt;↩︎&lt;/a&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&#xA;</content>
    <link href="https://octet-stream.net/b/scb/2026-07-23-automatic-javavm-and-context-detection-using-rust-on-android.html" rel="alternate"></link>
    <author>
      <name>Thomas Karpiniec</name>
      <email>tom.karpiniec@outlook.com</email>
    </author>
  </entry>
  <entry>
    <title>Using local DeepSeek v4 Flash in Xcode</title>
    <updated>2026-08-05T15:07:20+10:00</updated>
    <id>urn:uuid:e1e2225e-3ff3-46f0-a898-1d48d1d33eb7</id>
    <content type="html">&lt;p&gt;&#xA;I’m rather pleased. I’ve been able to bolt together enough pieces of software that I can now run DeepSeek v4 Flash with native MXFP4 routed-expert weights on my MacBook Pro, not only via a CLI harness like OpenCode, but also through Xcode’s native “intelligence” integration.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;Before you get too excited please realise that I’m running this on a ludicrously expensive M5 Max with 128 GB RAM. &lt;em&gt;However&lt;/em&gt;, I am hoping that the upcoming Qwen3.8-27B release will be awesome and it will be practical to have a similar workflow with a considerably more modest unified RAM budget.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;This is the stack I’ve ended up with:&#xA;&lt;/p&gt;&#xA;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Xcode Intelligence configured with a custom ACP agent&#xA;&lt;li&gt;OpenCode running as &lt;code&gt;opencode acp&lt;/code&gt;&#xA;&lt;li&gt;antirez’s &lt;a href=&#34;https://github.com/antirez/ds4&#34;&gt;ds4-server&lt;/a&gt;&#xA;&lt;li&gt;DeepSeek v4 Flash: the new 0731 update&lt;a id=&#34;footnote-1-ref&#34; href=&#34;#footnote-1&#34;&gt;[1]&lt;/a&gt;&#xA;&lt;/ul&gt;&#xA;&#xA;&lt;p&gt;&#xA;To prove it works I typed &lt;a href=&#34;https://gist.github.com/thombles/32d21ec7c3ac1773a89234ed8cee28c7&#34;&gt;a short prompt&lt;/a&gt; into Xcode—I wanted a Mac SwiftUI application to help me practise cardinal numbers in Polish. It whirred away autonomously for about 90 minutes, filling the context up to 95,200 tokens, and produced an app that more or less did what I asked. I spent an extra half an hour tinkering interactively through the chat interface to fix a few specification issues&lt;a id=&#34;footnote-2-ref&#34; href=&#34;#footnote-2&#34;&gt;[2]&lt;/a&gt; and a couple of layout bugs&lt;a id=&#34;footnote-3-ref&#34; href=&#34;#footnote-3&#34;&gt;[3]&lt;/a&gt;. The session started at around 17 output tokens/sec and dropped to about 15.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;a href=&#34;https://octet-stream.net/assets/serious/ds4fxcode/app-1-menu.png&#34;&gt;&lt;img src=&#34;https://octet-stream.net/assets/serious/ds4fxcode/app-1-menu.png&#34;&gt;&lt;/a&gt;&#xA;&#xA;&lt;a href=&#34;https://octet-stream.net/assets/serious/ds4fxcode/app-2-questions.png&#34;&gt;&lt;img src=&#34;https://octet-stream.net/assets/serious/ds4fxcode/app-2-questions.png&#34;&gt;&lt;/a&gt;&#xA;&#xA;&lt;a href=&#34;https://octet-stream.net/assets/serious/ds4fxcode/app-3-feedback.png&#34;&gt;&lt;img src=&#34;https://octet-stream.net/assets/serious/ds4fxcode/app-3-feedback.png&#34;&gt;&lt;/a&gt;&#xA;&#xA;&lt;p&gt;&#xA;For a fun twist, I made the app itself &lt;em&gt;also&lt;/em&gt; use the ds4 chat completions endpoint to provide some dynamic feedback on what the user typed, particularly to explain any mistakes. (As you can see, the “system prompt” needs improving.) DS4F happens to be weirdly good at handling broken Polish and explaining grammar. I’ll have more to say about that in a future post. I hope it got it right in the code. Otherwise I’m going to be practising the wrong thing.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;I have no strong opinions about the Swift code. From a quick skim it all seems pretty sensible and it made proper use of &lt;code&gt;JSONSerialization&lt;/code&gt; and &lt;code&gt;URLSession&lt;/code&gt; for making the HTTP request to the completions endpoint.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;Assuming you’d like to try something like this yourself&lt;a id=&#34;footnote-4-ref&#34; href=&#34;#footnote-4&#34;&gt;[4]&lt;/a&gt;, here are some notes and observations.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;Let’s start at the bottom of the stack. Since I began my experiments antirez has now merged all the relevant work so you should be able to just clone the ds4 repo, run &lt;code&gt;make -j&lt;/code&gt; and run &lt;code&gt;ds4-server&lt;/code&gt;. I’m executing it like this:&#xA;&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&#xA;&#34;$HOME/ds4/ds4-server&#34; \&#xA;    --ctx 393216 \&#xA;    --kv-disk-dir &#34;$HOME/.ds4/server-kv&#34; \&#xA;    --kv-disk-space-mb 100000 \&#xA;    --host 0.0.0.0 \&#xA;    --ssd-streaming \&#xA;    -m &#34;$HOME/ds4/gguf/DeepSeek-V4-Flash-MXFP4Experts-F16HC-F16Compressor-F16Indexer-Q8Attn-Q8Shared-Q8Out-chat-v2-mxfp4-0731.gguf&#34;&#xA;&lt;/pre&gt;&#xA;&#xA;&lt;p&gt;&#xA;Most early benchmarks indicate that DS4F works best with “maximum” thinking. DeepSeek recommends only using max thinking with a 384K context size, which is enforced in &lt;code&gt;ds4-server&lt;/code&gt;. Unless you choose a context of at least this size you’ll automatically get downgraded to high thinking. This comes with a RAM cost but it’s mild next to the weights themselves.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;You’ll want to enable SSD streaming unless you have a Mac with a stupendous amount of RAM. Note that so long as you’re not going into heavy swapping this will cause large amounts of disk &lt;em&gt;reads&lt;/em&gt; rather than writes so it shouldn’t contribute unduly to SSD wear. To give you an idea, across this coding session &lt;code&gt;ds4-server&lt;/code&gt; has read about 22 TB from disk.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;Since I have free disk space and generating the tokens is relatively expensive I choose to have a fairly large on-disk KV checkpoint cache. 100 GB is probably overkill for most purposes.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;If you’re only using it locally you probably won’t want to bind to &lt;code&gt;0.0.0.0&lt;/code&gt; but I sometimes access it over Tailscale so an endpoint available over LAN often suits me.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;So you run that and now you have a standard OpenAI-like chat completions endpoint running at &lt;code&gt;http://127.0.0.1:8000/v1&lt;/code&gt;.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;Next we need &lt;a href=&#34;https://opencode.ai&#34;&gt;OpenCode&lt;/a&gt;, which I have installed through Homebrew. It includes an implementation of &lt;a href=&#34;https://agentclientprotocol.com/get-started/introduction&#34;&gt;Agent Client Protocol&lt;/a&gt;, which is what Xcode needs.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;Now, in my OpenCode config my model is defined like this:&#xA;&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&#xA;      &#34;models&#34;: {&#xA;        &#34;deepseek-v4-flash&#34;: {&#xA;          &#34;name&#34;: &#34;DeepSeek V4 Flash (ds4.c local)&#34;,&#xA;          &#34;reasoning&#34;: true,&#xA;          &#34;limit&#34;: {&#xA;            &#34;context&#34;: 393216,&#xA;            &#34;input&#34;: 393216,&#xA;            &#34;output&#34;: 384000&#xA;          },&#xA;          &#34;options&#34;: {&#xA;            &#34;reasoningEffort&#34;: &#34;max&#34;&#xA;          },&#xA;          &#34;variants&#34;: {&#xA;            &#34;low&#34;: {&#xA;              &#34;disabled&#34;: true&#xA;            },&#xA;            &#34;medium&#34;: {&#xA;              &#34;disabled&#34;: true&#xA;            },&#xA;            &#34;high&#34;: {&#xA;              &#34;disabled&#34;: true&#xA;            },&#xA;            &#34;max&#34;: {&#xA;              &#34;reasoningEffort&#34;: &#34;max&#34;&#xA;            }&#xA;          }&#xA;        }&#xA;      }&#xA;&lt;/pre&gt;&#xA;&#xA;&lt;p&gt;&#xA;And at the top level I have:&#xA;&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&#xA;  &#34;compaction&#34;: {&#xA;    &#34;reserved&#34;: 65536&#xA;  },&#xA;&lt;/pre&gt;&#xA;&#xA;&lt;p&gt;&#xA;This is doing a few things. It forces DS4 to always use max effort even if ACP might try to default to a weaker level. It tells OpenCode the context size available. It allows large responses, which is important when using max thinking because the default limit is 32K and this model will quite happily think for more than that at a time. The reservation gives more breathing space for compaction. Having a large output limit can mess up OpenCode’s calculation of the compaction point, so it will constantly be compacting at a small context size. I fixed that by setting a large input limit too.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;Note that you also need an environment variable &lt;code&gt;OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX=384000&lt;/code&gt; in Xcode&#39;s settings, and in your shell if you use OpenCode there.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;While you need to have &lt;code&gt;ds4-server&lt;/code&gt; running yourself, Xcode will launch OpenCode on its own when you start a conversation. You need to configure it through Xcode’s Settings in the Intelligence section. Add &lt;code&gt;opencode&lt;/code&gt; as the path, and scroll down to set &lt;code&gt;acp&lt;/code&gt; as the argument and the above environment variable.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;a href=&#34;https://octet-stream.net/assets/serious/ds4fxcode/acp-1.png&#34;&gt;&lt;img src=&#34;https://octet-stream.net/assets/serious/ds4fxcode/acp-1.png&#34; style=&#34;width:50%&#34;&gt;&lt;/a&gt;&#xA;&#xA;&lt;a href=&#34;https://octet-stream.net/assets/serious/ds4fxcode/acp-2.png&#34;&gt;&lt;img src=&#34;https://octet-stream.net/assets/serious/ds4fxcode/acp-2.png&#34; style=&#34;width:50%&#34;&gt;&lt;/a&gt;&#xA;&#xA;&lt;p&gt;&#xA;Like me, you may be confused by how you actually talk to it, since Xcode defaults to Codex. Once you click the Coding Assistant button in the top left, you then need to click on the little “New message” icon to get a dropdown where you can choose the agent you just added. When you type in a message and hit enter it will start OpenCode in the background. After 20–30 seconds of I’m-not-sure-what-exactly, requests start hitting DS4.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;a href=&#34;https://octet-stream.net/assets/serious/ds4fxcode/new-menu.png&#34;&gt;&lt;img src=&#34;https://octet-stream.net/assets/serious/ds4fxcode/new-menu.png&#34; style=&#34;width:30%&#34;&gt;&lt;/a&gt;&#xA;&#xA;&lt;p&gt;&#xA;For some reason my Xcode uses a lot of CPU while DS4 is thinking. Maybe it’s constantly re-rendering the thinking box as reasoning streams in? If I swap away from the coding assistant back to the project view then it settles down. It’s clear that the Xcode UI designers were not anticipating DS4 quantities of thinking—when I click on a large one it can take a full couple of seconds for the scrollable popup to appear.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;If you get all that right, you should be off to the races. I’m looking forward to trying this out with the new Qwen model too. It’s unclear to me whether Xcode will make it easy to select multiple open models through OpenCode itself or whether I’ll need to set up wrapper scripts and create two ACP entries if I want to toggle between them.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;hr&gt;&#xA;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;footnote-1&#34;&gt;Apparently so called because it was released on July 31. &lt;a href=&#34;#footnote-1-ref&#34;&gt;↩︎&lt;/a&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;footnote-2&#34;&gt;Unsurprisingly in hindsight, it turns out that if you select random numbers between 1 and 999,999, most of the time you’re writing out 6-digit numbers like 556,195. This feels pretty laborious. Most of the grammatical trickiness a student of Polish needs to practise can be reproduced with smaller numbers. &lt;a href=&#34;#footnote-2-ref&#34;&gt;↩︎&lt;/a&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;footnote-3&#34;&gt;DeepSeek v4 Flash only has text inputs so I can’t just feed it a screenshot. It would be an interesting follow-up to see if I can get it to use a textual description of where the components are laid out in the actual window and use that as feedback to determine whether it wrote the SwiftUI it intended. &lt;a href=&#34;#footnote-3-ref&#34;&gt;↩︎&lt;/a&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;footnote-4&#34;&gt;Be aware that DeepSeek v4 Flash cloud inference is extremely cheap. That’s a much more cost-effective way to get tokens than buying hardware with lots of RAM. However, there will always be some people who are interested in local inference, for various reasons. You know who you are. &lt;a href=&#34;#footnote-4-ref&#34;&gt;↩︎&lt;/a&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&#xA;</content>
    <link href="https://octet-stream.net/b/scb/2026-08-05-using-local-deepseek-v4-flash-in-xcode.html" rel="alternate"></link>
    <author>
      <name>Thomas Karpiniec</name>
      <email>tom.karpiniec@outlook.com</email>
    </author>
  </entry>
  <entry>
    <title>WiFi HaLow on Phones</title>
    <updated>2026-08-09T21:37:02+10:00</updated>
    <id>urn:uuid:60aafe1b-8e5b-48e0-a2cb-fa464fb6d0d9</id>
    <content type="html">&lt;p&gt;&#xA;In &lt;a href=&#34;/b/scb/2026-07-22-hold-on-to-your-hats-halow-is-here.html&#34;&gt;a recent post&lt;/a&gt; I was getting excited about HaLow and I mentioned that I had some &lt;a href=&#34;https://heltec.org/project/ht-hd01/&#34;&gt;HT-HD01&lt;/a&gt; units on order from Heltec. Those have now arrived and my excitement continues. These units are very cool for three main reasons.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;&lt;b&gt;It’s affordable.&lt;/b&gt; They cost 69 USD each. Nice.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;&lt;b&gt;Versatile deployment modes.&lt;/b&gt; Out of the box you can build a standard AP/client scenario. You can also use &lt;a href=&#34;https://en.wikipedia.org/wiki/IEEE_802.11s&#34;&gt;802.11s meshing&lt;/a&gt;, which I haven’t tested yet. You have the choice between NAT and bridging, so you can make it so that all the connected devices think that they’re on the same ethernet segment. You can configure it both for providing internet access and for using it as a secondary network that sits alongside your other WiFi and cellular.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;&lt;b&gt;Versatile local-device connectivity, especially ethernet over USB.&lt;/b&gt; Interestingly, these dongles do not even attempt to make themselves known to the OS as a WiFi HaLow adapter. The dongle itself acts as the host on the HaLow network and then your device is bridged over regular WiFi/Ethernet. You have three choices: connect to the dongle’s local 2.4 GHz WiFi AP; connect to the dongle’s RJ45 ethernet port; or use ethernet-over-USB via the dongle’s USB-C port.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;a href=&#34;https://octet-stream.net/assets/serious/halow/iphone_with_radio.jpg&#34;&gt;&lt;img src=&#34;https://octet-stream.net/assets/serious/halow/iphone_with_radio.jpg&#34;&gt;&lt;/a&gt;&#xA;&#xA;&lt;a href=&#34;https://octet-stream.net/assets/serious/halow/ethernet_in_menu.jpeg&#34;&gt;&lt;img src=&#34;https://octet-stream.net/assets/serious/halow/ethernet_in_menu.jpeg&#34; style=&#34;width:50%&#34;&gt;&lt;/a&gt;&#xA;&#xA;&lt;p&gt;&#xA;Let’s focus on ethernet-over-USB for a moment because this is super interesting. I’ve tested this on a MacBook, an iPhone 17, and an Android Pixel 7. The Apple devices both did two things: they powered up the radio, and they added it as a wired ethernet device with zero configuration on the phone. This shows up as “Ethernet” in the screenshot above. This is significant so I’m going to spell it out in detail. If you would like your iPhone to communicate over a kilometre or two, you can plug in a device to its regular old USB-C port, and it will work. It does not need any batteries or charging of its own because it can get power from the iPhone. It does not require any pairing or user interaction. It shows up as an &lt;em&gt;additional&lt;/em&gt; IP interface, which means that it does not displace any existing connectivity you have via WiFi, Bluetooth or cellular. Because it is a normal IP interface, it &lt;em&gt;Just Works&lt;/em&gt; in every app that does LAN networking.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;For anyone with an iPhone this is kind of amazing. Unfortunately, it did not work on the Android. For whatever reason my Pixel doesn’t come with an appropriate driver to pick up the Ethernet connectivity. It did, however, appear to power up the radio.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;I mentioned before that these devices kind of default to being your primary network, where there exists some upstream gateway that is going to grant you both an IP address and internet access. This is not required. For my tests so far I haven’t gone full mesh and I’ve stuck with the simple “AP” and “STA” roles, where one radio is an AP, and the other connects as a client. With some config tweaks this coexists perfectly well with existing WiFi. I’m more interested in having HaLow as a secondary layer where you’re going to be using HaLow for a strictly P2P role with greater range.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;Out of the box, one of the Heltec boxes acting as an AP will advertise itself as the default route. This is a problem if it is not actually connected to an upstream network with internet access, and you would prefer that clients used their existing WiFi or cellular connectivity to access the internet. Ultimately you can do whatever you need—under the hood it’s a MediaTek MT7628 paired with a &lt;a href=&#34;https://www.morsemicro.com/chips/&#34;&gt;Morse Micro MM6108&lt;/a&gt; HaLow radio, running OpenWrt which is eminently hackable. You even have the option of flashing your own firmware if you want. The good news is that Heltec’s supplied user interface is sufficient to configure the DHCP server so that it will allocate IPs but not try to route unrelated traffic.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;Navigate to &lt;b&gt;Network &amp;gt; Interfaces&lt;/b&gt; and choose &lt;b&gt;Edit&lt;/b&gt; on &lt;code&gt;ahwlan&lt;/code&gt;.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;a href=&#34;https://octet-stream.net/assets/serious/halow/main_menu.png&#34;&gt;&lt;img src=&#34;https://octet-stream.net/assets/serious/halow/main_menu.png&#34; style=&#34;width:50%&#34;&gt;&lt;/a&gt;&#xA;&#xA;&lt;a href=&#34;https://octet-stream.net/assets/serious/halow/interfaces.png&#34;&gt;&lt;img src=&#34;https://octet-stream.net/assets/serious/halow/interfaces.png&#34; style=&#34;width:100%&#34;&gt;&lt;/a&gt;&#xA;&#xA;&lt;p&gt;&#xA;Then go to &lt;b&gt;DHCP Server &amp;gt; Advanced Settings&lt;/b&gt; and add a couple of extra DHCP options. The idea here is that a bare &lt;code&gt;3&lt;/code&gt; will prevent it from advertising a default route, and a bare &lt;code&gt;6&lt;/code&gt; will stop it advertising itself as a DNS server. This way you are free to talk to other devices across the HaLow LAN &lt;em&gt;in addition&lt;/em&gt; to the networks you already had.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;a href=&#34;https://octet-stream.net/assets/serious/halow/advanced_settings.png&#34;&gt;&lt;img src=&#34;https://octet-stream.net/assets/serious/halow/advanced_settings.png&#34; style=&#34;width:100%&#34;&gt;&lt;/a&gt;&#xA;&#xA;&lt;p&gt;&#xA;The other important thing to know is that these devices do not support wired ethernet and ethernet-over-USB simultaneously. You can toggle between these modes by briefly pressing the little button using the supplied metal widget. Everything is explained by the &lt;a href=&#34;https://wiki.heltec.org/docs/devices/wifi-halow/ht-hd01/#led-indicator-description&#34;&gt;colour of the LED&lt;/a&gt;. For an AP, cyan means wired ethernet and purple means USB. For a station, green means wired ethernet and blue means USB.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;The cool thing is that once you get this config locked in, it’s locked in. You can plug it in to a Mac or an iPhone and DHCP does the rest. You need only wait a minute or two for it to boot up.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;I intend to put these radios through their paces—expect more posts about them!&#xA;&lt;/p&gt;&#xA;&#xA;</content>
    <link href="https://octet-stream.net/b/scb/2026-08-09-wifi-halow-on-phones.html" rel="alternate"></link>
    <author>
      <name>Thomas Karpiniec</name>
      <email>tom.karpiniec@outlook.com</email>
    </author>
  </entry>
  <entry>
    <title>netwatcher v0.8.0 - the AI bugfix release</title>
    <updated>2026-08-17T17:43:38+10:00</updated>
    <id>urn:uuid:491a2b45-8dbe-478d-b1c8-97baba546d60</id>
    <content type="html">&lt;p&gt;&#xA;My little Rust library &lt;a href=&#34;https://crates.io/crates/netwatcher&#34;&gt;netwatcher&lt;/a&gt; has a new release and &lt;a href=&#34;https://github.com/thombles/netwatcher/releases/tag/v0.8.0&#34;&gt;the changelog is unusually long&lt;/a&gt;. As you’ve probably already guessed from the title, this is because I took the time to run some LLMs over it looking for bugs and doc inconsistencies. It turns out that even within Rust’s guardrails I wrote plenty of bugs. Who could have seen that coming?&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;This won’t be a surprise to anyone who’s been using LLMs in 2026. There are no special tricks and you don’t have to overthink it&lt;a id=&#34;footnote-1-ref&#34; href=&#34;#footnote-1&#34;&gt;[1]&lt;/a&gt;. We really are at the point where you say to any modern model “find the bugs” and it finds the bugs. It’ll even fix them too. Isn’t that magical? I may be slinging slop but darned if my library isn’t substantially better for it.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;The only findings I had to reject were cases where I’m actually being sloppy on purpose. For example, some platforms have edge cases where a network’s hardware address might not be readable. I choose to leave it as a &lt;code&gt;String&lt;/code&gt; with possible placeholder, since it kind of sucks to make it an &lt;code&gt;Option&lt;/code&gt; for the normal case. Similarly, &lt;em&gt;in principle&lt;/em&gt; it’s possible for an interface watcher to start failing in the middle. I choose to swallow the errors. Virtually everything was validated when the watch was first created, and transient errors will be handled gracefully by incorporating changes into the next update. Because I do this, you don’t need to check for an error every time and the code is lovely and clean. LLMs don’t enjoy this kind of corner-cutting (nor do certain humans) but I put my foot down. If there’s evidence that this causes problems in the real world then I’ll reconsider.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;The majority of the bugs were the sort of things where I think “oh yeah I could see that being a problem” but it wasn’t particularly likely to come up for most users most of the time: race conditions, handling panics over FFI, unusual interface types, things like that. Pre-LLM, these are the sorts of things that would come up as intermittent faults on the bug tracker and I’d have to spend hours poring over the code and vendor docs to figure out what went wrong. I feel like I’ve done a kind of speed-run on that hardening and now it’s ready for more users&lt;a id=&#34;footnote-2-ref&#34; href=&#34;#footnote-2&#34;&gt;[2]&lt;/a&gt;.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;That said, the most significant bugfix in this release was &lt;em&gt;not&lt;/em&gt; detected by LLM inspection, and I’m a little surprised that it wasn’t. It fell out by accident when I added IPv6-specific integration tests and I realised that if I changed an IPv6 address on Linux it wasn’t picked up by the library at all.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;The history of this bug is mildly amusing. On Linux the standard way to subscribe to IP address changes is to open a &lt;code&gt;NETLINK_ROUTE&lt;/code&gt; socket and use group bits to indicate what event types you care about. I needed to specify the constants &lt;code&gt;RTMGRP_IPV4_IFADDR&lt;/code&gt; and &lt;code&gt;RTMGRP_IPV6_IFADDR&lt;/code&gt;, which I took from the &lt;code&gt;nix&lt;/code&gt; crate. At this point, everything was correct.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;Later I was trying to make this work on Android (which is a kind of Linux) and the build failed because those constants were not present in the Android version of the &lt;code&gt;nix&lt;/code&gt; crate. To make the compiler happy I switched those constants to local definitions:&#xA;&lt;/p&gt;&#xA;&#xA;&lt;pre style=&#34;background-color:#f8f8f8;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#204a87;font-weight:bold&#34;&gt;const&lt;/span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#000&#34;&gt;RTMGRP_IPV4_IFADDR&lt;/span&gt;: &lt;span style=&#34;color:#204a87;font-weight:bold&#34;&gt;u32&lt;/span&gt; &lt;span style=&#34;color:#ce5c00;font-weight:bold&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#0000cf;font-weight:bold&#34;&gt;0x10&lt;/span&gt;&lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;;&lt;/span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#204a87;font-weight:bold&#34;&gt;const&lt;/span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#000&#34;&gt;RTMGRP_IPV6_IFADDR&lt;/span&gt;: &lt;span style=&#34;color:#204a87;font-weight:bold&#34;&gt;u32&lt;/span&gt; &lt;span style=&#34;color:#ce5c00;font-weight:bold&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#0000cf;font-weight:bold&#34;&gt;0x20&lt;/span&gt;&lt;span style=&#34;color:#000;font-weight:bold&#34;&gt;;&lt;/span&gt;&lt;span style=&#34;color:#f8f8f8;text-decoration:underline&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&#xA;&#xA;&lt;p&gt;&#xA;Unfortunately for me, this was wrong. If you look at &lt;a href=&#34;https://github.com/torvalds/linux/blob/8d3ae59288f1e7d58d76558a6ee96d533bc5019f/include/uapi/linux/rtnetlink.h#L697-L705&#34;&gt;the relevant kernel header&lt;/a&gt;, you can see that I copy-pasted the correct value for IPv4, then for IPv6 I used the value of the line directly underneath it, which is actually &lt;code&gt;RTMGRP_IPV4_MROUTE&lt;/code&gt;. IPv6 really needed the value &lt;code&gt;0x100&lt;/code&gt;.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;For whatever reason, my LLMs didn’t see this. Naively I’d have expected &lt;code&gt;RTMGRP_IPV6_IFADDR&lt;/code&gt; and &lt;code&gt;0x20&lt;/code&gt; in the same line to be a glaring, self-contained error, if you’ve been trained on lots of code where this would have been set to &lt;code&gt;0x100&lt;/code&gt;. Yet it didn’t, so we found the bug the boring, traditional way. (Tests.)&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;The best part of this bug is that as soon as I got Android compiling, I learnt that modern Android restricts netlink sockets and I was going to have to build an entirely different watcher mechanism involving their Java API. Thus I subtly broke the code for no gain whatsoever.&#xA;&lt;/p&gt;&#xA;&#xA;&lt;p&gt;&#xA;So there we have it. Enjoy the new release. If you’re maintaining a public library, at this stage you really have nothing to lose by asking an LLM to find the bugs. (Except for maybe getting put on strange lists.)&#xA;&lt;/p&gt;&#xA;&#xA;&lt;hr&gt;&#xA;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;footnote-1&#34;&gt;If you really want to know I used a mixture of DeepSeek v4 Flash and Pro and GPT-5.6 Luna and Sol, but it really doesn’t matter on a small codebase like this one. &lt;a href=&#34;#footnote-1-ref&#34;&gt;↩︎&lt;/a&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;footnote-2&#34;&gt;If crates.io downloads mean anything, usage seems to be increasing, which is gratifying. &lt;a href=&#34;#footnote-2-ref&#34;&gt;↩︎&lt;/a&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&#xA;</content>
    <link href="https://octet-stream.net/b/scb/2026-08-17-netwatcher-0.8.0-the-ai-bugfix-release.html" rel="alternate"></link>
    <author>
      <name>Thomas Karpiniec</name>
      <email>tom.karpiniec@outlook.com</email>
    </author>
  </entry>
</feed>