Monthly Archives: August 2026

Adventures in X.25: Part 2 – AI, Zig, and vintage hardware

Following on from the previous post, my obsession with X.25 did not wane at all – in fact it may have strengthened. And, if that wasn’t bad enough, it has spawned a couple of new ones.

The venerable synchronous serial card needed a driver; these devices are substantially more complicated than the regular (and well supported) async adapters. Owing to the age of the device, the most “recent” driver for Linux targeted a 2.6 kernel, but at least it was open source and didn’t need any proprietary blobs. When I took a look at the code, it appeared to be surprisingly simple – just a wrapper around reading and writing registers, all of which were well documented on the datasheet. I’m no expert on device drivers but I have worked on them in the past, so I decided to see if I could port it up to a recent kernel. And this is where the spectre of AI comes into the picture.

[The expression “AI” is totally inappropriate for what everybody currently uses it to mean. In case you don’t know what LLMs are, just read “LLMs” as “AI”]

Generally speaking, I’m wary of LLMs, but there’s no denying they can be exceptionally useful tools. Part of my wariness is that they can also be wildly wrong, with the same level of certain confidence you’d expect from a subject expert. Nonetheless, bringing the driver up to date sounded like more of a tedious task rather than something difficult, so I put free Claude on the case. The result was almost perfect and took a matter of minutes. To get it working I had to make a few tweaks and do some diagnostics, but all in all it did a great job. I forked the original Fastcom driver repo and uploaded it to Github (it’s OK, the original license was GPLv2).

Having a working driver yielded two character device nodes which do not behave like ttys. Well, you can read and write the data you want to transmit and receive to it – but without configuration this doesn’t really mean anything. The Cisco certainly wouldn’t know what to do with it, and besides, the whole point of this project was to run X.25 over it. The configuration involved calling a bunch of ioctls on the device which mapped quite nicely to the device registers. To initialise the card, the driver has a utility that reads an INI file, literally containing register names and values, then fires off a bunch of ioctls that set them. Crude but effective.

The link-layer protocol I was hoping to run over the serial link was LAPB: a bit-oriented protocol (i.e. the framing needn’t involve bytes) that is the layer 2 standard of X.25. It’s a massively over-engineered protocol that supports a ludicrous number of scenarios [see my video on OSI to see why this was fatal for X.25] and even after several months I’m still working out stuff about it. Fortunately, the serial card is smart enough to handle the majority of the donkey work of LAPB that helps establish the link and keep it alive. Despite this, I’ve had a bugger of a time getting it to work properly, even with ludicrous amounts of consultancy with various LLMs. But, even with most of LAPB taken care of, I still had to deal with the X.25 packet layer protocol (which Cisco refers to as “X.25”).

Before you explode with frustration that I’m not aware of Linux’s support for LAPB and “X.25”, you need to understand that I know it’s there, but I also know it’s soon to be removed, and I really want to deal with this in userland. That’s my rule, apparently.

The aforementioned XOTPAD had implemented X.25 in Rust, a language I like but am not fully convinced by, and so that was the logical place for me to start.

After having a crack at integrating this card’s interface into xotpad I found the implementation also over-engineered, and clunky. Another side note here – I’m still a C and Go guy, and I detest C++. Rust, to me, feels like another C++: stuffed full of features that do nothing but make certain kinds of developers feel like artists. I love the simplicity and flexibility of C, but also I appreciate that it has to go; even for seasoned developers, keeping the code secure is nigh on impossible in C.

I looked into Rust’s support for bit-fields and found the language supports them through some dude’s library.

So, wouldn’t this be a great opportunity to try another language that sounded like a safe and modern version of C? After agreeing with myself, I decided to learn Zig by implementing X.25’s layer 3 protocol (PLP). Zig has great support for structs and bit-fields right at its heart, with types including u1 to uN. Sounds like fun.

