<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xml:base="https://spwoodcock.dev/" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    
    <title>Sam Woodcock</title>
    <link>https://spwoodcock.dev/</link>
    <atom:link href="https://spwoodcock.dev/feed.xml" rel="self" type="application/rss+xml" />
    <description>Personal site, showcasing my work and ramblings.</description>
    <language>en</language>
    <item>
      <title>Why many &#39;tech for good&#39; software projects fail</title>
      <link>https://spwoodcock.dev/blog/2026-09-20-tech-for-good-projects/</link><description>&lt;p&gt;I lead technology at an NGO working on humanitarian and global development
causes. I’m reasonably new to applying software skills in this space, so lately
I’ve been trying to read more widely.&lt;/p&gt;
&lt;p&gt;Recently I read Tim Unwin’s &lt;em&gt;Digital Inclusion in an Unequal World&lt;/em&gt;
(&lt;a href=&quot;https://ict4d.org.uk&quot;&gt;ict4d.org.uk&lt;/a&gt;), which promptly sent me down a rabbit
hole. To further elucidate this dense topic, I gathered a corpus of ICT4D -
information and communication technology for development - writing into an
archive: &lt;a href=&quot;https://github.com/spwoodcock/ict4d-archives&quot;&gt;github.com/spwoodcock/ict4d-archives&lt;/a&gt;,
spanning twenty-plus years of reports and field accounts.&lt;/p&gt;
&lt;p&gt;I then ran a small LLM-assisted synthesis over the corpus to identify the
recurring patterns. What follows are my findings.&lt;/p&gt;
&lt;div class=&quot;markdown-alert markdown-alert-note&quot;&gt;&lt;p class=&quot;markdown-alert-title&quot;&gt;&lt;svg class=&quot;octicon octicon-info mr-2&quot; viewBox=&quot;0 0 16 16&quot; version=&quot;1.1&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;On using an LLM for this&lt;/p&gt;&lt;p&gt;Following on from my post on &lt;a href=&quot;https://spwoodcock.dev/blog/2026-02-27-using-llms-ethically/&quot;&gt;using LLMs ethically&lt;/a&gt;,
this feels like one of the genuinely good use cases: qualitative synthesis
of existing knowledge, in service of ICT4D.&lt;/p&gt;
&lt;p&gt;The harms from about 10 minutes of running Opus 4.8 are relatively low,
while the upsides - not wasting scarce resources on projects nobody needs,
and not building &lt;em&gt;on&lt;/em&gt; communities - are high.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;I should caveat all of this by saying I’m reasonably new to the sector, so take it
with a pinch of salt. But there are some pretty clear patterns worth writing down -
and these are by no means novel ideas. A recent
&lt;a href=&quot;https://ajic.wits.ac.za/article/view/24415&quot;&gt;scoping review&lt;/a&gt; corroborates the
findings here, with evidence that common failure modes are much the same
as what follows.&lt;/p&gt;
&lt;h2 id=&quot;a-recurring-story&quot;&gt;A recurring story&lt;/h2&gt;
&lt;p&gt;Reading about failed projects in the development sector can really fill you
with cynicism.&lt;/p&gt;
&lt;p&gt;The failures I read about seemed to fall into two broad categories:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Well-intentioned people who either fail to capture what communities &lt;em&gt;truly&lt;/em&gt;
need or are unable to keep things self-sustaining when their enthusiasm
wanes or funding is cut.&lt;/li&gt;
&lt;li&gt;Institutional failings, where the whole project isn’t really about serving
communities, but the interests of the organisation itself (or its
wealthy corporate partners).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The canonical example of the first is the
&lt;a href=&quot;https://en.wikipedia.org/wiki/Roundabout_PlayPump&quot;&gt;Roundabout PlayPump&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;With regard to “tech for good” software, the most important thing I took away
is this: &lt;strong&gt;working software is rarely the primary challenge.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;five-common-failures&quot;&gt;Five common failures&lt;/h2&gt;
&lt;p&gt;Most of the following failures are structural to the system and repeat time
and time again.&lt;/p&gt;
&lt;h3 id=&quot;1-growth-over-equity&quot;&gt;1. Growth over equity&lt;/h3&gt;
&lt;p&gt;“Tech for good” is often measured by growth: users, downloads, countries
worked in, cash transfers.&lt;/p&gt;
&lt;p&gt;But the people who most need help are often the least profitable and
hardest to reach.&lt;/p&gt;
&lt;p&gt;While it’s true that from a utilitarian perspective the most efficient and
expedient way to reach as many people as possible can make sense, this
systematically missed out the true victims of the global systems that are
not designed with a concern for their needs.&lt;/p&gt;
&lt;h3 id=&quot;2-institutions-captured-by-whoever-pays&quot;&gt;2. Institutions captured by whoever pays&lt;/h3&gt;
&lt;p&gt;Follow the money and you often get to the heart of true motivations.&lt;/p&gt;
&lt;p&gt;This is a problem the world over for NGOs. Funders’ priorities best community
priorities. When money defines the mission, incentives are not aligned and
communities lose out.&lt;/p&gt;
&lt;h3 id=&quot;3-me-over-we&quot;&gt;3. “Me” over “we”&lt;/h3&gt;
&lt;p&gt;Awards, keynote speaking, “founders’ stories”, glossy apps, release events,
and circle jerking.&lt;/p&gt;
&lt;p&gt;The sector tends to reward individuals and institutions, but doesn’t
focus as strongly on day-to-day efforts to maintain known and needed technology
and efforts to hand over power to the people that actually use the tools.&lt;/p&gt;
&lt;h3 id=&quot;4-the-innovation-fetish&quot;&gt;4. The innovation fetish&lt;/h3&gt;
&lt;p&gt;Short project cycles plus a bias toward novelty produce a permanent pursuit of the
newest thing. Yesterday it was blockchain. Today it’s AI.&lt;/p&gt;
&lt;p&gt;Hype outpaces learning, and decades of reports on failed projects go unread.
Wilful blindness is a useful tool in the search for new funding, but doesn’t
favour well-tested and proven tech.&lt;/p&gt;
&lt;h3 id=&quot;5-extraction-and-dependency&quot;&gt;5. Extraction and dependency&lt;/h3&gt;
&lt;p&gt;This one is intrinsically linked to a world caught in the grips
of surveillance capitalism.&lt;/p&gt;
&lt;p&gt;Value - especially data - flows &lt;em&gt;out&lt;/em&gt; of communities, often in exchange for
something “free”. Dependency flows &lt;em&gt;in&lt;/em&gt;: a system only the original vendor can
change, risk of
&lt;a href=&quot;https://www.goodreads.com/en/book/show/222376640-enshittification&quot;&gt;enshitification&lt;/a&gt;
and rising costs, and a soft lock-in that makes a mockery of any open-source licence
(open code is not the same as local ownership).&lt;/p&gt;
&lt;p&gt;As an example, take UNICEF Giga’s work connecting schools across developing
countries. There are some genuine benefits - lower connectivity costs, government
leadership, measurable reach - but connectivity is an &lt;em&gt;output&lt;/em&gt;, and it’s easily
mistaken for an educational &lt;em&gt;outcome&lt;/em&gt;. A regional UNICEF
&lt;a href=&quot;https://evaluationreports.unicef.org/GetDocument?documentID=19824&amp;amp;fileID=59676&quot;&gt;case study&lt;/a&gt;
praises government ownership of the project, while also flagging donor dependency
and the need to transition further toward it as a future risk (not to mention government
ownership is not the same as community ownership, either).&lt;/p&gt;
&lt;p&gt;It’s always worth asking: who ends up owning the infrastructure,
the contracts and the connectivity data - and who pays to keep it running once
donor support ends?&lt;/p&gt;
&lt;h2 id=&quot;the-path-to-success&quot;&gt;The path to success&lt;/h2&gt;
&lt;p&gt;Not everything fails. Some ICT4D systems have run for fifteen or twenty years and
genuinely serve their communities.&lt;/p&gt;
&lt;p&gt;Specifically for software projects (my main wheelhouse), some tools I’ve had the
pleasure to work with have lasted and been used widely:
&lt;a href=&quot;https://spwoodcock.dev/blog/2024-06-contributing-to-odk/&quot;&gt;ODK&lt;/a&gt;,
&lt;a href=&quot;https://www.ushahidi.com&quot;&gt;Ushahidi&lt;/a&gt;, &lt;a href=&quot;https://ckan.org&quot;&gt;CKAN&lt;/a&gt;,
&lt;a href=&quot;https://home.rapidpro.io&quot;&gt;RapidPro&lt;/a&gt;, and &lt;a href=&quot;https://www.dimagi.com/commcare&quot;&gt;CommCare&lt;/a&gt;.
Generally these projects only sustain if some people find them useful (that isn’t
to say that every application of them necessarily improved people’s lives).&lt;/p&gt;
&lt;p&gt;Across the wider archive, including the
&lt;a href=&quot;https://github.com/spwoodcock/ict4d-archives/blob/main/data/analysis/findings.md&quot;&gt;comparison of 19 long-running ICT4D projects&lt;/a&gt;,
the same patterns recur in their histories, governance, funding, implementation
and evaluations. It’s not a guaranteed recipe, but it does give us six useful
questions to ask, &lt;strong&gt;before you write a line of code&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Solve a real problem.&lt;/strong&gt; Who experiences the problem, and have they actually
asked for it to be solved? Write down how the work happens today, the institution
responsible for it, and why an existing tool or a non-digital approach isn’t enough.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Share decision-making power.&lt;/strong&gt; Who can propose, prioritise, reject, fund and
release changes? If the people affected can only offer feedback that we’re free
to ignore, we’re consulting them, not sharing power with them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fund transitional ownership.&lt;/strong&gt; Name who will own the data, operations,
budget and outcomes. Cost the unglamorous work - hosting, security, support,
upgrades and training - and fund it beyond the end of the grant. True community ownership
(not neo-colonial lip service).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Design for the people most likely to be excluded.&lt;/strong&gt; Test with people facing
constraints around connectivity, cost, language, literacy or disability, and
keep an assisted or non-digital route open. Work out how the data could hurt
someone, and who is accountable for consent, correction and appeal.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Measure outcomes, not activity.&lt;/strong&gt; Before launch, define the change that should
follow from the software - not users registered, app installs or data collected.
Check who benefits and who remains excluded, and publish the result even if it
is null or disappointing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reuse before you build.&lt;/strong&gt; List the tools and previous attempts you investigated,
speak to the people who used or maintained them, and explain why adapting one
isn’t the answer. If something new is still justified, make sure people can
export their data and walk away from it.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;a-simpler-way-to-think-about-it&quot;&gt;A simpler way to think about it&lt;/h2&gt;
&lt;p&gt;You can boil all of this down to a simple rule:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Build &lt;em&gt;with&lt;/em&gt; people, not &lt;em&gt;for&lt;/em&gt; them, and never &lt;em&gt;on&lt;/em&gt; them.&lt;/strong&gt;
(idea adapted from Tim Unwin, mentioned above)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;“For” sounds generous, but it’s usually paternalistic. “On” is what happens when
people become test subjects for someone else’s idea. “With” is the only one that
survives for the long term - because the community helped decide what gets
built, and has a stake in keeping it alive.&lt;/p&gt;
&lt;div class=&quot;markdown-alert markdown-alert-note&quot;&gt;&lt;p class=&quot;markdown-alert-title&quot;&gt;&lt;svg class=&quot;octicon octicon-info mr-2&quot; viewBox=&quot;0 0 16 16&quot; version=&quot;1.1&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;A small case study: Ushahidi&lt;/p&gt;&lt;p&gt;After Kenya’s disputed 2007 election, Kenyan developers and bloggers built
&lt;a href=&quot;https://www.ushahidi.com&quot;&gt;Ushahidi&lt;/a&gt; - Swahili for “testimony” - in a matter of
days. People could report violence by web or SMS and see it mapped in near real
time. It began with people living through a crisis, using simple technology to
solve a problem they had named themselves. Nearly two decades later it is still
maintained and used around the world. The reach came later; it was never the
point. That origin did not guarantee success: later evaluations also found weak
feedback loops, training and coordination. The point is not that Ushahidi was
perfect, but that it began with a real problem rather than technology in search
of one.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;The cheapest improvement available to this whole sector is still the same: stop
reinventing the same failure, build on what already works, and start with the
people the software is meant to serve.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;postscript-tools-i-try-to-build-in-this-spirit&quot;&gt;Postscript: tools I try to build in this spirit&lt;/h2&gt;
&lt;p&gt;A few tools I’ve been lucky to work on that try to put these ideas into practice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://drone.hotosm.org&quot;&gt;Drone Tasking Manager&lt;/a&gt;&lt;/strong&gt; coordinates the collection
of up-to-date imagery. The intended outcome isn’t the imagery, but the decisions
that can be taken by communities, once they better understand the structure of
the areas where they live.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://spwoodcock.dev/blog/2025-04-field-mapping-past-present-future/&quot;&gt;Field Tasking Manager&lt;/a&gt;&lt;/strong&gt; prevents
overlap in field-mapping campaigns. It takes existing, well-established tools, such as
ODK and QField, and solves a problem around coordination that has been reported by
many communities globally.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;While I think they both tick some of the boxes, there is definitely room for improvement.
They should be held to the same six standards above, and of course, changed or even
abandoned if they fail to over time.&lt;/p&gt;
</description><pubDate>Sun, 20 Sep 2026 00:00:00 +0000</pubDate>
      <dc:creator>Sam Woodcock</dc:creator>
      <guid>https://spwoodcock.dev/blog/2026-09-20-tech-for-good-projects/</guid>
    </item>
    <item>
      <title>Intro to photogrammetry</title>
      <link>https://spwoodcock.dev/blog/2026-09-01-photogrammetry/</link><description>&lt;div class=&quot;markdown-alert markdown-alert-note&quot;&gt;&lt;p class=&quot;markdown-alert-title&quot;&gt;&lt;svg class=&quot;octicon octicon-info mr-2&quot; viewBox=&quot;0 0 16 16&quot; version=&quot;1.1&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;Note&lt;/p&gt;&lt;p&gt;Big disclaimer up front: the maths behind this stuff is &lt;strong&gt;far&lt;/strong&gt; from my
area of expertise. I’m writing as a photogrammetry layperson that got
interested in this topic after processing a tonne of projects using
OpenDroneMap. Excuse any ignorance you may observe!&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;how-i-ended-up-here&quot;&gt;How I ended up here&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;I recently had a call with someone worried about the future of
&lt;a href=&quot;https://github.com/OpenSfM/OpenSfM&quot;&gt;OpenSfM&lt;/a&gt; - the engine at the heart of
&lt;a href=&quot;https://opendronemap.org&quot;&gt;OpenDroneMap&lt;/a&gt; (ODM), which turns drone photos
into maps. Their idea: &lt;em&gt;“why not use &lt;a href=&quot;https://colmap.github.io&quot;&gt;COLMAP&lt;/a&gt;
instead? It’s come a long way and has a big community”&lt;/em&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;At the time I thought OpenSfM was essentially &lt;strong&gt;dead&lt;/strong&gt; upstream, leaving
the ODM team to maintain their fork forever. So I went digging into whether
COLMAP could replace it.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;And in doing so, I got completely sucked into learning how photogrammetry
actually &lt;em&gt;works&lt;/em&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Plot twist: OpenSfM turned out to be &lt;strong&gt;very much alive&lt;/strong&gt; - a bunch of major
upstream improvements started landing right as I began poking around. More
on that later.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;But the exploration taught me a lot, so let’s dig in. This post is my
attempt to demystify photogrammetry for the layperson (i.e. past-me), then
weigh up COLMAP vs OpenSfM for &lt;strong&gt;drone mapping&lt;/strong&gt; specifically.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;markdown-alert markdown-alert-note&quot;&gt;&lt;p class=&quot;markdown-alert-title&quot;&gt;&lt;svg class=&quot;octicon octicon-info mr-2&quot; viewBox=&quot;0 0 16 16&quot; version=&quot;1.1&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;Note&lt;/p&gt;&lt;p&gt;If you didn’t know, I’m the tech lead @ HOTOSM, where we use drone imagery
and tools like ODM to produce open geospatial data for humanitarian
response. My lens is “what works for community drone mapping”.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;what-even-is-photogrammetry&quot;&gt;What even is photogrammetry?&lt;/h2&gt;
&lt;p&gt;Photogrammetry is the art of turning a pile of overlapping &lt;strong&gt;2D photos&lt;/strong&gt; into
a &lt;strong&gt;3D reconstruction&lt;/strong&gt; of the world.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://spwoodcock.dev/images/blog/2026-09-photogrammetry-overview1.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;In the context of aerial imagery, you can imagine it more like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://spwoodcock.dev/images/blog/2026-09-photogrammetry-overview2.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;The trick is the same one your &lt;strong&gt;two eyes&lt;/strong&gt; use for depth perception. Each
eye sees a slightly different view, and your brain fuses them into a sense
of distance.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Photogrammetry does this with many “eyes” - dozens or thousands of photos
spread out over space (in our case, snapped by a drone flying a grid).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;If the same physical point shows up in several photos taken from different
positions, a bit of geometry lets us work out &lt;strong&gt;where that point sits in
3D&lt;/strong&gt;, &lt;em&gt;and&lt;/em&gt; &lt;strong&gt;where the camera was&lt;/strong&gt; for each shot.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The pipeline splits into two big stages, worth understanding separately:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Sparse reconstruction&lt;/strong&gt; (Structure-from-Motion): find the camera
positions and a rough skeleton of 3D points.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dense reconstruction&lt;/strong&gt; (Multi-View Stereo): use those camera positions
to fill in a detailed surface.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Then downstream tools turn that into the things we actually want:
&lt;strong&gt;orthophotos&lt;/strong&gt; (a flat, top-down, distortion-corrected map image) and
&lt;strong&gt;elevation models&lt;/strong&gt;. Let’s take each stage in turn.&lt;/p&gt;
&lt;h2 id=&quot;stage-1-structure-from-motion-the-sparse-bit&quot;&gt;Stage 1: Structure-from-Motion (the sparse bit)&lt;/h2&gt;
&lt;p&gt;We hand over a folder of photos; ideally we get back every camera’s position,
its lens settings, and a sparse cloud of 3D points. It happens in three
steps.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Feature detection&lt;/strong&gt; - find distinctive little landmarks in each photo:
corners, textures, anything recognisable again elsewhere. Think of spotting
the same chimney or road marking in shot after shot.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Feature matching&lt;/strong&gt; - work out which landmark in photo A is the same
landmark in photos B, C, D… This is what links the photos together.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://spwoodcock.dev/images/blog/2026-09-photogrammetry-matching.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;div class=&quot;markdown-alert markdown-alert-note&quot;&gt;&lt;p class=&quot;markdown-alert-title&quot;&gt;&lt;svg class=&quot;octicon octicon-info mr-2&quot; viewBox=&quot;0 0 16 16&quot; version=&quot;1.1&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;Note&lt;/p&gt;&lt;p&gt;We match features across overlapping photos, so this is why
we need to make sure to fly with good photo overlap!&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;3. Triangulation &amp;amp; bundle adjustment&lt;/strong&gt; - the big number-crunch. Here’s the
catch22: to place a 3D point you need to know where the cameras were, but to know
where the cameras were you need 3D points. So the software solves for
&lt;strong&gt;everything at once&lt;/strong&gt; - all camera positions and all 3D points together -
nudging them until the whole scene is as consistent as possible.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://spwoodcock.dev/images/blog/2026-09-photogrammetry-triangulation.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;The output is a &lt;strong&gt;sparse point cloud&lt;/strong&gt; - the scene’s skeleton - plus every
camera’s pose. Not pretty yet, but the difficult geometry is done.&lt;/p&gt;
&lt;h3 id=&quot;incremental-vs-global&quot;&gt;Incremental vs global&lt;/h3&gt;
&lt;p&gt;Two strategies for this are available.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Incremental&lt;/strong&gt; - add photos &lt;strong&gt;one at a time&lt;/strong&gt;, re-optimising as you go.
Robust and accurate, but takes a long time on large image sets.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Global&lt;/strong&gt; - solve for &lt;strong&gt;all&lt;/strong&gt; camera poses at once. Scales far better and
is dramatically faster. It used to trade away some accuracy, but recent
work (&lt;a href=&quot;https://github.com/colmap/glomap&quot;&gt;GLOMAP&lt;/a&gt;, now part of COLMAP) has
mostly closed that gap.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;colmap-vs-the-opensfm-fork&quot;&gt;COLMAP vs the OpenSfM fork&lt;/h3&gt;
&lt;p&gt;This was my &lt;a href=&quot;https://community.opendronemap.org/t/should-odm-adopt-colmap-as-an-opensfm-alternative/26826&quot;&gt;original question&lt;/a&gt;. For &lt;strong&gt;drone mapping specifically&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OpenDroneMap’s OpenSfM fork&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;👍 Fast, with ~2× lower memory use - great for the low-spec devices our
communities actually use.&lt;/li&gt;
&lt;li&gt;👍 Loads of drone-specific georeferencing baked in: camera angles,
Ground Control Points, flexible coordinate systems (huge credit to
&lt;a href=&quot;https://github.com/OpenDroneMap/OpenSfM/pull/46&quot;&gt;YanNoun&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;👎 Looked dead upstream, meaning maintenance responsibility for the ODM
team (since changed!).&lt;/li&gt;
&lt;li&gt;👎 No real GPU acceleration at the time.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;COLMAP&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;👍 Healthy, actively developed, well tested.&lt;/li&gt;
&lt;li&gt;👍 Global SfM built in, and strong GPU support.&lt;/li&gt;
&lt;li&gt;👍 Very robust in awkward scenes - ODM is tuned for &lt;strong&gt;nadir&lt;/strong&gt;
(straight-down) shots, so COLMAP does better on oblique angles.&lt;/li&gt;
&lt;li&gt;👎 &lt;strong&gt;No native drone georeferencing&lt;/strong&gt; - all the stuff Yann already solved.&lt;/li&gt;
&lt;li&gt;👎 Its global pipeline estimates camera parameters differently, and those
numbers feed &lt;strong&gt;straight into&lt;/strong&gt; the final map’s accuracy.&lt;/li&gt;
&lt;li&gt;👎 Real integration effort into the ODM stack.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;stage-2-multi-view-stereo-the-dense-bit&quot;&gt;Stage 2: Multi-View Stereo (the dense bit)&lt;/h2&gt;
&lt;p&gt;Once SfM has nailed the camera positions, dense reconstruction fills in the
detail - going from a sparse skeleton to a dense, photorealistic surface.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://spwoodcock.dev/images/blog/2026-09-photogrammetry-sparse.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://spwoodcock.dev/images/blog/2026-09-photogrammetry-dense.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Above is the the sparse skeleton from SfM with the dense result below.&lt;/p&gt;
&lt;p&gt;In ODM this is currently a &lt;strong&gt;separate&lt;/strong&gt; tool,
&lt;a href=&quot;https://github.com/cdcseacave/openMVS&quot;&gt;OpenMVS&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;After this, it’s rasterised into the final orthophoto and elevation model.&lt;/p&gt;
&lt;h3 id=&quot;could-openmvs-be-replaced&quot;&gt;Could OpenMVS be replaced?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;The most exciting recent OpenSfM news: it’s growing its &lt;strong&gt;own&lt;/strong&gt; dense
reconstruction, plus direct orthophoto + elevation output. That could make
OpenSfM a &lt;strong&gt;single framework&lt;/strong&gt; for the whole chain - no separate OpenMVS
step.&lt;/li&gt;
&lt;li&gt;It’s also gaining &lt;strong&gt;OpenCL&lt;/strong&gt; GPU acceleration. Unlike CUDA, OpenCL supports
AMD, Intel &lt;em&gt;and&lt;/em&gt; Nvidia, with graceful fallback to CPU when there’s no GPU
at all - which matters hugely for mixed, low-spec community hardware.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;where-colmap-falls-down-here&quot;&gt;Where COLMAP falls down here&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;COLMAP &lt;em&gt;can&lt;/em&gt; also do the dense step, in theory replacing both tools at once.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;But&lt;/strong&gt; its dense reconstruction is &lt;strong&gt;CUDA-only&lt;/strong&gt; - useless if you don’t
have an Nvidia GPU. That rules it out as a general ODM replacement.&lt;/li&gt;
&lt;li&gt;So even in a COLMAP world, it’d only ever replace the &lt;strong&gt;sparse&lt;/strong&gt; step;
we’d still need OpenMVS (or future-OpenSfM) for the dense.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;wrapping-up-what-s-best-for-what&quot;&gt;Wrapping up: what’s best for what?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;For drone mapping in the ODM/HOTOSM world&lt;/strong&gt;: the newly revived
&lt;a href=&quot;https://github.com/OpenSfM/OpenSfM&quot;&gt;official OpenSfM&lt;/a&gt; is the right tool
&lt;em&gt;right now&lt;/em&gt; - the drone-specific georeferencing, low memory, CPU
fallback, and renewed upstream momentum works across our community
of mixed spec devices.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;For difficult, oblique, or non-drone scenes&lt;/strong&gt;: COLMAP is genuinely
excellent and battle-tested. Outside the drone-nadir sweet spot, I would
definitely try it out.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The COLMAP question isn’t dead&lt;/strong&gt;, just weakened by OpenSfM’s revival. It
could still be a future successor - once the georeferencing and
camera-parameter issues above are solved. But honestly, I can’t see
CUDA being replaced any time soon.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;what-to-watch-for&quot;&gt;What to watch for&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;OpenSfM’s dense + direct-ortho work maturing, potentially collapsing the
whole sparse → dense → map chain into one framework.&lt;/li&gt;
&lt;li&gt;OpenCL acceleration landing properly - a big win for anyone without a fancy
CUDA card.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Old-hardware support&lt;/strong&gt; - I initially worried that moving to pre-built
packages would drop support for very old CPUs (no AVX/AVX2). A maintainer
kindly set me straight: the Conda packages compile with &lt;code&gt;-march=nocona&lt;/code&gt;
(a ~2004 baseline, even older than ODM’s old &lt;code&gt;nehalem&lt;/code&gt; target) plus
&lt;code&gt;-mtune=haswell&lt;/code&gt;, which lets newer chips grab some optimisations &lt;em&gt;without&lt;/em&gt;
breaking backwards compatibility. A couple of forked libs still cap the
effective baseline at &lt;code&gt;nehalem&lt;/code&gt; - the same as before. So very old hardware
should keep working.&lt;/li&gt;
&lt;/ul&gt;
</description><pubDate>Sat, 29 Aug 2026 00:00:00 +0000</pubDate>
      <dc:creator>Sam Woodcock</dc:creator>
      <guid>https://spwoodcock.dev/blog/2026-09-01-photogrammetry/</guid>
    </item>
    <item>
      <title>How to fix the eSIM eUICC on a Google Pixel running GrapheneOS</title>
      <link>https://spwoodcock.dev/blog/2026-08-05-graphene-pixel-esim-fix/</link><description>&lt;h2 id=&quot;pretext-broken-firmware&quot;&gt;Pretext: broken firmware&lt;/h2&gt;
&lt;p&gt;I have been running GrapheneOS for some years now, and recently
bought a second hand Pixel 9.&lt;/p&gt;
&lt;p&gt;After doing a full migration and installing apps, I realised the eSIM
capability was broken. I spent a while having to hotspot from my old
phone as a compromise. Not ideal.&lt;/p&gt;
&lt;div class=&quot;markdown-alert markdown-alert-note&quot;&gt;&lt;p class=&quot;markdown-alert-title&quot;&gt;&lt;svg class=&quot;octicon octicon-info mr-2&quot; viewBox=&quot;0 0 16 16&quot; version=&quot;1.1&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;Note&lt;/p&gt;&lt;p&gt;All of this was done on a Pixel 9 (&lt;code&gt;tokay&lt;/code&gt;), GrapheneOS &lt;code&gt;2026080500&lt;/code&gt;,
&lt;code&gt;P24 (ST54L)&lt;/code&gt;. Other Pixels need different strings throughout.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;After some digging it seems that the cause might have been:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The device was enrolled in the Android developer programme by the
previous owner.&lt;/li&gt;
&lt;li&gt;Google pushed out some updates that broke the firmware on the
eUICC for the eSIM capabilities.&lt;/li&gt;
&lt;li&gt;There have been genuine reports of some Pixels being shipped
with the eUICC broken on the hardware level, but the reports of
broken eSIM capability were so wide-spread and across models that
I was suspicious if this was actually firmware related.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Finally, a
&lt;a href=&quot;https://discuss.grapheneos.org/d/33125-unable-to-activate-esim&quot;&gt;recent post&lt;/a&gt;
on the GrapheneOS forums thread I have been following confirmed that
suspicion.&lt;/p&gt;
&lt;p&gt;A user posted a fix on a similar
&lt;a href=&quot;https://issuetracker.google.com/issues/525705741&quot;&gt;Google issue tracker thread&lt;/a&gt;
and users started reporting a 5th August OTA update also came with a fix.&lt;/p&gt;
&lt;p&gt;I subscribed to the GrapheneOS beta channel and updated to the
&lt;a href=&quot;https://grapheneos.org/releases#2026080500&quot;&gt;2026080500&lt;/a&gt; update
that should include the patch. No dice.&lt;/p&gt;
&lt;div class=&quot;markdown-alert markdown-alert-note&quot;&gt;&lt;p class=&quot;markdown-alert-title&quot;&gt;&lt;svg class=&quot;octicon octicon-info mr-2&quot; viewBox=&quot;0 0 16 16&quot; version=&quot;1.1&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;Note&lt;/p&gt;&lt;p&gt;You can confirm you have the issue by dialing &lt;code&gt;*#*#4636#*#*&lt;/code&gt; on your phone.
In ‘Phone Information’ you will see
&lt;code&gt;eUICC info: { Available memory in bytes:-1 }&lt;/code&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;the-fix&quot;&gt;The Fix&lt;/h2&gt;
&lt;p&gt;I started writing a guide on how to apply this fix using GrapheneOS entirely
and Magisk, avoiding the need to flash LineageOS. But the attempt failed
due to missing ‘Network’ permissions on the eSIM updater utility on
GrapheneOS.&lt;/p&gt;
&lt;p&gt;See full details by expanding below:&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;The failed attempt to use GrapheneOS only!&lt;/summary&gt;
&lt;p&gt;This
&lt;a href=&quot;https://xdaforums.com/t/guide-fix-pixel-esim-euicc.4797083&quot;&gt;excellent XDA forums post&lt;/a&gt;
has a guide to fix your vanilla Pixel eUICC.&lt;/p&gt;
&lt;p&gt;I had to tweak this a little to work with Graphene, where root capability
is a bit more of a pain:&lt;/p&gt;
&lt;h3 id=&quot;1-root-graphene&quot;&gt;1. Root Graphene&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Update to the latest GrapheneOS via OTA.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Download the matching &lt;code&gt;.zip&lt;/code&gt; image for the release image of
&lt;strong&gt;your device model&lt;/strong&gt; from the
&lt;a href=&quot;https://grapheneos.org/releases&quot;&gt;releases page&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Extract the contents and save the &lt;code&gt;init_boot.img&lt;/code&gt; file inside.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Use the &lt;a href=&quot;https://grapheneos.org/install/web&quot;&gt;Graphene web installer&lt;/a&gt;
to unlock the bootloader (ensure OEM Unlocking is enabled on-device).&lt;/p&gt;
&lt;div class=&quot;markdown-alert markdown-alert-warning&quot;&gt;&lt;p class=&quot;markdown-alert-title&quot;&gt;&lt;svg class=&quot;octicon octicon-alert mr-2&quot; viewBox=&quot;0 0 16 16&quot; version=&quot;1.1&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;Warning&lt;/p&gt;&lt;p&gt;This will wipe your device!&lt;/p&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Reboot the wiped device, follow setup, then copy the &lt;code&gt;init_boot.img&lt;/code&gt;
into your Downloads dir on the device. &lt;strong&gt;Also make sure you connect
to an internet source for the chip flash later&lt;/strong&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Download the latest
&lt;a href=&quot;https://github.com/topjohnwu/Magisk/releases&quot;&gt;Magisk release&lt;/a&gt;
APK, and also copy this to your device Downloads.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Install Magisk, open. Install --&amp;gt; Select and Patch a File --&amp;gt;
your &lt;code&gt;init_boot.img&lt;/code&gt; file. It will output a &lt;code&gt;magisk_patched-XXXXX.img&lt;/code&gt;
to your Downloads dir. Copy it to your PC.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Ensure USB Debugging is enabled and you have &lt;code&gt;platform-tools&lt;/code&gt; or similar
for both &lt;code&gt;adb&lt;/code&gt; &amp;amp; &lt;code&gt;fastboot&lt;/code&gt; on your PC.
Run the following:&lt;/p&gt;

&lt;div style=&quot;position: relative&quot; id=&quot;code-container-112&quot;&gt;
  &lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code id=&quot;code-112&quot; class=&quot;language-bash&quot;&gt;adb &lt;span class=&quot;token function&quot;&gt;reboot&lt;/span&gt; bootloader
fastboot flash init_boot magisk_patched-XXXXX.img
fastboot &lt;span class=&quot;token function&quot;&gt;reboot&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

  &lt;button class=&quot;code-copy &quot; data-clipboard-target=&quot;#code-112&quot; style=&quot;all: unset; background: transparent; position: absolute; top: 7.5px; right: 6px; padding-top: 3px; cursor: pointer;&quot; title=&quot;Copy&quot;&gt;
    &lt;span style=&quot;display:inline-block;background:url(https://api.iconify.design/si/copy-duotone.svg?color=%23bac2de) no-repeat center center / contain;width: 24px; height: 24px;&quot; class=&quot;code-copy-clipboard&quot;&gt;&lt;/span&gt;
  &lt;/button&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You are now rooted.&lt;/p&gt;
&lt;h3 id=&quot;2-fix-the-euicc&quot;&gt;2. Fix The eUICC&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Run the following:&lt;/p&gt;

&lt;div style=&quot;position: relative&quot; id=&quot;code-container-126&quot;&gt;
  &lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code id=&quot;code-126&quot; class=&quot;language-bash&quot;&gt;adb shell
&lt;span class=&quot;token function&quot;&gt;su&lt;/span&gt;                              &lt;span class=&quot;token comment&quot;&gt;# grant the root prompt on-device&lt;/span&gt;
resetprop ro.build.type eng
resetprop ro.build.tags eng
resetprop ro.debuggable &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
setenforce &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;
stop &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;sleep&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; start&lt;/code&gt;&lt;/pre&gt;

  &lt;button class=&quot;code-copy &quot; data-clipboard-target=&quot;#code-126&quot; style=&quot;all: unset; background: transparent; position: absolute; top: 7.5px; right: 6px; padding-top: 3px; cursor: pointer;&quot; title=&quot;Copy&quot;&gt;
    &lt;span style=&quot;display:inline-block;background:url(https://api.iconify.design/si/copy-duotone.svg?color=%23bac2de) no-repeat center center / contain;width: 24px; height: 24px;&quot; class=&quot;code-copy-clipboard&quot;&gt;&lt;/span&gt;
  &lt;/button&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Keep the adb shell open, wait for the framework to restart
(the screen will bounce, this isn’t a full reboot), then:&lt;/p&gt;

&lt;div style=&quot;position: relative&quot; id=&quot;code-container-132&quot;&gt;
  &lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code id=&quot;code-132&quot; class=&quot;language-bash&quot;&gt;am start &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; com.google.euiccpixel/com.google.euiccpixel.ui.OtaUpdateMenu&lt;/code&gt;&lt;/pre&gt;

  &lt;button class=&quot;code-copy &quot; data-clipboard-target=&quot;#code-132&quot; style=&quot;all: unset; background: transparent; position: absolute; top: 7.5px; right: 6px; padding-top: 3px; cursor: pointer;&quot; title=&quot;Copy&quot;&gt;
    &lt;span style=&quot;display:inline-block;background:url(https://api.iconify.design/si/copy-duotone.svg?color=%23bac2de) no-repeat center center / contain;width: 24px; height: 24px;&quot; class=&quot;code-copy-clipboard&quot;&gt;&lt;/span&gt;
  &lt;/button&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Find your config key in the linked XDA forum post or elsewhere.
For a Pixel 9 it is &lt;code&gt;P24 (ST54L)&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Click the ‘Upgrade Firmware’ dropdown on your phone and select
the &lt;code&gt;GGL_P24_ST54L_GSMALIVE_534-1.1_DVT_CG7_v2&lt;/code&gt; option.
Note the xxx_P24_ST54L_xxx. DO NOT PROCEED if you get a warning,
as it could brick your eSIM.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Then …&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I got to this point in the writeup &amp;amp; found the eSIM updater app
doesn’t have network permission on GrapheneOS.&lt;/p&gt;
&lt;p&gt;Using some &lt;code&gt;adb logcat&lt;/code&gt; I found that it gets to the point of the image
handshake from the secure element, then dies the instant it does
&lt;code&gt;Sending HTTPS POST&lt;/code&gt; with “Unable to resolve host,”.&lt;/p&gt;
&lt;p&gt;This means any attempt to update using this app is doomed to fail,
and we can’t update the app permissions retroactively…&lt;/p&gt;
&lt;p&gt;(I did a fair bit of debugging to confirm this was the case
and it wasn’t firewalls, faulty internet, DNS resolution issues).&lt;/p&gt;
&lt;/details&gt;
&lt;p&gt;I decided to pivot and simply follow the XDA guide by installing
LineageOS and MindTheGapps.&lt;/p&gt;
&lt;div class=&quot;markdown-alert markdown-alert-warning&quot;&gt;&lt;p class=&quot;markdown-alert-title&quot;&gt;&lt;svg class=&quot;octicon octicon-alert mr-2&quot; viewBox=&quot;0 0 16 16&quot; version=&quot;1.1&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;Warning&lt;/p&gt;&lt;p&gt;This wipes your device twice (bootloader unlock, then the Graphene
reinstall). Back up somewhere that isn’t the phone.&lt;/p&gt;
&lt;/div&gt;
&lt;h3 id=&quot;1-install-lineageos&quot;&gt;1. Install LineageOS&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;I flashed back to a clean public build using
&lt;a href=&quot;https://flash.android.com/back-to-public&quot;&gt;Google Flash Tool&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Use the &lt;a href=&quot;https://grapheneos.org/install/web&quot;&gt;Graphene web installer&lt;/a&gt;
to unlock the bootloader (ensure OEM Unlocking is enabled on-device).
It’s just a &lt;code&gt;fastboot flashing unlock&lt;/code&gt; frontend, so using it here is fine.&lt;/li&gt;
&lt;li&gt;Install Lineage, making sure to install Google Apps via official instructions.&lt;/li&gt;
&lt;li&gt;Boot Lineage. Connect Wi-Fi. Toggle USB debugging &amp;amp; Rooted debugging.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;2-update-the-euiccpixel-package&quot;&gt;2. Update The euiccpixel Package&lt;/h3&gt;
&lt;p&gt;I had to update to v&lt;code&gt;D.2.0.22&lt;/code&gt; for this to work.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Download the official
&lt;a href=&quot;https://developers.google.com/android/images&quot;&gt;Pixel build&lt;/a&gt;
for &lt;strong&gt;your device&lt;/strong&gt; from Google.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Then get the pkg while keeping my filesystem clean:&lt;/p&gt;

&lt;div style=&quot;position: relative&quot; id=&quot;code-container-212&quot;&gt;
  &lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code id=&quot;code-212&quot; class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; run &lt;span class=&quot;token parameter variable&quot;&gt;--rm&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-it&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-v&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token environment constant&quot;&gt;$PWD&lt;/span&gt;&quot;&lt;/span&gt;:/work &lt;span class=&quot;token parameter variable&quot;&gt;-w&lt;/span&gt; /work ubuntu:24.04 &lt;span class=&quot;token function&quot;&gt;bash&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;apt-get&lt;/span&gt; update &lt;span class=&quot;token parameter variable&quot;&gt;-qq&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;apt-get&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-y&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-qq&lt;/span&gt; android-sdk-libsparse-utils e2fsprogs &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; python3 aapt &lt;span class=&quot;token function&quot;&gt;unzip&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;unzip&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; tokay-cp2a.260805.005-factory-9b3ac3f4.zip
&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; tokay-cp2a.260805.005
&lt;span class=&quot;token function&quot;&gt;unzip&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; image-tokay-*.zip

&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token for-or-select variable&quot;&gt;P&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; system_ext product&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-rf&lt;/span&gt; out_&lt;span class=&quot;token variable&quot;&gt;$P&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;mkdir&lt;/span&gt; out_&lt;span class=&quot;token variable&quot;&gt;$P&lt;/span&gt;
  debugfs &lt;span class=&quot;token parameter variable&quot;&gt;-R&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;rdump / out_&lt;span class=&quot;token variable&quot;&gt;$P&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$P&lt;/span&gt;.img &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;/dev/null &lt;span class=&quot;token operator&quot;&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;2&lt;/span&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;&amp;amp;1&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;done&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;find&lt;/span&gt; out_* &lt;span class=&quot;token parameter variable&quot;&gt;-iname&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;*euicc*.apk&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-vi&lt;/span&gt; overlay &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-vi&lt;/span&gt; EuiccGoogle &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-vi&lt;/span&gt; Permissions
&lt;span class=&quot;token comment&quot;&gt;# out_system_ext/priv-app/EuiccSupportPixel-P23/EuiccSupportPixel-P23.apk&lt;/span&gt;
aapt dump badging out_system_ext/priv-app/EuiccSupportPixel-P23/EuiccSupportPixel-P23.apk &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;grep&lt;/span&gt; versionName
&lt;span class=&quot;token comment&quot;&gt;# package: name=&#39;com.google.euiccpixel&#39; versionCode=&#39;20225&#39; versionName=&#39;D.2.0.22 (15551270)&#39; platformBuildVersionName=&#39;D.2.0.22 (15551270)&#39; platformBuildVersionCode=&#39;20225&#39; compileSdkVersion=&#39;29&#39; compileSdkVersionCodename=&#39;10&#39;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Not ideal! The Google thread above suggests that people managed to get D.2.0.23&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# But I inspected the cert and it&#39;s valid until Jan 2027.&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# After testing, this version worked fine, see below.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

  &lt;button class=&quot;code-copy &quot; data-clipboard-target=&quot;#code-212&quot; style=&quot;all: unset; background: transparent; position: absolute; top: 7.5px; right: 6px; padding-top: 3px; cursor: pointer;&quot; title=&quot;Copy&quot;&gt;
    &lt;span style=&quot;display:inline-block;background:url(https://api.iconify.design/si/copy-duotone.svg?color=%23bac2de) no-repeat center center / contain;width: 24px; height: 24px;&quot; class=&quot;code-copy-clipboard&quot;&gt;&lt;/span&gt;
  &lt;/button&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;As this was a &lt;strong&gt;huge&lt;/strong&gt; pain in the ass and wasted hours of my life
(trying to find &lt;code&gt;D.2.0.23&lt;/code&gt; unnecessarily),
I have attached the &lt;a href=&quot;https://spwoodcock.dev/downloads/EuiccSupportPixel-P23.apk&quot;&gt;EuiccSupportPixel-P23.apk&lt;/a&gt;
extracted from &lt;code&gt;tokay-cp2a.260805.005-factory-9b3ac3f4.zip&lt;/code&gt; for you to
simply install and save the faff.&lt;/p&gt;
&lt;p&gt;Unmodified, but verify it anyway:&lt;/p&gt;

&lt;div style=&quot;position: relative&quot; id=&quot;code-container-221&quot;&gt;
  &lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code id=&quot;code-221&quot; class=&quot;language-bash&quot;&gt;sha256sum EuiccSupportPixel-P23.apk
&lt;span class=&quot;token comment&quot;&gt;# 14b99f90faed25c23284a6008fabea4e6a2bdc583b8709a04b47859d007ad9a7&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

  &lt;button class=&quot;code-copy &quot; data-clipboard-target=&quot;#code-221&quot; style=&quot;all: unset; background: transparent; position: absolute; top: 7.5px; right: 6px; padding-top: 3px; cursor: pointer;&quot; title=&quot;Copy&quot;&gt;
    &lt;span style=&quot;display:inline-block;background:url(https://api.iconify.design/si/copy-duotone.svg?color=%23bac2de) no-repeat center center / contain;width: 24px; height: 24px;&quot; class=&quot;code-copy-clipboard&quot;&gt;&lt;/span&gt;
  &lt;/button&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;3-fix-the-euicc&quot;&gt;3. Fix The eUICC&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Run the following:&lt;/p&gt;

&lt;div style=&quot;position: relative&quot; id=&quot;code-container-232&quot;&gt;
  &lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code id=&quot;code-232&quot; class=&quot;language-bash&quot;&gt;adb &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-r&lt;/span&gt; EuiccSupportPixel-P23.apk
adb root
adb shell
setenforce &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;
getenforce
am start &lt;span class=&quot;token parameter variable&quot;&gt;-S&lt;/span&gt; com.google.euiccpixel/.ui.OtaUpdateMenu&lt;/code&gt;&lt;/pre&gt;

  &lt;button class=&quot;code-copy &quot; data-clipboard-target=&quot;#code-232&quot; style=&quot;all: unset; background: transparent; position: absolute; top: 7.5px; right: 6px; padding-top: 3px; cursor: pointer;&quot; title=&quot;Copy&quot;&gt;
    &lt;span style=&quot;display:inline-block;background:url(https://api.iconify.design/si/copy-duotone.svg?color=%23bac2de) no-repeat center center / contain;width: 24px; height: 24px;&quot; class=&quot;code-copy-clipboard&quot;&gt;&lt;/span&gt;
  &lt;/button&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Find your config key in the linked XDA forum post or elsewhere.
For a Pixel 9 it is &lt;code&gt;P24 (ST54L)&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Click the ‘Upgrade Firmware’ dropdown on your phone and select
the &lt;code&gt;GGL_P24_ST54L_GSMALIVE_534-1.1_DVT_CG7_v2&lt;/code&gt; option.
Note the xxx_P24_ST54L_xxx. DO NOT PROCEED if you get a warning,
as it could brick your eSIM.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This should succeed! Check &lt;code&gt;*#*#4636#*#*&lt;/code&gt; again for a positive
memory value.&lt;/p&gt;
&lt;h3 id=&quot;4-reflash-grapheneos&quot;&gt;4. Reflash GrapheneOS&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Simply use
&lt;a href=&quot;https://grapheneos.org/install/web&quot;&gt;https://grapheneos.org/install/web&lt;/a&gt;
to re-install
as normal, then re-lock the bootloader.&lt;/li&gt;
&lt;li&gt;Add an eSIM. Profit!?&lt;/li&gt;
&lt;/ol&gt;
</description><pubDate>Wed, 05 Aug 2026 00:00:00 +0000</pubDate>
      <dc:creator>Sam Woodcock</dc:creator>
      <guid>https://spwoodcock.dev/blog/2026-08-05-graphene-pixel-esim-fix/</guid>
    </item>
    <item>
      <title>Automate the setup of Garage S3 standalone mode</title>
      <link>https://spwoodcock.dev/blog/2026-03-03-automate-garage-standalone/</link><description>&lt;h2 id=&quot;why-garage&quot;&gt;Why Garage?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Minio was a great open-source S3-compatible API until late 2025.&lt;/li&gt;
&lt;li&gt;However, they slowly shifted to a paid model, eventually ceasing
development of the open source tool.&lt;/li&gt;
&lt;li&gt;Full details of the history can be
&lt;a href=&quot;https://blog.vonng.com/en/db/minio-resurrect&quot;&gt;found here&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;In the linked blog, there is reference of a community fork of Minio,
but I have had my eye on an alternative project for a while now:
&lt;a href=&quot;https://garagehq.deuxfleurs.fr&quot;&gt;Garage&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;is-garage-a-good-replacement&quot;&gt;Is Garage a good replacement?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;It actually has a different primary focus to Minio.&lt;/li&gt;
&lt;li&gt;While Minio was an ‘enterprise’ replacement for AWS S3,
providing a replicas + quorum setup.&lt;/li&gt;
&lt;li&gt;Garage is a developed to handle geo-distributed servers,
running on low-spec hardware.&lt;/li&gt;
&lt;li&gt;It focuses on resilience across sites and simpler operations
rather than classic consensus-driven design.&lt;/li&gt;
&lt;li&gt;That said, it’s still possible to run Garage as a standalone
node - what we plan to do in this post.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;automating-the-setup-of-garage-via-compose&quot;&gt;Automating the setup of Garage via Compose&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;I use &lt;strong&gt;docker compose&lt;/strong&gt; to run local software stacks
during development.&lt;/li&gt;
&lt;li&gt;Garage typically requires a bit of command line config
to work for the first time, plus as of 3rd March 2026
doesn’t have a mechanism for local container
&lt;a href=&quot;https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/1354&quot;&gt;healthchecks&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Below is the config I used to make this work for a local
development setup, entirely replacing Minio:&lt;/li&gt;
&lt;/ul&gt;

&lt;div style=&quot;position: relative&quot; id=&quot;code-container-75&quot;&gt;
  &lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code id=&quot;code-75&quot; class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;garage_data&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;networks&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;net&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;s3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; docker.io/dxflrs/garage&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;v1.3.1
    &lt;span class=&quot;token key atrule&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; ./deploy/garage.toml&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;/etc/garage.toml&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;ro
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; garage_data&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;/var/lib/garage
    &lt;span class=&quot;token key atrule&quot;&gt;networks&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;net&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;restart&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; unless&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stopped
    &lt;span class=&quot;token comment&quot;&gt;# https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/1354&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# healthcheck:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;#   test: [&quot;CMD-SHELL&quot;, &quot;/garage node health-check&quot;]&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;#   start_period: 5s&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;#   interval: 5s&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;#   timeout: 5s&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;#   retries: 10&lt;/span&gt;

  &lt;span class=&quot;token key atrule&quot;&gt;s3-init&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; docker.io/alpine&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;3.23&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;depends_on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;s3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;condition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; service_started
    &lt;span class=&quot;token key atrule&quot;&gt;network_mode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; service&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;s3
    &lt;span class=&quot;token key atrule&quot;&gt;pid&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; service&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;s3
    &lt;span class=&quot;token key atrule&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;GARAGE_ADMIN_TOKEN&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; garage&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;admin&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;token
    &lt;span class=&quot;token key atrule&quot;&gt;restart&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;on-failure:2&quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;entrypoint&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; /bin/sh
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;eu
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;c
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
        # chroot into the s3 container&#39;s filesystem so the garage binary runs
        # with its own libs/linker, while the shared network namespace keeps
        # 127.0.0.1:3901 pointing at the live Garage server
        G=&quot;chroot /proc/1/root /garage -c /etc/garage.toml&quot;&lt;/span&gt;

        &lt;span class=&quot;token comment&quot;&gt;# Wait for server&lt;/span&gt;
        for i in $$(seq 1 20); do
          if $$G node id &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;q 2&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;/dev/null; then break; fi
          echo &quot;Waiting for Garage RPC&lt;span class=&quot;token punctuation&quot;&gt;...&lt;/span&gt; ($$i/20)&quot;
          sleep 3
        done
        $$G node id &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;q &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; echo &quot;Garage RPC not ready&quot;; exit 1; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;token comment&quot;&gt;# Init garage nodes&lt;/span&gt;
        if $$G status 2&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token important&quot;&gt;&amp;amp;1&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; grep &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;q &#39;NO ROLE ASSIGNED&#39;; then
          NODE_ID=$$($$G node id &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;q &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; cut &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;c1&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;16)
          $$G layout assign &quot;$$NODE_ID&quot; &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;z local &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;c 1G
          $$G layout apply &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;version 1
        fi

        &lt;span class=&quot;token comment&quot;&gt;# Create S3 bucket&lt;/span&gt;
        $$G key import &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;yes &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;n fieldtm&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;qfield &#92;
          GK3515373e4c851ebaad366558 &#92;
          7d37d093435a41f2aab8f13c19ba067d9776c90215f56614adad6ece597dbb34 &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; true
        $$G bucket create qfield&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;cloud &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; true
        $$G bucket allow qfield&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;cloud &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;key fieldtm&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;qfield &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;read &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;write &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;owner &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt; true

        echo &quot;Garage initialized.&quot;&lt;/code&gt;&lt;/pre&gt;

  &lt;button class=&quot;code-copy &quot; data-clipboard-target=&quot;#code-75&quot; style=&quot;all: unset; background: transparent; position: absolute; top: 7.5px; right: 6px; padding-top: 3px; cursor: pointer;&quot; title=&quot;Copy&quot;&gt;
    &lt;span style=&quot;display:inline-block;background:url(https://api.iconify.design/si/copy-duotone.svg?color=%23bac2de) no-repeat center center / contain;width: 24px; height: 24px;&quot; class=&quot;code-copy-clipboard&quot;&gt;&lt;/span&gt;
  &lt;/button&gt;
&lt;/div&gt;
&lt;p&gt;for reference: &lt;code&gt;garage.toml&lt;/code&gt;&lt;/p&gt;

&lt;div style=&quot;position: relative&quot; id=&quot;code-container-79&quot;&gt;
  &lt;pre class=&quot;language-toml&quot; tabindex=&quot;0&quot;&gt;&lt;code id=&quot;code-79&quot; class=&quot;language-toml&quot;&gt;&lt;span class=&quot;token key property&quot;&gt;metadata_dir&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/var/lib/garage/meta&quot;&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;data_dir&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/var/lib/garage/data&quot;&lt;/span&gt;

&lt;span class=&quot;token key property&quot;&gt;db_engine&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;lmdb&quot;&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;replication_factor&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;compression_level&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;

&lt;span class=&quot;token key property&quot;&gt;rpc_bind_addr&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;[::]:3901&quot;&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;rpc_public_addr&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;127.0.0.1:3901&quot;&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;rpc_secret&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;1799bccfd7411eddcf9ebd316bc1f5287ad12a68094e1c6ac6abde7e6feae1ec&quot;&lt;/span&gt;

&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token table class-name&quot;&gt;s3_api&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;s3_region&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;garage&quot;&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;api_bind_addr&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;[::]:3900&quot;&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;root_domain&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;.s3.garage.localhost&quot;&lt;/span&gt;

&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token table class-name&quot;&gt;s3_web&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;bind_addr&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;[::]:3902&quot;&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;root_domain&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;.web.garage.localhost&quot;&lt;/span&gt;

&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token table class-name&quot;&gt;admin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;api_bind_addr&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;[::]:3903&quot;&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;admin_token&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;garage-admin-token&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

  &lt;button class=&quot;code-copy &quot; data-clipboard-target=&quot;#code-79&quot; style=&quot;all: unset; background: transparent; position: absolute; top: 7.5px; right: 6px; padding-top: 3px; cursor: pointer;&quot; title=&quot;Copy&quot;&gt;
    &lt;span style=&quot;display:inline-block;background:url(https://api.iconify.design/si/copy-duotone.svg?color=%23bac2de) no-repeat center center / contain;width: 24px; height: 24px;&quot; class=&quot;code-copy-clipboard&quot;&gt;&lt;/span&gt;
  &lt;/button&gt;
&lt;/div&gt;
</description><pubDate>Tue, 03 Mar 2026 00:00:00 +0000</pubDate>
      <dc:creator>Sam Woodcock</dc:creator>
      <guid>https://spwoodcock.dev/blog/2026-03-03-automate-garage-standalone/</guid>
    </item>
    <item>
      <title>Using LLMs ethically</title>
      <link>https://spwoodcock.dev/blog/2026-02-27-using-llms-ethically/</link><description>&lt;h2 id=&quot;the-bigger-picture&quot;&gt;The bigger picture&lt;/h2&gt;
&lt;p&gt;A small digression, before we dig in.&lt;/p&gt;
&lt;p&gt;Insofar as possible, this blog isn’t overly political, normally focusing on technical topics and implementations.
But caught up in the zeitgeist of the times, the subject of LLM usage is inescapable.&lt;/p&gt;
&lt;p&gt;Most people seem to care about how they can use them effectively, to maximise productivity, automate away labour, and other things.
The angle of this blog is different: I want to talk about &lt;strong&gt;ethics&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;But before we get there, we need to zoom out a little.&lt;/p&gt;
&lt;h3 id=&quot;can-we-live-life-virtuously&quot;&gt;Can we live life virtuously?&lt;/h3&gt;
&lt;p&gt;Sure, if you can afford it.&lt;/p&gt;
&lt;p&gt;I can choose vegetarianism on moral grounds because of extremely fortunate positions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I was born in a liberal democracy.&lt;/li&gt;
&lt;li&gt;I am not living in abject poverty.&lt;/li&gt;
&lt;li&gt;I have time and education to read about animal welfare and climate science.&lt;/li&gt;
&lt;li&gt;I have access to varied food options.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A child born into extreme poverty in South Sudan does not have the luxury of philosophising about the suffering of livestock. Survival comes first.&lt;/p&gt;
&lt;p&gt;Ethics is often downstream of privilege.
This raises a difficult question: should we hold everyone globally to the same ethical standards?
If ethics requires surplus time, energy, money, and education, then it is unevenly distributed by default.&lt;/p&gt;
&lt;p&gt;We should remember at all times that our moral judgments should be tempered with humility.&lt;/p&gt;
&lt;h3 id=&quot;which-hills-are-we-willing-to-die-on&quot;&gt;Which hills are we willing to die on?&lt;/h3&gt;
&lt;p&gt;We all make ethically dubious choices constantly.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Saying we “care deeply” about climate change while booking another short-haul flight.&lt;/li&gt;
&lt;li&gt;Buying from Amazon while knowing about the treatment of workers and market control issues.&lt;/li&gt;
&lt;li&gt;Using social media platforms we know are harmful to teens.&lt;/li&gt;
&lt;li&gt;Scrolling past news of a humanitarian crisis because we feel overwhelmed.&lt;/li&gt;
&lt;li&gt;Eating food we know was produced in ways we wouldn’t want to witness.&lt;/li&gt;
&lt;li&gt;Not acknowledging a homeless person that just spoke to you.&lt;/li&gt;
&lt;li&gt;Using “I’m busy” as a shield against civic responsibility.&lt;/li&gt;
&lt;li&gt;Buying cheap clothes that were probably not produced under great labour conditions.&lt;/li&gt;
&lt;li&gt;Staying silent in a conversation when speaking up would be socially costly.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;How can we live with ourselves?&lt;/p&gt;
&lt;p&gt;Because we are deeply human and life is inherently &lt;a href=&quot;https://www.goodreads.com/book/show/40988963-tragedy-the-greeks-and-us&quot;&gt;tragic&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;No one can optimise for every moral dimension simultaneously. Philosophers from Aristotle to Herbert Simon have argued that the reasonable standard is not perfection, but deliberate &lt;a href=&quot;https://en.wikipedia.org/wiki/Satisficing&quot;&gt;satisficing&lt;/a&gt;: making the best available choice given real constraints, and owning it.&lt;/p&gt;
&lt;h3 id=&quot;what-can-we-realistically-strive-for&quot;&gt;What can we realistically strive for?&lt;/h3&gt;
&lt;p&gt;So what remains?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Awareness.&lt;/li&gt;
&lt;li&gt;Deliberation.&lt;/li&gt;
&lt;li&gt;Intentional trade-offs.&lt;/li&gt;
&lt;li&gt;A willingness to accept responsibility.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We just do our best given the circumstances. Nobody is a saint.&lt;/p&gt;
&lt;p&gt;This framing matters when we start talking about LLMs.&lt;/p&gt;
&lt;h2 id=&quot;creating-an-ethical-usage-framework-for-llms&quot;&gt;Creating an ethical usage framework for LLMs&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;We need a way to reason about LLM usage that acknowledges trade-offs instead of pretending they don’t exist.&lt;/li&gt;
&lt;li&gt;While working within the tech team of an NGO, we have been deliberating this dilemma.&lt;/li&gt;
&lt;li&gt;Full details can be found here: &lt;a href=&quot;https://github.com/hotosm/docs/pull/21&quot;&gt;https://github.com/hotosm/docs/pull/21&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;getting-crushed-in-the-wheels-of-the-hype-train&quot;&gt;Getting crushed in the wheels of the hype train&lt;/h3&gt;
&lt;p&gt;There is enormous pressure right now.&lt;/p&gt;
&lt;p&gt;If you don’t use LLMs, you’re:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Inefficient.&lt;/li&gt;
&lt;li&gt;Old-fashioned.&lt;/li&gt;
&lt;li&gt;“Falling behind.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This narrative is powerful, especially in tech.&lt;/p&gt;
&lt;p&gt;And to be fair, there’s something to it. Defenders of aggressive LLM adoption have a reasonable case: these tools can meaningfully reduce time spent on low-value repetitive tasks, free up human attention for higher-order work, and (in resource-constrained organisations) stretch limited capacity further.&lt;/p&gt;
&lt;p&gt;The ethical question isn’t whether benefits can be found.
But these tools can be used (i) deliberately (ii) compulsively (iii) through acquiescence.&lt;/p&gt;
&lt;p&gt;I am advocating for more thoughtful and reflective consideration of our actions
(a tough sell, considering this isn’t the default state of humanity).&lt;/p&gt;
&lt;h3 id=&quot;energy-usage-a-red-herring&quot;&gt;Energy usage: a red herring?&lt;/h3&gt;
&lt;p&gt;Energy consumption is frequently cited as the core ethical issue.&lt;/p&gt;
&lt;p&gt;There is something to this, as data centres consume significant power.
Companies like OpenAI, Google, and Microsoft operate vast compute infrastructure to train and serve models.&lt;/p&gt;
&lt;p&gt;But we need proportionality.&lt;/p&gt;
&lt;p&gt;If you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fly multiple times per year,&lt;/li&gt;
&lt;li&gt;Own and regularly drive a car,&lt;/li&gt;
&lt;li&gt;Heat a large home,&lt;/li&gt;
&lt;li&gt;Consume meat frequently,&lt;/li&gt;
&lt;li&gt;Participate fully in consumer culture,&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;…then your LLM usage is probably not your largest environmental impact.&lt;/p&gt;
&lt;p&gt;It doesn’t make this irrelevant, but there are deeper concerns (listed in the framework discussion linked above).&lt;/p&gt;
&lt;h3 id=&quot;genuine-concerns&quot;&gt;Genuine concerns&lt;/h3&gt;
&lt;p&gt;I won’t dive too deeply into this, as the linked framework doc covers in more detail.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;digital divide&lt;/strong&gt; is perhaps the most structurally troubling issue. Access to these tools is strongly unequal. Wealthy individuals and organisations in high-income countries gain productivity advantages that compound over time, while communities without reliable internet, hardware, or the educational background to use these tools effectively fall further behind.&lt;/p&gt;
&lt;p&gt;This isn’t a new dynamic, as it mirrors historical patterns of technological colonialism, but LLMs accelerate it. For those of us in privileged positions, benefiting from these tools without any consideration of that asymmetry is a form of indifference worth highlighting.&lt;/p&gt;
&lt;p&gt;Other major concerns, explored in depth in the linked discussion, include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;IP theft&lt;/strong&gt;: both the legalities and ethical position, with neither having easy answers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Extractive contributions&lt;/strong&gt;: fracturing open-source communities through disingenuous contribution, and real burn-out risk from maintainers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Exploitation&lt;/strong&gt;: exposure of communities to labour exploitation through low-paid data labelling and content moderation work that underpins the entire industry.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;the-least-bad-option&quot;&gt;The least bad option&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;If you are using LLMs to further maximise productivity and shareholder returns and are ok with that, that’s your call to make. I’d only ask: have you made a conscious decision, or did it happen by default?&lt;/li&gt;
&lt;li&gt;If you are coerced to use LLMs by organisational pressure or market forces, then I’m truly sorry that the world is structured this way. Learn the facts: perhaps there is scope for reasoned push-back.&lt;/li&gt;
&lt;li&gt;If you are in the tricky position of working on software that are digital public goods, yet can see that LLM usage may be beneficial to the project as a whole, then make your own assessment. There is a chance that the work you do is impactful enough to offset the potential negatives engendered.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The key point here is that it’s all about choosing the &lt;strong&gt;least bad option&lt;/strong&gt; you can justify, and being &lt;strong&gt;honest about why&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;contribute-to-the-discussion&quot;&gt;Contribute to the discussion&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Open source communities around the world are hotly discussing this topic currently.&lt;/li&gt;
&lt;li&gt;If you genuinely care, and have a perspective to share, then your input is duly welcomed.&lt;/li&gt;
&lt;li&gt;There couldn’t be a more important time to weigh in on the topic.&lt;/li&gt;
&lt;/ul&gt;
</description><pubDate>Fri, 27 Feb 2026 00:00:00 +0000</pubDate>
      <dc:creator>Sam Woodcock</dc:creator>
      <guid>https://spwoodcock.dev/blog/2026-02-27-using-llms-ethically/</guid>
    </item>
    <item>
      <title>A coders approach to storing recipes</title>
      <link>https://spwoodcock.dev/blog/2026-02-22-cooklang/</link><description>&lt;h2 id=&quot;sometimes-i-like-to-cook-things&quot;&gt;Sometimes I like to cook things&lt;/h2&gt;
&lt;p&gt;This is a very short blog about something I recently discovered: Cooklang.&lt;/p&gt;
&lt;p&gt;‘Cook’ isn’t some coding jargon, nor a confession that I have gone full Walter White.&lt;/p&gt;
&lt;p&gt;I mean literally cooking. Food.&lt;/p&gt;
&lt;p&gt;Finding the best option to catalogue recipes took a bit of research,
but I finally landed on a solution I’m happy with.&lt;/p&gt;
&lt;h2 id=&quot;hosting-your-own-recipes&quot;&gt;Hosting your own recipes&lt;/h2&gt;
&lt;p&gt;When evaluating this, I cared about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Plain text formats&lt;/li&gt;
&lt;li&gt;Git-friendly&lt;/li&gt;
&lt;li&gt;Self-hostable&lt;/li&gt;
&lt;li&gt;No mandatory mobile app&lt;/li&gt;
&lt;li&gt;Longevity / open standard&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There are a few options available:&lt;/p&gt;
&lt;h3 id=&quot;mealie&quot;&gt;Mealie&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Seemed like best full integrated app out there to store recipes.&lt;/li&gt;
&lt;li&gt;It’s full database, Python API, Vue frontend solution - longevity concerns for something so simple.&lt;/li&gt;
&lt;li&gt;Seems a bit heavyweight, but probably be best option if you need multi-tenancy / logins.&lt;/li&gt;
&lt;li&gt;It also keeps the approach super simple and non-technical for other users.&lt;/li&gt;
&lt;li&gt;Tandoor is a good alternative.&lt;/li&gt;
&lt;li&gt;Both can be viewed as PWA, or also have third-party mobile app skins.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;nextcloud-cookbook&quot;&gt;Nextcloud Cookbook&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;I already run Nextcloud, so this seemed like an obvious solution.&lt;/li&gt;
&lt;li&gt;While I love the simplicity of using something I already have, it does
rely on continued maintenance of this installable plugin app.&lt;/li&gt;
&lt;li&gt;There is a third-party mobile app to view the stored recipes too, but it’s
a bit of a pain to need an app for this…&lt;/li&gt;
&lt;li&gt;Perhaps I will return to this if my requirements change, as it does have a
pretty neat ‘input URL and store the receipe’ approach - seems simple enough.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;cooklang&quot;&gt;Cooklang&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;I loved the idea behind &lt;a href=&quot;https://cooklang.org&quot;&gt;cooklang&lt;/a&gt;, a markdown
extension spec and collection of Rust libraries.&lt;/li&gt;
&lt;li&gt;The recipes are still human readable markdown, even without a specialised
parser. No app lock in. Future-proof.&lt;/li&gt;
&lt;li&gt;No app needs to be installed to view them, or catalogue them. Instead,
simply host on an existing website (i.e. this blog).&lt;/li&gt;
&lt;li&gt;To save time, I have been using an LLM to convert PDF recipes I already
have into cooklang format for posterity.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example for Crème Brûlée:&lt;/p&gt;

&lt;div style=&quot;position: relative&quot; id=&quot;code-container-139&quot;&gt;
  &lt;pre class=&quot;language-markdown&quot; tabindex=&quot;0&quot;&gt;&lt;code id=&quot;code-139&quot; class=&quot;language-markdown&quot;&gt;&lt;span class=&quot;token blockquote punctuation&quot;&gt;&gt; &gt;&lt;/span&gt; source: https://cooking.nytimes.com/recipes/9039-vanilla-creme-brulee
&lt;span class=&quot;token blockquote punctuation&quot;&gt;&gt; &gt;&lt;/span&gt; servings: 4
&lt;span class=&quot;token blockquote punctuation&quot;&gt;&gt; &gt;&lt;/span&gt; time: 1 hour

Preheat #oven{} to 165°C.

In a #saucepan{}, combine @heavy cream{470%ml}, @vanilla bean{1}, split lengthwise (or @vanilla extract{1%tsp}), and @salt{0.8%g}. Cook over low heat just until hot. Let sit for a few minutes, then discard the vanilla bean. If using vanilla extract, add it now.

In a #bowl{}, beat @egg yolks{5} and @sugar{100%g} together until light.

Stir about a quarter of the cream into the egg mixture, then pour the egg mixture back into the cream and stir to combine.

Pour into four #6-ounce ramekins{4} and place ramekins in a #baking dish{}. Fill the dish with boiling water halfway up the sides of the ramekins.

Bake for &lt;span class=&quot;token strike&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;token content&quot;&gt;{30%minutes} to &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;~&lt;/span&gt;&lt;/span&gt;{40%minutes}, or until centres are barely set. Cool completely. Refrigerate for several hours and up to a couple of days.

When ready to serve, top each custard with about @sugar for topping{1%tsp} per ramekin in a thin layer. Place ramekins under a broiler 2 to 3 inches from the heat source. Broil until sugar melts and browns, about ~{5%minutes}. Serve within two hours.&lt;/code&gt;&lt;/pre&gt;

  &lt;button class=&quot;code-copy &quot; data-clipboard-target=&quot;#code-139&quot; style=&quot;all: unset; background: transparent; position: absolute; top: 7.5px; right: 6px; padding-top: 3px; cursor: pointer;&quot; title=&quot;Copy&quot;&gt;
    &lt;span style=&quot;display:inline-block;background:url(https://api.iconify.design/si/copy-duotone.svg?color=%23bac2de) no-repeat center center / contain;width: 24px; height: 24px;&quot; class=&quot;code-copy-clipboard&quot;&gt;&lt;/span&gt;
  &lt;/button&gt;
&lt;/div&gt;
&lt;h2 id=&quot;view-the-recipes&quot;&gt;View the recipes&lt;/h2&gt;
&lt;p&gt;I added an ‘easter egg’ to this website.&lt;/p&gt;
&lt;p&gt;If you click the “bonus-content” button in the footer 5 times,
you will be able to view my hosted recipes.&lt;/p&gt;
&lt;p&gt;Enjoy!&lt;/p&gt;
</description><pubDate>Sun, 22 Feb 2026 00:00:00 +0000</pubDate>
      <dc:creator>Sam Woodcock</dc:creator>
      <guid>https://spwoodcock.dev/blog/2026-02-22-cooklang/</guid>
    </item>
    <item>
      <title>A second attempt reverse engineering drones: Potensic Atom 2</title>
      <link>https://spwoodcock.dev/blog/2026-02-04-potensic-atom-redux/</link><description>&lt;p&gt;See &lt;a href=&quot;https://spwoodcock.dev/blog/2025-05-11-potensic-atom-waypoints/&quot;&gt;the previous blog&lt;/a&gt; about hacking
the Potensic Atom 1 for full context on this latest update.&lt;/p&gt;
&lt;h2 id=&quot;the-potensic-atom-2&quot;&gt;The Potensic Atom 2&lt;/h2&gt;
&lt;p&gt;I was updated by a reader than the Potensic Atom 2 recently received a
new firmware upgrade:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In the last blog, I only considered the Potensic Atom 1, as the 2
did not support waypoint missions at the time.&lt;/li&gt;
&lt;li&gt;While waypoint missions on the Atom 1 were usable, they had many
limitations, such as no gimbal angle adjustment, or altitude adjustments,
being possible in an automated way.&lt;/li&gt;
&lt;li&gt;I had read that the new Atom 2 waypoint feature was much more capable,
so decided to give it a try.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://spwoodcock.dev/images/blog/2026-02-potensic-atom-2.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-new-waypoint-format&quot;&gt;The New Waypoint Format&lt;/h2&gt;
&lt;p&gt;I captured a few test waypoint missions and discovered:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A directory in shared storage, that is easily accessible
(thankfully not sandboxed storage!).&lt;/li&gt;
&lt;li&gt;Within this directory, we have a structure as so:&lt;/li&gt;
&lt;/ul&gt;

&lt;div style=&quot;position: relative&quot; id=&quot;code-container-47&quot;&gt;
  &lt;pre class=&quot;language-yaml&quot; tabindex=&quot;0&quot;&gt;&lt;code id=&quot;code-47&quot; class=&quot;language-yaml&quot;&gt;/sdcard/Android/data/com.ipotensic.atom/files/Waypoint/
└── 1770653566867/                  &lt;span class=&quot;token comment&quot;&gt;# One waypoint mission (unix timestamp in ms)&lt;/span&gt;
    ├── global.json                 &lt;span class=&quot;token comment&quot;&gt;# Mission-wide settings (speed, RTH, finish action)&lt;/span&gt;
    ├── 1770653566867.json          &lt;span class=&quot;token comment&quot;&gt;# Main mission file: waypoints + poi list&lt;/span&gt;
    ├── task_1770653542265.jpg      &lt;span class=&quot;token comment&quot;&gt;# Mission main thumbnail for Potensic Eve UI&lt;/span&gt;
    ├── point_1770653364152.jpg     &lt;span class=&quot;token comment&quot;&gt;# Thumbnails for Waypoints in Potensic Eve UI&lt;/span&gt;
    ├── point_1770653404823.jpg
    ├── point_1770653417852.jpg
    ├── point_1770653512308.jpg
    ├── point_1770653539697.jpg
    ├── poi_1770653390402.jpg       &lt;span class=&quot;token comment&quot;&gt;# Thumbnails for POIs in Potensic Eve UI&lt;/span&gt;
    ├── poi_1770653407306.jpg
    └── poi_1770653425134.jpg&lt;/code&gt;&lt;/pre&gt;

  &lt;button class=&quot;code-copy &quot; data-clipboard-target=&quot;#code-47&quot; style=&quot;all: unset; background: transparent; position: absolute; top: 7.5px; right: 6px; padding-top: 3px; cursor: pointer;&quot; title=&quot;Copy&quot;&gt;
    &lt;span style=&quot;display:inline-block;background:url(https://api.iconify.design/si/copy-duotone.svg?color=%23bac2de) no-repeat center center / contain;width: 24px; height: 24px;&quot; class=&quot;code-copy-clipboard&quot;&gt;&lt;/span&gt;
  &lt;/button&gt;
&lt;/div&gt;
&lt;p&gt;Let’s take a look at each file in turn.&lt;/p&gt;
&lt;h3 id=&quot;global-json&quot;&gt;global.json&lt;/h3&gt;

&lt;div style=&quot;position: relative&quot; id=&quot;code-container-54&quot;&gt;
  &lt;pre class=&quot;language-json&quot; tabindex=&quot;0&quot;&gt;&lt;code id=&quot;code-54&quot; class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;finishAction&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;BACK&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;globalHeight&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;globalHeightType&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;isOrder&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;lostAction&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;RETURN&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;speed&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5.0&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

  &lt;button class=&quot;code-copy &quot; data-clipboard-target=&quot;#code-54&quot; style=&quot;all: unset; background: transparent; position: absolute; top: 7.5px; right: 6px; padding-top: 3px; cursor: pointer;&quot; title=&quot;Copy&quot;&gt;
    &lt;span style=&quot;display:inline-block;background:url(https://api.iconify.design/si/copy-duotone.svg?color=%23bac2de) no-repeat center center / contain;width: 24px; height: 24px;&quot; class=&quot;code-copy-clipboard&quot;&gt;&lt;/span&gt;
  &lt;/button&gt;
&lt;/div&gt;
&lt;p&gt;This is not an exhaustive list of all options, but I have documented
the commands I have seen in the JSON:&lt;/p&gt;
&lt;hr&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;text-align:left&quot;&gt;Field&lt;/th&gt;
&lt;th style=&quot;text-align:left&quot;&gt;What does it mean?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;finishAction&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;BACK&lt;/code&gt;, &lt;code&gt;LAND&lt;/code&gt;, &lt;code&gt;HOVER&lt;/code&gt;, &lt;code&gt;RETURN&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;lostAction&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;RETURN&lt;/code&gt; is the only option currently&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;speed&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;Default mission speed in m/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;isOrder&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;true&lt;/code&gt; to execute waypoints in forward order, and &lt;code&gt;false&lt;/code&gt; to run in reverse order&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;globalHeight&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;Default height, if not specified per-point elsewhere. I doubt we will use this.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;globalHeightType&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;Probably 0 = relative to takeoff? This can’t be modified in Potensic Eve, so perhaps a future feature.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;mission-id-json&quot;&gt;&amp;lt;mission_id&amp;gt;.json&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;This format is custom and a bit weird.&lt;/li&gt;
&lt;/ul&gt;

&lt;div style=&quot;position: relative&quot; id=&quot;code-container-132&quot;&gt;
  &lt;pre class=&quot;language-json&quot; tabindex=&quot;0&quot;&gt;&lt;code id=&quot;code-132&quot; class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; WAYPOINTS &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; ; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; POI LIST &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

  &lt;button class=&quot;code-copy &quot; data-clipboard-target=&quot;#code-132&quot; style=&quot;all: unset; background: transparent; position: absolute; top: 7.5px; right: 6px; padding-top: 3px; cursor: pointer;&quot; title=&quot;Copy&quot;&gt;
    &lt;span style=&quot;display:inline-block;background:url(https://api.iconify.design/si/copy-duotone.svg?color=%23bac2de) no-repeat center center / contain;width: 24px; height: 24px;&quot; class=&quot;code-copy-clipboard&quot;&gt;&lt;/span&gt;
  &lt;/button&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Yes, Potensic concatenates two arrays with a semicolon 😂.&lt;/li&gt;
&lt;li&gt;The Waypoint array is as follows:&lt;/li&gt;
&lt;/ul&gt;

&lt;div style=&quot;position: relative&quot; id=&quot;code-container-145&quot;&gt;
  &lt;pre class=&quot;language-json&quot; tabindex=&quot;0&quot;&gt;&lt;code id=&quot;code-145&quot; class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;action&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;START_RECORD&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;fileName&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;point_1770653364152.jpg&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;gimbalPitch&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;-90&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;gimbalType&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; DEFINE&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;height&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;46.1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;hoverTime&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;lat&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;52.4553672&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;lng&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.302835&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;poiHeight&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;50.0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;poiLat&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;poiLng&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;poiType&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;speed&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5.0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;speedType&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;GLOBAL&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;yaw&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;-171&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;yawType&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; DEFINE&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;zoomRatio&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1.0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;zoomType&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; DEFINE
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

  &lt;button class=&quot;code-copy &quot; data-clipboard-target=&quot;#code-145&quot; style=&quot;all: unset; background: transparent; position: absolute; top: 7.5px; right: 6px; padding-top: 3px; cursor: pointer;&quot; title=&quot;Copy&quot;&gt;
    &lt;span style=&quot;display:inline-block;background:url(https://api.iconify.design/si/copy-duotone.svg?color=%23bac2de) no-repeat center center / contain;width: 24px; height: 24px;&quot; class=&quot;code-copy-clipboard&quot;&gt;&lt;/span&gt;
  &lt;/button&gt;
&lt;/div&gt;
&lt;hr&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;text-align:left&quot;&gt;Field&lt;/th&gt;
&lt;th style=&quot;text-align:left&quot;&gt;What does it mean?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;lat&lt;/code&gt;, &lt;code&gt;lng&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;Combined GPS location&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;height&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;Altitude relative to start point&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;hoverTime&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;Pause time at waypoint (seconds)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;speedType&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;GLOBAL&lt;/code&gt; = use global.json value, &lt;code&gt;DEFINE&lt;/code&gt; = per-point&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;speed&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;Travel speed to next point, if using &lt;code&gt;DEFINE&lt;/code&gt; for speedType&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;gimbalType&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;DEFINE&lt;/code&gt; = adjust per-point, I haven’t seen a global setting yet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;gimbalPitch&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;The gimbal angle for the point, between -90 and +20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;yawType&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;DEFINE&lt;/code&gt; = adjust per-point, a global setting wouldn’t be much use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;yaw&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;The direction horizontally to capture an image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;zoomType &lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;DEFINE&lt;/code&gt; or &lt;code&gt;GLOBAL&lt;/code&gt;, as with other params&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;zoomRatio&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;Adjust camera zoom. We won’t use this&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;action&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;START_RECORD&lt;/code&gt; (video), &lt;code&gt;STOP_RECORD&lt;/code&gt;, &lt;code&gt;PHOTO&lt;/code&gt;, &lt;code&gt;NONE&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;poiHeight&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;Paired with POIs, as described below. Not relevant for us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;poiLat&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;Paired with POIs, as described below. Not relevant for us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;poiLng&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;Paired with POIs, as described below. Not relevant for us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align:left&quot;&gt;&lt;code&gt;fileName&lt;/code&gt;&lt;/td&gt;
&lt;td style=&quot;text-align:left&quot;&gt;Points to a thumbnail for the Potensic Eve UI. See info below&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;ul&gt;
&lt;li&gt;Perhaps with further exploration, we might find there are additional settings,
such as using the previous waypoint value for gimbal pitch and yaw?&lt;/li&gt;
&lt;li&gt;The POI array is as follows:&lt;/li&gt;
&lt;/ul&gt;

&lt;div style=&quot;position: relative&quot; id=&quot;code-container-302&quot;&gt;
  &lt;pre class=&quot;language-json&quot; tabindex=&quot;0&quot;&gt;&lt;code id=&quot;code-302&quot; class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;fileName&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;poi_1770653390402.jpg&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;height&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;46.1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;lat&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;52.4553676&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;lng&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.3028353&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;fileName&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;poi_1770653407306.jpg&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;height&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;46.1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;lat&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;52.4553026&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;lng&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.303056&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;fileName&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;poi_1770653425134.jpg&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;height&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;46.1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;lat&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;52.4553901&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;lng&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.3025566&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

  &lt;button class=&quot;code-copy &quot; data-clipboard-target=&quot;#code-302&quot; style=&quot;all: unset; background: transparent; position: absolute; top: 7.5px; right: 6px; padding-top: 3px; cursor: pointer;&quot; title=&quot;Copy&quot;&gt;
    &lt;span style=&quot;display:inline-block;background:url(https://api.iconify.design/si/copy-duotone.svg?color=%23bac2de) no-repeat center center / contain;width: 24px; height: 24px;&quot; class=&quot;code-copy-clipboard&quot;&gt;&lt;/span&gt;
  &lt;/button&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;I &lt;em&gt;believe&lt;/em&gt; these POIs simply indicate a fixed target to keep the camera on,
so are most useful in video capture mode.&lt;/li&gt;
&lt;li&gt;For waypoint missions capturing fixed shots, they don’t seem to be needed
(simply having &lt;code&gt;[]&lt;/code&gt; for these seems to work).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;photo-thumbnails&quot;&gt;Photo Thumbnails&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;The directory also contains many thumbnails - small compressed &lt;code&gt;.jpg&lt;/code&gt;
images.&lt;/li&gt;
&lt;li&gt;These are captured by the camera when the waypoint or POI is captured,
simply providing a thumbnail in the Potensic Eve UI for each point.&lt;/li&gt;
&lt;li&gt;After testing, they are purely aesthetic, and omitting them does not matter
(we can use dummy filenames in the JSON file).&lt;/li&gt;
&lt;li&gt;I thought it could be nice to update the main mission thumbnail (e.g. to
an image of a task number, for easy identification), however, updating these
files doesn’t seem to reflect in the Potensic Eve UI (perhaps they are cached
elsewhere on the filesystem, or in the app sandbox storage?).&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;markdown-alert markdown-alert-note&quot;&gt;&lt;p class=&quot;markdown-alert-title&quot;&gt;&lt;svg class=&quot;octicon octicon-info mr-2&quot; viewBox=&quot;0 0 16 16&quot; version=&quot;1.1&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;Note&lt;/p&gt;&lt;p&gt;As a quick aside, there is also an interesting directory present:
&lt;code&gt;/sdcard/Android/data/com.ipotensic.atom/files/NoFlyZone/ATOM2/json/dist&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Inside this directory, we have a few files, such as CN.json, which contains
no-fly geometries in China.&lt;/p&gt;
&lt;p&gt;The JSON files are non-standard, containing keys such as &lt;code&gt;type&lt;/code&gt; and &lt;code&gt;shape&lt;/code&gt;,
with their a single lat/lng pair and &lt;code&gt;radius&lt;/code&gt; defined, or a polygon comprised
of lat/lng pairs.&lt;/p&gt;
&lt;p&gt;Presumably, the Potensic Atom 2 is prevented from flying in these areas
by the Potensic Eve app?&lt;/p&gt;
&lt;p&gt;I wonder if we can configure other JSON files, e.g. &lt;code&gt;FR.json&lt;/code&gt; to add
no-fly zones in say France?&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;pre-generating-waypoint-missions&quot;&gt;Pre-Generating Waypoint Missions&lt;/h2&gt;
&lt;p&gt;So to summarise above, it seems we only really need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A directory &lt;code&gt;/sdcard/Android/data/com.ipotensic.atom/files/Waypoint/&amp;lt;unix_timestamp&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;global.json&lt;/code&gt; in the directory, with global flight params.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;unix_timestamp&amp;gt;.json&lt;/code&gt; in the directory, with waypoints.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;updating-the-drone-flightplan-python-package&quot;&gt;Updating The &lt;code&gt;drone-flightplan&lt;/code&gt; Python Package&lt;/h3&gt;
&lt;p&gt;I made a &lt;a href=&quot;https://github.com/hotosm/drone-tm/pull/726&quot;&gt;PR here&lt;/a&gt; to update
the &lt;a href=&quot;https://pypi.org/project/drone-flightplan&quot;&gt;drone-flightplan&lt;/a&gt; Python
package.&lt;/p&gt;
&lt;p&gt;This package is used as part of DroneTM to generate waypoint missions
in various drone manfacturer formats.&lt;/p&gt;
&lt;h3 id=&quot;testing-it-all-works&quot;&gt;Testing It All Works&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Success! Pre-generating the waypoint missions worked well.&lt;/li&gt;
&lt;li&gt;However, the main caveat, as with DJI Fly, is that you must first
create a waypoint mission, then replace the JSON files with your
new content (being sure to keep the filename the same).&lt;/li&gt;
&lt;li&gt;Simply copying the directory to &lt;code&gt;/sdcard/Android/data/com.ipotensic.atom/files/Waypoint/&lt;/code&gt;,
results in it being deleted when Potensic Eve is loaded.&lt;/li&gt;
&lt;li&gt;I believe this is because Potensic Eve uses a similar &lt;code&gt;map.db&lt;/code&gt; SQLite
file to record the waypoint missions (as with the Potensic Atom 1 + Fly app).&lt;/li&gt;
&lt;li&gt;I didn’t bother creating a &lt;strong&gt;debuggable&lt;/strong&gt; modified Potensic Eve in order
to inspect the app sandbox storage for this, but I’m sure it’s the case.&lt;/li&gt;
&lt;li&gt;While in theory it’s possible to populate the SQLite entries in the sandbox,
in practice it’s much simpler to just modify an existing mission (avoiding
the need for the user to install a modded debuggable app).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;additional-findings&quot;&gt;Additional Findings&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;It’s possible to name a newly created waypoint mission in the app. This likely
saves into the SQLite db, and cannot be externally. Handy to mark the mission
as your primary &lt;em&gt;replace&lt;/em&gt; mission though.&lt;/li&gt;
&lt;li&gt;The gimbal angle and yaw cannot be set globally, so must be configured
at each point where they need to change.&lt;/li&gt;
&lt;li&gt;If using wayline mode in DroneTM, setting the photo internval timer via the
mission config is not possible (same as DJI, where it must be manually enabled
at the start of the flight).&lt;/li&gt;
&lt;/ul&gt;
</description><pubDate>Wed, 04 Feb 2026 00:00:00 +0000</pubDate>
      <dc:creator>Sam Woodcock</dc:creator>
      <guid>https://spwoodcock.dev/blog/2026-02-04-potensic-atom-redux/</guid>
    </item>
    <item>
      <title>HOT 2026 Q1 Tech Summary &amp; Roadmap</title>
      <link>https://spwoodcock.dev/blog/2026-02-03-hot-tech-tools-update-q1/</link><description>&lt;div style=&quot;max-width: 600px; margin: 2rem auto;&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https://spwoodcock.dev/images/blog/2026-02-hot-tech-tools-update.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;markdown-alert markdown-alert-note&quot;&gt;&lt;p class=&quot;markdown-alert-title&quot;&gt;&lt;svg class=&quot;octicon octicon-info mr-2&quot; viewBox=&quot;0 0 16 16&quot; version=&quot;1.1&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;Disclaimer&lt;/p&gt;&lt;p&gt;I am the current tech lead at HOT. I am using my personal website for this, for the sake of simplicity.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;tech-into-the-future&quot;&gt;Tech Into The Future&lt;/h2&gt;
&lt;p&gt;A lot has changed at HOT in the past few years.&lt;/p&gt;
&lt;p&gt;HOT’s central mission is to address the development and humanitarian needs of people in vulnerable areas through open mapping.&lt;/p&gt;
&lt;p&gt;HOT is building out an end-to-end mapping workflow that is an important component of this mission. The workflow includes local imagery collection (drone, camera imagery), AI assisted processing of that imagery, conducting field mapping and analyzing data for impact.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;user-friendly&lt;/strong&gt; workflow is aimed at &lt;strong&gt;allowing anyone&lt;/strong&gt;, especially those in vulnerable areas, to &lt;strong&gt;collect, access, and use map data&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id=&quot;end-to-end-mapping&quot;&gt;End-To-End Mapping&lt;/h2&gt;
&lt;p&gt;A recent example of a blog covering end-to-end can be &lt;a href=&quot;https://www.hotosm.org/en/projects/end-to-end-mapping-bali&quot;&gt;found here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In summary, we need a solution to guide users through the many steps required to create digital map data from scratch:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Imagery collection: &lt;a href=&quot;https://dronetm.org&quot;&gt;Drone Tasking Manager&lt;/a&gt; | &lt;a href=&quot;https://openaerialmap.org&quot;&gt;OpenAerialMap&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Remote mapping: &lt;a href=&quot;https://tasks.hotosm.org&quot;&gt;Tasking Manager&lt;/a&gt; | &lt;a href=&quot;https://fair.hotosm.org&quot;&gt;fAIr&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Field tagging: &lt;a href=&quot;https://fmtm.hotosm.org&quot;&gt;Field Tasking Manager&lt;/a&gt; | &lt;a href=&quot;https://chatmap.hotosm.org&quot;&gt;ChatMap&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Data insights: &lt;a href=&quot;https://export.hotosm.org&quot;&gt;Export Tool&lt;/a&gt; | &lt;a href=&quot;https://export.hotosm.org&quot;&gt;uMap&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That’s a lot of tools to build and maintain!&lt;/p&gt;
&lt;p&gt;In addition to this, we have two ongoing overarching projects:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;HOT Portal&lt;/strong&gt;: a single platform to unify all of the above tools, making for a simpler user experience. More details to come.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sustainable Infrastructure&lt;/strong&gt;: migrating all of our software into a more sustainable and resilient infrastructure on Kubernetes&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For more details, see an &lt;a href=&quot;https://www.hotosm.org/en/tools-resources/open-mapping-solutions&quot;&gt;overview of our tech tools&lt;/a&gt; and the &lt;a href=&quot;https://docs.hotosm.org&quot;&gt;tech documentation site&lt;/a&gt;, including links to roadmaps.&lt;/p&gt;
&lt;h2 id=&quot;where-are-we-now&quot;&gt;Where Are We Now?&lt;/h2&gt;
&lt;p&gt;I’m hoping that this series of quarterly blogs will help to shed more light on where we are at delivering the end-to-end solution.&lt;/p&gt;
&lt;p&gt;They should inform you about recently released features and future goals for each tool in our suite.&lt;/p&gt;
&lt;h3 id=&quot;hot-portal&quot;&gt;HOT Portal&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;This platform will provide a unified place to access all of HOT’s tools.&lt;/li&gt;
&lt;li&gt;To make them more accessible, a goal of the Portal team is to ensure the same login is used across all of the tools listed below: login once; access any tool.&lt;/li&gt;
&lt;li&gt;The Portal website is about to be launched, and will include a summary of ongoing projects for each tool, in addition to a ‘My Data’ section to see projects you are involved with.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;margin: 3rem 0 2rem 0; padding: 1rem 1.5rem; background: #F3F3F3; border-radius: 6px; border-left: 4px solid #515057; text-align: center;&quot;&gt;
  &lt;h2 style=&quot;color: #2C3038; margin: 0; font-size: 1.3rem; font-weight: 600;&quot; id=&quot;imagery-collection&quot;&gt;Imagery Collection&lt;/h2&gt;
&lt;/div&gt;
&lt;h3 id=&quot;drone-tasking-manager&quot;&gt;Drone Tasking Manager&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;DroneTM is now a stable, widely used platform for planning, uploading, and processing drone imagery in real-world mapping campaigns.&lt;/li&gt;
&lt;li&gt;While the primary workflows are capable of collecting and processing imagery, many potential improvements have been identified throughout drone mapping campaigns, giving plenty of potential room for improvement into 2026.&lt;/li&gt;
&lt;li&gt;A major goal has been improvements to imagery upload and processing: faster uploads, fewer failures, resumable, and no requirement to divide images into task areas before uploading. The work will be released on the main website Feb/Mar 2026.&lt;/li&gt;
&lt;li&gt;A new team member within Tech will be pushing forward DroneTM significantly in the months to come - stay tuned.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;openaerialmap&quot;&gt;OpenAerialMap&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;A new STAC-based API was released at the end of 2025. While not widely advertised - due to the lack of a nice UI to utilize it - the API is available to developers and power users wishing to get an early start on usage.&lt;/li&gt;
&lt;li&gt;Going forward, HOT will be working on a new imagery uploader in 2026, in addition to running a new UI to easily visualize available imagery.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;markdown-alert markdown-alert-note&quot;&gt;&lt;p class=&quot;markdown-alert-title&quot;&gt;&lt;svg class=&quot;octicon octicon-info mr-2&quot; viewBox=&quot;0 0 16 16&quot; version=&quot;1.1&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;Note&lt;/p&gt;&lt;p&gt;STAC is an interoperable standard that will facilitate serving a much larger catalogue of open imagery, with richer metadata information, and potential for federation (self-hosted imagery from a government, university, individual can all be incorporated into the larger catalogue).&lt;/p&gt;
&lt;/div&gt;
&lt;div style=&quot;margin: 3rem 0 2rem 0; padding: 1rem 1.5rem; background: #F3F3F3; border-radius: 6px; border-left: 4px solid #515057; text-align: center;&quot;&gt;
  &lt;h2 style=&quot;color: #2C3038; margin: 0; font-size: 1.3rem; font-weight: 600;&quot; id=&quot;remote-mapping&quot;&gt;Remote Mapping&lt;/h2&gt;
&lt;/div&gt;
&lt;h3 id=&quot;tasking-manager&quot;&gt;Tasking Manager&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;In 2025, we worked on a significant upgrade to improve performance, stability, and future-proof the tool into the future.&lt;/li&gt;
&lt;li&gt;The primary goal has been to freeze the current capabilities of TM, then ensure the stability of the platform as it is now.&lt;/li&gt;
&lt;li&gt;The most recent feature to be added was the &lt;a href=&quot;https://www.openstreetmap.org/user/SColchester/diary/407525&quot;&gt;Super Mapper&lt;/a&gt; badge capability.&lt;/li&gt;
&lt;li&gt;We are also close to finalising the ability to map within a ‘sandbox’, so the data is not uploaded to OSM in sensitive contexts or private datasets.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;fair&quot;&gt;fAIr&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;While fAIr has had many improvements and model iterations over the years, a major shift in its approach is underway.&lt;/li&gt;
&lt;li&gt;Instead of HOT being primarily an AI-model generator, instead we will host fAIr as a platform to connect the geoAI community/developer/engineers with humanitarian open mapping community. This will make open GeoAI models usable by the open mapping community, without the need for tech/programming skills.&lt;/li&gt;
&lt;li&gt;We have also finalised work on fAIrSwipe: enabling users to validate fAIr generated features and push them to OSM within MapSwipe.&lt;/li&gt;
&lt;li&gt;A new team member in Tech will be pushing this forward into 2026, alongside the existing fAIr team.&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;margin: 3rem 0 2rem 0; padding: 1rem 1.5rem; background: #F3F3F3; border-radius: 6px; border-left: 4px solid #515057; text-align: center;&quot;&gt;
  &lt;h2 style=&quot;color: #2C3038; margin: 0; font-size: 1.3rem; font-weight: 600;&quot; id=&quot;field-tagging&quot;&gt;Field Tagging&lt;/h2&gt;
&lt;/div&gt;
&lt;h3 id=&quot;field-tasking-manager&quot;&gt;Field Tasking Manager&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;FieldTM has continually morphed in different directions since its prototyping in late 2023.&lt;/li&gt;
&lt;li&gt;After many rounds of user feedback and continual iteration, we have settled on a simpler approach to field mapping.&lt;/li&gt;
&lt;li&gt;The primary focus will be as a project creator in downstream field mapping tools (primarily QField and ODK Collect). The area can be divided up with FieldTM, but the task assignment and progress tracking will be handled in ODK or QField.&lt;/li&gt;
&lt;li&gt;Stay tuned for a major update in the coming months that will bring a simpler user interface and user experience.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;chatmap&quot;&gt;ChatMap&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;ChatMap has gone from success to success, culminating in a very effective response to &lt;a href=&quot;https://www.hotosm.org/en/projects/hurricane-melissa-response-in-the-caribbean&quot;&gt;Hurricane Melissa in 2025&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;A re-design is about to be launched in February, including an option to see live map updates as users send messages in the field.&lt;/li&gt;
&lt;li&gt;There will also be integration with other tools (e.g. ESRI), user accounts, new options for filtering content, and a feature for saving maps directly into ChatMap (uMap will now be optional).&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;margin: 3rem 0 2rem 0; padding: 1rem 1.5rem; background: #F3F3F3; border-radius: 6px; border-left: 4px solid #515057; text-align: center;&quot;&gt;
  &lt;h2 style=&quot;color: #2C3038; margin: 0; font-size: 1.3rem; font-weight: 600;&quot; id=&quot;data-insights&quot;&gt;Data Insights&lt;/h2&gt;
&lt;/div&gt;
&lt;h3 id=&quot;export-tool&quot;&gt;Export Tool&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Export Tool is currently in a feature freeze stage, where no new active development is taking place.&lt;/li&gt;
&lt;li&gt;The website is still available and functional for those wishing to export OSM data.&lt;/li&gt;
&lt;li&gt;A review is being made of the underlying OSM data export capabilities, and if the tool could be simplified to ensure its longevity.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;div style=&quot;margin: 4rem 0 3rem 0; padding: 2.5rem 2rem; background: #F3F3F3; border-radius: 10px; border: 2px solid #C4C3C5; text-align: center;&quot;&gt;
  &lt;h2 style=&quot;margin: 0 0 0.5rem 0; font-size: 1.75rem; font-weight: 700; color: #2C3038;&quot; id=&quot;quick-summary&quot;&gt;Quick Summary&lt;/h2&gt;
  &lt;p style=&quot;margin: 0; color: #615F66; font-size: 1.05rem;&quot;&gt;Where each tool is heading in 2026&lt;/p&gt;
&lt;/div&gt;
&lt;!-- Custom CSS for quarterly tech updates --&gt;
&lt;style&gt;
.tool-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 2rem;
  margin: 2rem 0;
  max-width: 1200px;
}

@media (min-width: 768px) {
  .tool-cards {
    grid-template-columns: repeat(2, 1fr);
  }
}

.tool-card {
  background: #ffffff;
  border-radius: 10px;
  padding: 2rem;
  border: 1px solid #E1E0E1;
  box-shadow: 0 2px 8px rgba(44, 48, 56, 0.06);
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}

.tool-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 16px rgba(44, 48, 56, 0.12);
  border-color: #C4C3C5;
}

.tool-card h3 {
  margin-top: 0;
  margin-bottom: 1.5rem;
  font-size: 1.35rem;
  font-weight: 600;
  color: #2C3038;
}

.tool-section + .tool-section {
  margin-top: 1.5rem;
}

.tool-section h4 {
  margin-bottom: 0.75rem;
  font-size: 0.875rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: #828085;
}

.tool-section ul {
  padding-left: 1.25rem;
  margin: 0;
}

.tool-section li {
  margin-bottom: 0.5rem;
  line-height: 1.6;
  color: #515057;
}
&lt;/style&gt;
&lt;div class=&quot;tool-cards&quot;&gt;
  &lt;article class=&quot;tool-card&quot;&gt;
    &lt;h3 id=&quot;drone-tasking-manager-2&quot;&gt;Drone Tasking Manager&lt;/h3&gt;
    &lt;div class=&quot;tool-section&quot;&gt;
      &lt;h4 id=&quot;recent-achievements&quot;&gt;Recent achievements&lt;/h4&gt;
      &lt;ul&gt;
        &lt;li&gt;New image upload capabilities, resilient to dropped connections, with some validation checks in place&lt;/li&gt;
        &lt;li&gt;Addition to QGroundControl and MavLink flightplan output types&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/div&gt;
    &lt;div class=&quot;tool-section&quot;&gt;
      &lt;h4 id=&quot;whats-next&quot;&gt;What&#39;s next&lt;/h4&gt;
      &lt;ul&gt;
        &lt;li&gt;Addition of Potensic Atom 2 (low cost ~$500 drone)&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/div&gt;
  &lt;/article&gt;
  &lt;article class=&quot;tool-card&quot;&gt;
    &lt;h3 id=&quot;openaerialmap-2&quot;&gt;OpenAerialMap&lt;/h3&gt;
    &lt;div class=&quot;tool-section&quot;&gt;
      &lt;h4 id=&quot;recent-achievements-2&quot;&gt;Recent achievements&lt;/h4&gt;
      &lt;ul&gt;
        &lt;li&gt;Release of a new STAC-based API with richer metadata support&lt;/li&gt;
        &lt;li&gt;Foundation laid for federated imagery catalogues&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/div&gt;
    &lt;div class=&quot;tool-section&quot;&gt;
      &lt;h4 id=&quot;whats-next-2&quot;&gt;What&#39;s next&lt;/h4&gt;
      &lt;ul&gt;
        &lt;li&gt;New imagery uploader and imagery discovery interface&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/div&gt;
  &lt;/article&gt;
  &lt;article class=&quot;tool-card&quot;&gt;
    &lt;h3 id=&quot;tasking-manager-2&quot;&gt;Tasking Manager&lt;/h3&gt;
    &lt;div class=&quot;tool-section&quot;&gt;
      &lt;h4 id=&quot;recent-achievements-3&quot;&gt;Recent achievements&lt;/h4&gt;
      &lt;ul&gt;
        &lt;li&gt;Major migration to new future-proof foundation&lt;/li&gt;
        &lt;li&gt;Introduction of the Super Mapper badge&lt;/li&gt;
        &lt;li&gt;Foundations laid for &#39;sandbox&#39; mapping, with release imminent&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/div&gt;
    &lt;div class=&quot;tool-section&quot;&gt;
      &lt;h4 id=&quot;whats-next-3&quot;&gt;What&#39;s next&lt;/h4&gt;
      &lt;ul&gt;
        &lt;li&gt;Feature freeze with a focus on stability and long-term maintenance&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/div&gt;
  &lt;/article&gt;
  &lt;article class=&quot;tool-card&quot;&gt;
    &lt;h3 id=&quot;fair-2&quot;&gt;fAIr&lt;/h3&gt;
    &lt;div class=&quot;tool-section&quot;&gt;
      &lt;h4 id=&quot;recent-achievements-4&quot;&gt;Recent achievements&lt;/h4&gt;
      &lt;ul&gt;
        &lt;li&gt;fAIrSwipe: integration with MapSwipe&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/div&gt;
    &lt;div class=&quot;tool-section&quot;&gt;
      &lt;h4 id=&quot;whats-next-4&quot;&gt;What&#39;s next&lt;/h4&gt;
      &lt;ul&gt;
        &lt;li&gt;Shift in strategy toward community-driven AI workflow platform&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/div&gt;
  &lt;/article&gt;
  &lt;article class=&quot;tool-card&quot;&gt;
    &lt;h3 id=&quot;field-tasking-manager-2&quot;&gt;Field Tasking Manager&lt;/h3&gt;
    &lt;div class=&quot;tool-section&quot;&gt;
      &lt;h4 id=&quot;recent-achievements-5&quot;&gt;Recent achievements&lt;/h4&gt;
      &lt;ul&gt;
        &lt;li&gt;Addition of QField as an option for mapping app&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/div&gt;
    &lt;div class=&quot;tool-section&quot;&gt;
      &lt;h4 id=&quot;whats-next-5&quot;&gt;What&#39;s next&lt;/h4&gt;
      &lt;ul&gt;
        &lt;li&gt;Simplified user experience with new user interface&lt;/li&gt;
        &lt;li&gt;Further refinements to the QField mapping workflow, via plugin&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/div&gt;
  &lt;/article&gt;
  &lt;article class=&quot;tool-card&quot;&gt;
    &lt;h3 id=&quot;chatmap-2&quot;&gt;ChatMap&lt;/h3&gt;
    &lt;div class=&quot;tool-section&quot;&gt;
      &lt;h4 id=&quot;recent-achievements-6&quot;&gt;Recent achievements&lt;/h4&gt;
      &lt;ul&gt;
        &lt;li&gt;See map updates live as they are collected&lt;/li&gt;
        &lt;li&gt;Saving maps to revisit later&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/div&gt;
    &lt;div class=&quot;tool-section&quot;&gt;
      &lt;h4 id=&quot;whats-next-6&quot;&gt;What&#39;s next&lt;/h4&gt;
      &lt;ul&gt;
        &lt;li&gt;Expand support for visualizing in other GIS tools&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/div&gt;
  &lt;/article&gt;
  &lt;article class=&quot;tool-card&quot;&gt;
    &lt;h3 id=&quot;hot-portal-2&quot;&gt;HOT Portal&lt;/h3&gt;
    &lt;div class=&quot;tool-section&quot;&gt;
      &lt;h4 id=&quot;recent-achievements-7&quot;&gt;Recent achievements&lt;/h4&gt;
      &lt;ul&gt;
        &lt;li&gt;Unified login added to DroneTM&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/div&gt;
    &lt;div class=&quot;tool-section&quot;&gt;
      &lt;h4 id=&quot;whats-next-7&quot;&gt;What&#39;s next&lt;/h4&gt;
      &lt;ul&gt;
        &lt;li&gt;Dashboard launch and announcement&lt;/li&gt;
        &lt;li&gt;Unified login for all of our tools&lt;/li&gt;
      &lt;/ul&gt;
    &lt;/div&gt;
  &lt;/article&gt;
&lt;/div&gt;
</description><pubDate>Tue, 03 Feb 2026 00:00:00 +0000</pubDate>
      <dc:creator>Sam Woodcock</dc:creator>
      <guid>https://spwoodcock.dev/blog/2026-02-03-hot-tech-tools-update-q1/</guid>
    </item>
    <item>
      <title>Need webhooks? Just use the Postgres HTTP extension</title>
      <link>https://spwoodcock.dev/blog/2025-12-19-pgsql-http-for-webhooks/</link><description>&lt;h2 id=&quot;reducing-complexity&quot;&gt;Reducing complexity&lt;/h2&gt;
&lt;p&gt;I previously wrote a blog about
&lt;a href=&quot;https://spwoodcock.dev/blog/2025-02-golang-webhook-odk/&quot;&gt;using pg_notify and a Golang listener&lt;/a&gt;
to create a webhook based on Postgres events.&lt;/p&gt;
&lt;p&gt;I no longer believe this is the best approach, after trying out the
&lt;a href=&quot;https://github.com/pramsey/pgsql-http&quot;&gt;pgsql-http extension&lt;/a&gt;
for this use case.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;http&lt;/code&gt; extension is super simple:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Ensure the extension is installed and enabled.&lt;/li&gt;
&lt;li&gt;Create an SQL trigger for an event in your database.&lt;/li&gt;
&lt;li&gt;Call a function from your trigger that does a http call
(GET, POST, etc possible).&lt;/li&gt;
&lt;li&gt;You can send a payload to an external webhook, including
DB data, and even an &lt;code&gt;X-API-Key&lt;/code&gt; header for authentication
(or whatever you use in your API).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Now we no longer need an external service / listener running
to wait for the events. Everything can be handled entirely in
Postgres (a pattern that I am really loving recently, from cron
jobs, webhooks, ML embeddings, etc).&lt;/p&gt;
&lt;h2 id=&quot;how-this-works-in-practice&quot;&gt;How this works in practice&lt;/h2&gt;
&lt;h3 id=&quot;adding-the-extension&quot;&gt;Adding the extension&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;A custom dockerfile is very simple:&lt;/li&gt;
&lt;/ul&gt;

&lt;div style=&quot;position: relative&quot; id=&quot;code-container-50&quot;&gt;
  &lt;pre class=&quot;language-bash&quot; tabindex=&quot;0&quot;&gt;&lt;code id=&quot;code-50&quot; class=&quot;language-bash&quot;&gt;FROM postgres:18 AS pg-18

RUN &lt;span class=&quot;token function&quot;&gt;apt-get&lt;/span&gt; update &lt;span class=&quot;token punctuation&quot;&gt;&#92;&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;apt-get&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-y&lt;/span&gt; postgresql-18-http &lt;span class=&quot;token punctuation&quot;&gt;&#92;&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-rf&lt;/span&gt; /var/lib/apt/lists/*&lt;/code&gt;&lt;/pre&gt;

  &lt;button class=&quot;code-copy &quot; data-clipboard-target=&quot;#code-50&quot; style=&quot;all: unset; background: transparent; position: absolute; top: 7.5px; right: 6px; padding-top: 3px; cursor: pointer;&quot; title=&quot;Copy&quot;&gt;
    &lt;span style=&quot;display:inline-block;background:url(https://api.iconify.design/si/copy-duotone.svg?color=%23bac2de) no-repeat center center / contain;width: 24px; height: 24px;&quot; class=&quot;code-copy-clipboard&quot;&gt;&lt;/span&gt;
  &lt;/button&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;You can use the same logic to add the extension to a
Debian server install.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I actually build and publish pre-built images based on
this exact logic - simply adding the extension into the
official image
&lt;a href=&quot;https://github.com/hotosm/central-webhook/pkgs/container/postgres&quot;&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For example: &lt;code&gt;docker pull ghcr.io/hotosm/postgres:17-http&lt;/code&gt;&lt;/p&gt;
&lt;h3 id=&quot;adding-a-database-trigger&quot;&gt;Adding a database trigger&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;If you manage you own database schema, you can add functions
and triggers to your hearts content. Just add a &lt;code&gt;http_request&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;div style=&quot;position: relative&quot; id=&quot;code-container-76&quot;&gt;
  &lt;pre class=&quot;language-sql&quot; tabindex=&quot;0&quot;&gt;&lt;code id=&quot;code-76&quot; class=&quot;language-sql&quot;&gt;PERFORM http&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&#39;POST&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&#39;https://some.url.com/endpoint&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    http_headers&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;Content-Type&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;application/json&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&#39;application/json&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    webhook_payload::&lt;span class=&quot;token keyword&quot;&gt;text&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;::http_request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

  &lt;button class=&quot;code-copy &quot; data-clipboard-target=&quot;#code-76&quot; style=&quot;all: unset; background: transparent; position: absolute; top: 7.5px; right: 6px; padding-top: 3px; cursor: pointer;&quot; title=&quot;Copy&quot;&gt;
    &lt;span style=&quot;display:inline-block;background:url(https://api.iconify.design/si/copy-duotone.svg?color=%23bac2de) no-repeat center center / contain;width: 24px; height: 24px;&quot; class=&quot;code-copy-clipboard&quot;&gt;&lt;/span&gt;
  &lt;/button&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;I was adding this to an existing database schema for
&lt;a href=&quot;https://github.com/getodk/central&quot;&gt;ODK Central&lt;/a&gt;, so instead
opted to build a small Golang CLI for installing and uninstalling
the trigger, based on certain events.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For the example implementation, see
&lt;a href=&quot;https://github.com/hotosm/central-webhook&quot;&gt;this repo&lt;/a&gt;&lt;/p&gt;
</description><pubDate>Thu, 18 Dec 2025 00:00:00 +0000</pubDate>
      <dc:creator>Sam Woodcock</dc:creator>
      <guid>https://spwoodcock.dev/blog/2025-12-19-pgsql-http-for-webhooks/</guid>
    </item>
    <item>
      <title>The field mapping phoenix</title>
      <link>https://spwoodcock.dev/blog/2025-12-09-field-mapping-phoenix/</link><description>&lt;h2 id=&quot;a-bittersweet-message&quot;&gt;A bittersweet message&lt;/h2&gt;
&lt;p&gt;This blog is somewhat bittersweet, given my
&lt;a href=&quot;https://spwoodcock.dev/blog/2025-04-field-mapping-past-present-future/&quot;&gt;previous blog&lt;/a&gt;
from 8 months ago. A lot can change in that time.&lt;/p&gt;
&lt;p&gt;The NGO sector has gone through a funding crisis of sorts, in
the wake of the USAID cuts vis-à-vis the Trump administration.&lt;/p&gt;
&lt;p&gt;The Field Tasking Manager (FieldTM) project that I have worked
on-and-off for 2.5yrs is in the process of direction shift and
reduced funding.&lt;/p&gt;
&lt;p&gt;Disregarding internal politics and my initial disapproval of the
decision making around this, I have started to see this as a
positive shift for the field mapping community, for which I will
explore further below.&lt;/p&gt;
&lt;h2 id=&quot;existing-field-mapping-tools&quot;&gt;Existing field mapping tools&lt;/h2&gt;
&lt;p&gt;In my opinion, the following are the best-in-class for open-source
field data collection tools:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;QField&lt;/strong&gt;: very much geospatial focused and is map-first from the start.
There has been a big push recently from the team to improve the user
experience for less GIS-savvy users, including incorporating a data
collection model based on XLSForms. It has over 500,000 active users.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;ODK&lt;/strong&gt;: what we based FieldTM on from the start. With over 2 million
annual users, ODK is the current standard for survey based data collection
in many sectors. They are undergoing a transition to add options for
map-first workflows, instead of form/survey-first workflows. I still
love ODK and believe it is a good complementary workflow to QField.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both tools have a great community and a strong team of active developers.&lt;/p&gt;
&lt;h2 id=&quot;scope-of-the-fieldtm-project&quot;&gt;Scope of the FieldTM project&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Under my stewardship, we tried to be ambitious and make a comprehensive
all-in-one field mapping solution.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;This encompasses the creation of field mapping projects, collection
of survey data directly in the FieldTM tool, and then visualisation
of submission data in a unified place.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;In hindsight, we coupled the solution a bit too tightly to a single tool,
reducing the potential for more dynamic integration with other downstream
mapping options.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;I also failed to appreciate how difficult creating a field mapping tool is,
and have since reflected this is best left to organisations with more
resources and bigger tech communities. Particularly regarding offline
workflows and conflict reconciliation mechanisms.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;where-do-we-go-now&quot;&gt;Where do we go now?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;We are reducing the scope of FieldTM: a simple, reliable project creator
that configures and hands off to downstream tools: QField, ODK, and
potentially others.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;At this point in time, we already have most of what we need to add coordination
and tasking mechanisms &lt;strong&gt;into these downstream tools&lt;/strong&gt; rather than implementing
it ourselves in a layer above.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;This shift feels less like a retreat and more like a return to the core purpose
of FieldTM: enabling field teams to coordinate, plan, and execute mapping
campaigns using the &lt;strong&gt;tools they already know and trust&lt;/strong&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;If anything, a leaner FieldTM will incorporate all of our learnings and
missteps to date, emerging as something more resilient, maintainable, and
genuinely useful for the community it is supposed to serve.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;stay-in-the-loop&quot;&gt;Stay in the loop&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Keep on top of the ongoing changes to FieldTM via Github issues and
discussions in the &lt;a href=&quot;https://github.com/hotosm/field-tm&quot;&gt;Git repo&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;We will firstly be focusing on a solid integration with QField and
QFieldCloud as underlying mapping tools.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Call to action&lt;/strong&gt;: if you are a software developer or field mapper,
your input into the project future is very much welcome!&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</description><pubDate>Tue, 09 Dec 2025 00:00:00 +0000</pubDate>
      <dc:creator>Sam Woodcock</dc:creator>
      <guid>https://spwoodcock.dev/blog/2025-12-09-field-mapping-phoenix/</guid>
    </item>
  </channel>
</rss>