Robware Software by Rob

Welcome

Hello, I'm Rob. I'm a senior software engineer at On The Beach. Professionally my focus has been in backend development primarily using C#. In my spare time I spend my time riding bikes or making stuff, typically involving an Arduino.

This website is primarily an outlet for me to write about things which have been technically challenging, either in a professional or personal capacity, though not limited to that.

If you wish to get in contact, then get in touch via my LinkedIn profile.

Latest code commit

RepositoryPlaceMark
Branchmain
SHAad0f436bc37fa9147b11f68d9bf332439854c779
MessageFix the drag-zoom anchor to stay fixed, not track the finger (#163) Vikunja task 231. Rob, on a real phone after task 230 merged: the map still panned around while zooming, and needed to stay fixed. The handler recomputed its delta from the finger's current position on every frame, so the map continuously repositioned to hold the tapped point under the moving finger — panning, and any sideways drift in the drag panned the map sideways too. Task 230 fixed a different fault on the same line, the initial jump, and left the tracking in place. Delta is now a constant, captured once as the tap's own screen offset from the container centre and never recomputed, so the tapped point stays pinned where it was tapped while only the zoom varies with the drag. That is Rob's settled decision: the map scales about the point you double-tapped, and moving your finger changes the zoom amount only, never the position. Design record: ADR-0147, with ADR-0146 annotated as incomplete rather than wrong. Leaflet's own Map.TouchZoom does recompute its delta each frame, confirmed against the vendored bundle, because a two-finger pinch has no anchor but the fingers themselves. This gesture has one finger and one anchor decided at the tap, so Leaflet is the source of the reference point rather than of recompute-every-frame as a technique. Proven rather than reasoned. A test-only commit went first and its e2e job failed on the anchor's distance from the tap point, measuring 20 and 25 pixels of drift against a 5-pixel threshold; the fix then turned it green. Review re-derived the invariant independently and confirmed both previous formulas fail it, so these tests guard against regressing to either. They sample the anchor's screen position across the drag, including one with deliberate sideways drift — an assertion on the zoom level or the map centre would not catch this, which is exactly why the previous round's tests missed it. Merged with an admin override: branch protection requires one approving review, which Forgejo will not accept on a self-authored pull request.
Timestamp15:42:08 on Friday the 14th of August 2026

Latest Blog Post

Expanding local LLM capabilities by creating skills from documentation

I've been using OpenCode lately with locally running LLMs with LM Studio, mostly with the Qwen 3.6 35B A3B model. Compared to the likes of Claude, these local models have some significant limitations. One major limitation I was finding with Qwen is that it would always generate FluentAssertions syntax for unit tests despite having Shouldly specified. It seems to understand I want to use Shoudly, but would get stuck generating FluentAssertions syntax. Quite annoying. In order to get around this I decided I'd create a custom skill from the documentation, and then get my agent to explicitly load it.

The steps are pretty basic:

  1. Grab a copy of the git repo
  2. Navigate to the documentation folder
  3. Using OpenCode's own Build agent, tell your LLM to create a skill from the documentation

It really is that simple. I did the same thing with NSubstitute.

~/.config/opencode/skills/shouldly$ tree
.
├── references
│   ├── completeIn.md
│   ├── configuration.md
│   ├── dictionary
│   │   ├── containKeyAndValue.md
│   │   ├── containKey.md
│   │   └── README.md
│   ├── dynamicShould.md
│   ├── enumerable
│   │   ├── allBe.md
│   │   ├── contain.md
│   │   ├── empty.md
│   │   ├── have.md
│   │   ├── oneOf.md
│   │   ├── README.md
│   │   ├── shouldBe.md
│   │   ├── subsetOf.md
│   │   └── unique.md
│   ├── equality
│   │   ├── assignableTo.md
│   │   ├── exampleClasses.md
│   │   ├── greaterLessThan.md
│   │   ├── haveFlag.md
│   │   ├── inRange.md
│   │   ├── matchApproved.md
│   │   ├── notBe.md
│   │   ├── null.md
│   │   ├── ofType.md
│   │   ├── oneOf.md
│   │   ├── README.md
│   │   ├── sameAs.md
│   │   ├── shouldBe.md
│   │   ├── ShouldMatchApprovedChanged.png
│   │   ├── ShouldMatchApprovedInitial.png
│   │   └── trueFalse.md
│   ├── exceptions
│   │   ├── notThrow.md
│   │   ├── README.md
│   │   └── throw.md
│   ├── getting-started.md
│   ├── satisfyAllConditions.md
│   ├── string
│   │   ├── contain.md
│   │   ├── endWith.md
│   │   ├── match.md
│   │   ├── null.md
│   │   ├── README.md
│   │   ├── shouldBe.md
│   │   └── startWith.md
│   └── upgrade
│       └── 3to4.md
└── SKILL.md

I also recommend creating an agent to specialise in software development and explicitly requesting the skill(s) in the agent file's frontmatter:

skills: csharp-developer, shouldly, nsubstitute

I also got it to create documentation on the Seeed Studio XIAO ESP32C3 by telling it to turn the getting started page in to a skill, and also the CAN Bus expansion board, to help with my campervan control system project.

Doing this has significantly improved the accuracy of the output and reduced the number of times it gets stuck in a loop and also reduced the amount of time spent thinking.

Posted on Friday the 10th of July 2026

View more