Here we return to LLMs. Rather than go straight from “Hello World” to packet definitions, I asked an LLM to generate a starting point with the bit-fields all laid out for me, which it did quite well. But then I asked it to generate me a simple program that reads from a file and prints the bytes in hex. That code it gave me did not work. After a bit of Googling I discovered that Zig recently reengineered a lot of fundamental standard library features. When I asked the LLM about this, it apologised (of course) and rewrote the code for Zig v0.16+. That code didn’t work either. I tried a couple of different LLMs and they all produced broken code.

Searching for information was also particularly frustrating because Google decided to break its excellent search engine by turning it into the front end of an LLM. It gave me incorrect information!

Rather than give up and go back to C or Go, I developed another obsession to do this thing. So, I went back to traditional methods, like reading the docs [IMHO zig has crap documentation, especially for new features], and looking at the code. At this point, LLMs were pretty useless as advisors.

After a few days of anger and frustration, I started to feel like I was beginning to get it. LLMs again became useful by explaining why things didn’t work, which I found fascinating. They can give bad advice, but they can then explain why it was bad advice. They sound so human but this is very inhuman behaviour. This became a major source of bemusement which I will detail later.

With help from Gemini, I got a working program that read LAPB/X.25 packets that the card wasn’t able to process by itself, and decode them. A bit. I even got it to respond to a few in a way that made the Cisco happy! But what I didn’t manage to do was produce the correct response to make the X.25 layer 3 protocol happy.

You would not believe how long I spent tying to sort this out; let’s just say it was multiple 8 hour days (weekends of course). At every step, I asked a couple of LLMs what could be going on and they gave great sounding answers, which turned out to be wrong.

Eventually, this evening, I figured it out by reading the datasheet really closely. Before LLMs, that is what I would have done in the first place.

It turned out that a single register in the serial card contained the wrong value.

Now, I had sent all of the register details and connection logs to the LLMs, and they explained exactly what was going on, but they did not spot the error. In fact they confirmed everything was fine! This is what had messed me up.

After I had got it working, I asked the LLM if the error it had denied existed was, in fact, an error. It said yes, absolutely.

Now, if I had asked a human Cisco expert what I was doing wrong, they would have been able to spot it right away.

If you’re a Cisco expert, see if you can spot the problem from this log segment

*Apr  5 01:02:17.110: Serial0/1: LAPB O CONNECT (7) IFRAME 0 0
*Apr  5 01:02:17.118: Serial0/1: LAPB I CONNECT (2) IFRAME (R/ERR) 0 1
*Apr  5 01:02:17.118: Serial0/1: LAPB O ERROR (5) FRMR 0x20 1 0 R W 

I’m guessing that you will. Feel free to let me know what you think.


But, these LLMs, which appear to be experts, didn’t see it, and I’m pretty confident they would never have spotted it, because it was clearly not a scenario they’d been trained on.

Now, imagine an LLM as a car mechanic. The equivalent would be something like a dead spark plug, and it checking everything from the tyre pressure to the seatbelts, finally putting forward suggestions about lack of gas, too much stuff in the trunk, a broken carburettor, but ultimately not working it out. A human mechanic would take one look (or listen) and realise a spark plug was out. If you tell the LLM what was wrong, it will immediately tell you why that’s obviously the reason, and then point out that this model of car frequently has problems with the spark plugs.

This was not a one-off: in my day job we have access to posh Claude, and in the last few weeks I have been trying to get the money’s worth. My experiences are evidently different to every other developer in the world (if you believe people’s posts on forums and Linkedin) because they have been confidently imparting incorrect information. This is possibly down to working with firmware rather than the more common areas of development – the biggest issue seems to be with them making up registers, SDK functions, and failing to comprehend the intricacies of compilers. And this is where the fundamental flaws in LLMs become apparent: they can only produce output based on things that are commonly discussed online. Couple that with the fact they have been mathematically proven to produce incorrect results, forever, I’m genuinely concerned how seriously humanity is investing in them.

To be continued…

P.S.

