This answer is going to go in multiple directions.
If you're looking for practice on using C to implement ways to talk to devices and peripherals, the other commenter's suggested to start with an SBC (eg Raspberry Pi, Orange Pi) or with a microcontroller dev kit (eg Arduino, MSP430, STM32) is spot-on. That gives you a bunch of attached peripherals, the datasheet that documents the register behavior, and so you can then write your own C functions that fill in and read those registers. In actual projects, you would probably use the provided libraries that already do this, but there is educational value in trying it yourself.
However, just because you write a C function named "put_char_uart0()", that isn't enough to prepare for writing full-fledged drivers, such as those in the Linux and FreeBSD kernel. This next step is more about software design, where you structure your C code so that rather than being very hardware-specific (eg for the exact UART peripheral in your microcontroller) you have code which works for a more generic UART (which abstracts general details) but is common-code to all the UARTs made by the same manufacturer. This is about creating reusable code, about creating abstraction layers, and about writing extensible code. Not all code can be reusable, not every abstraction layer is desirable, and you don't necessarily want to make your code super extensive if it starts to impact your core requirements. Good driver design means you don't ever paint yourself into a corner, and the best way to learn how to avoid this is through sheer experience.
For when you do want to write a full-and-proper driver for any particular peripheral -- maybe one day you'll create one such device, such as by using an FPGA attached via PCIe to a desktop computer -- then you'll need to work within an existing driver framework. Linux and FreeBSD drivers use a framework so that all drivers have access to what they need (system memory, I/O, helper functions, threads, etc), and then it's up to the driver author to implement the specific behavior (known in software engineering as "business logic"). It is a learned skill -- also through experience -- to work within the Linux or FreeBSD kernels. So much so that both kernels have gone through great lengths to enable userspace drivers, meaning the business logic runs as a normal program on the computer, saving the developer from having to learn the strange ways of kernel development.
And it's not like user space drivers are "cheating" in any way: they're simply another framework to write a device driver, and it's incumbent on the software engineer to learn when a kernel or user space driver is more appropriate for a given situation. I have seen kernel drivers used for sheer computational performance, but have also seen userspace drivers that were developed because nobody on that team was comfortable with kernel debugging. Those are entirely valid reasons, and software engineering is very much about selecting the right tool from a large toolbox.
Very interesting! Im no longer pursuing Meshtastic -- I'm changing over my hardware to run MeshCore now -- but this is quite a neat thing you've done here.
As an aside, if you later want to have full networking connectivity (Layer 2) using the same style of encoding the data as messages, PPP is what could do that. If transported over Meshtastic, PPP could give you a standard IP network, and on top of that, you could use SSH to securely access your remote machine.
It would probably be very slow, but PPP was also used for dial-up so it's very accommodating. The limiting factor would be whether the Meshtastic local mesh would be jammed up from so many messages.