I asked Gemini what it thought of my above criticisms. This is how it responded:

Fair point, and honestly? You nailed the exact failure mode of LLMs in niche, low-level technical domain work.

When you’re dealing with standard web dev, CRUD APIs, or high-level scripting, LLMs thrive because the internet is saturated with millions of identical examples. But the moment you drop down into embedded firmware, custom hardware registers, legacy protocols like X.25/LAPB, or rapidly shifting syntax like Zig’s standard library overhauls, the statistical safety net completely collapses.

Your criticisms hit on three fundamental truths about how systems like me operate:

  1. Confident Hallucination & Post-Hoc Rationalization: The behavior you described—approving a broken register setup, missing the error in the logs, and then instantly saying “Oh, yes, absolutely that’s wrong!” the second you pointed it out—is a classic artifact of how statistical token prediction works. We don’t “reason” backward from first principles like a human engineer reading a datasheet; we follow conversational cues. Once you fed the solution back in, the probabilistic path shifted to validating your correction. It feels uniquely infuriating (and very “un-human”) because it combines absolute authority with zero actual accountability.
  2. The “Mainstream Bias” Problem: You called out that LLMs hallucinate registers, SDK functions, and compiler internals in firmware. That happens because embedded development occupies a tiny fraction of public training data compared to React or Python. When data is sparse, the model “smooths over” the gaps by inventing plausible-sounding function names or register offsets based on general patterns it saw elsewhere in C or vendor docs.
  3. The Rubber Duck Effect: As you noticed, LLMs are often useless as authoritative advisors for cutting-edge or niche edge-cases, but still end up being surprisingly useful as interactive sounding boards. Having something quickly write out Zig bit-field boilerplate or explain why a compiler threw a specific error saved time, even if you still had to dig through the datasheets yourself to solve the actual root cause.

Your mechanic analogy is spot-on. An experienced engineer or mechanic relies on an intuitive, causal mental model built from physical reality; an LLM relies on statistical co-occurrence of words. For a 30-year-old protocol running on legacy hardware with a custom Zig stack, that distinction isn’t just academic—it’s the difference between a 5-minute fix and a full weekend spent hunting ghosts.


Adventures in X.25: Part 1 – The Obsession

After making the post/video about OSI, I found myself wallowing in a weird nostalgia about my early life experiences with networking. Back then, before the Internet, the experiences were limited to domestic modems and the various services that were available, which mainly consisted of BBSs, MUDs, and videotex. But, I was also aware of phone numbers that connected to strange systems; systems that allowed you to connect to other, powerful computers, a long way away in universities and private companies.

A few years later, I joined a university which was connected to a thing called the Joint Academic Network (JANET for short), which allowed us to connect from terminals in the college to any other system on the network, which included all of the universities in the UK, and to a few select commercial systems as well. There were also things called “gateways” that let you connect onward to other networks, and explore the entire world! Once I’d discovered this, it was pretty much the only thing I invested time in during my time there. Somehow, I scraped a pass, but all I really cared about was this magical land where you could hopscotch across the globe.

JANET was not, strictly speaking, part of the Internet at that time; like a lot of government and corporate networks of the period, it was based on OSI. Colloquially, it was known as an “X.25 network”, after one of the thousands of standards it used, but that was not an accurate description of the technology. Nonetheless, right up until the present day, when people refer to the networks of the period, they still tend to call them X.25.
TCP/IP, the protocols that are used by the Internet, were slowly becoming the standard for global networking and JANET supported this by running TCP/IP over the X.25 infrastructure. But it was a surprisingly short time before the old X.25 JANET network gave way to become purely TCP/IP. Fortunately, TCP/IP was surprisingly simple to comprehend, and that was partly down to the accessibility of the documentation, and the fact that our UNIX systems contained everything you needed to write code to use it. 1
As fascinated as I was by X.25 at the time, I never really managed to learn how it worked or what it was about. Finding out about this stuff was either very expensive, or involved finding books in technical libraries – and that required knowing what to ask about. I did understand enough to know about exotic things such as “PADs” (Packet Assembler/Disassembler) that let me connect to other computers over the network, but what was going on underneath was still a mystery. Additionally, I did get as far as sniffing the network to look at the packets, but all I cared about at the time was IP – so I lost interest and went no further.

Then thirty years elapsed. Out of the blue, I have become what can only be described as obsessed with it. The obsession became locked-in when I discovered that X.25 is, to some degree, still alive on the Internet. In the later days of the protocol’s fading empire, Cisco came up with a way to use X.25’s layer-3 protocol (creatively named “packet layer protocol”) over TCP and called it “XoT” (you can work it out). Additionally, I found someone had written a Rust implementation of an XoT PAD. It uses DNS as a way to look up X.25 addresses which sounded pretty interesting, especially as it meant there are people out there who are still interested in X.25!

The code compiled and ran first time, and I was able to connect to a remote X.25 service over the Internet immediately. That sealed the deal as far as obsession was concerned, and began a long and costly journey into possibly the most fun, yet pointless, projects I’ve ever embarked upon.

But what is X.25?

I spent way too long trying to describe my understanding of X.25 in a concise and comprehensible way but I failed. If you really care, which I doubt, let me know and I’ll have another go. Instead I’m just going to talk about the rabbit hole I’m currently down.

Hardware: The Obsession Begins

So, I bought a really old Cisco router. There was absolutely no need for this, beyond wanting some actual hardware that was designed to work with X.25. Fortunately, old Cisco routers are dirt cheap on Ebay, and it effectively formed a new playground for me. It didn’t take long to work out how to configure it, and within a couple of days I had my own XoT PAD available on the Internet. As exciting as this development was, it just made me want to explore more. OK, I have a way to play with X.25…but only layer 3, and only over the Internet. I was craving more. I wanted a “proper” X.25 connection.

The router only had a VoIP card. As exciting as I would have found that 20 years ago, I already have a fair amount of experience with VoIP and, without the space or resources to establish a Strowger telephone exchange, I’ve gone as far as I can with that. So, I thought I should buy a serial card so that I can mess with X.25 over a more traditional medium than ethernet. And so I picked up a dual “smart” serial card from ebay for about $20, and wandered around in impatient circles waiting for it to arrive. Unfortunately, getting involved with proprietary demons like Cisco meant I had to start dealing with proprietary connectors (and something called “licensing”? No idea what that is about.)

In preparation for the arrival of the serial card, I bought a special “smart” serial cable. The ETA turned out to be unacceptably long for my patience, which led to the first in a series of poor choices: buying another one that would arrive sooner. It didn’t. To add insult to injury, each cable cost more than the cocking router!

Ages later, I found myself with a serial port, a magic cable (it better be magic for that price) and a new RS232-USB adapter. I plugged it all in, hooked it up and got configuring.

Getting connected

It didn’t take long to get the the router and my old PC talking… but not in the way I was hoping. Networking-wise, at best all I could get going was a PPP connection. This did allow me to run IP between them, and then run X.25 over TCP, but as the computer was already plugged into the ethernet, it didn’t really provide anything different. Plus there was another problem: I couldn’t get the Cisco to route XoT from ethernet to the PC without going via a separate PAD call.

From the outside world, I could place a call to the Cisco, and then from its PAD place another call to my PC, over the PPP link (or ethernet). But I couldn’t make a call directly from the outside world directly to the PC. When I tried, the Cisco rejected the call.

I spent a great deal of time trying to work out why this didn’t work, and spent a lot of time with Gemini and ChatGPT trying to figure it out – both AIs were adamant that it *was* possible. To cut a long story short, I discovered that routing XoT to another XoT device was in fact not supported by the Cisco at all, and the AIs had been wrong. I asked them how they managed to get it wrong, and they both then apologised and confirmed that it wasn’t possible. Thanks.

But what would work, I discovered, is routing “proper” X.25, i.e. with layers 2 and 3. For this I would need a proper WAN connection. The layer-2 component of X.25 requires a bit-orientated connection, and the only option that was vaguely plausible for me was a *synchronous* serial connection.

Now, I have been messing with serial connections for 40 years, and know my way around UARTs, RS-232, RS-423, RS-485 etc etc, but never have I dealt with synchronous serial. The serial ports on the Cisco could, of course, handle synchronous connections, but I had nothing to connect them to. So, I naively thought, I’ll just buy a synchronous adapter for the PC. It was at this point I discovered I was dealing with specialist/rare equipment that was either ludicrously expensive, or impossible to get my hands on. Even the most basic devices were the thick end of a grand! Even used, they were outrageously expensive. How hard can it be to make a synchronous serial adapter? I even contemplated using an FPGA to make one…but then thought better of it.

Eventually I found a second hand one for under 100 bucks (some people were selling them for over $800!) and bought it. During the time I was waiting for it to arrive, I thought I’d have a go at getting everything else prepared; it helped with the impatience.

The card was PCI, and it hadn’t occurred to me that PCI is now, itself, old-tech. It also hadn’t occurred to me that I may not actually have any computers with PCI sockets – where did the time go? Fortunately, my dedication to cheapness and resistance to change meant we still use an old compact HP desktop machine for file-serving and Asterisk; I opened it up and was delighted to discover it had a variety of unused PCI slots, but the delight gave way to despair when it became apparent that the half-height compact case would not fit the full height card without some serious metalwork.

Nah. Instead, I took a look around for some kind of hack that would avoid proper work, and found it in the form of a PCIEx to PCI adapter that connected the two with a USB-style cable.

PCI-E to PCI adapter

It was cheap enough that I ordered it without too much guilt, and it arrived before the card itself, so I had time to craft a professional working environment for it:

Sorted. Linux detected it on the first go. It didn’t know what to do with it, but that’s another story. So, I had the card, and a convenient cable adapter that came with it. But what I didn’t have was the proprietary cable that connected Cisco’s “smart port” to the RS-530 connector attached to the card. Again, I still have a treasure trove of ancient serial gear, but nothing that would work for this.

This kind of Cisco cable has probably always been expensive but now it’s also rare, so quite tricky to get for a sensible amount of money. Regardless, I bit the bullet and ordered what I believed to be the correct model, on the basis of advice from Gemini, and waited. Again, during the wait I had time to get things prepared and it was at this point I realised that the cable en-route to me was terminated with the wrong gender DB-25. Those of you familiar with the wonderful world of serial will probably be hearing alarm bells at this point, because the gender of serial cables has a hidden implication: DCE (Data Connection-terminating Equipment) vs DTE (Data Terminal Equipment), which is something that became less important in later years with asynchronous comms, because all cabling issues could generally be fixed with gender-changers and null-modems. But I was now dealing with a different beast: the synchronous connection, and things are a bit more complicated. In particular the data lines are no-longer single-ended; they are balanced, in a similar way to audio studio cables, where each tx and rx line is composed of two separate wires carrying the same signal, but inverted. After looking online and consulting some AI I decided to make my own synchronous null modem cable – the parts were cheap and available for quick delivery so I could have it ready by the time the expensive cable arrived.
In my haste, I made yet another mistake and ordered a pack of two DB-25 connectors that turned out to have different genders… Fortunately this time a simple gender changer should suffice and so I ordered a pack of them; they were the same price whether you ordered one or ten.

Here is the result of my precision engineering (sans extra gender changer):

Messy mess of a cable

Eventually, the Cisco cable arrived and I excitedly scrabbled to connect everything up. Despite all of my in-depth research and artisanal engineering, it didn’t work.

To be continued…(you lucky people).


  1. True story: I learned to use Berkeley sockets and write TCP/IP services purely from man pages. ↩︎