As I said isolation is all well and good but I can easily render it useless
but it's worth the extra 5 bucks.
I use isolators for all sorts of purposes, and get ridiculed for it here!

It's mostly that wall warts without protective ground (like USB power supplies) tend to have a different 0V output potential that those that do (like desktop and laptop power supplies), because we do not have polarized (live and neutral) sockets, they're both live. So, connecting an SBC to the same wall socket than my computer/laptop does not mean they have the same 0V ("GND") reference potential; it can differ by a couple of hundred volts. Also, when you need a low-noise frontend for some stuff, even if they share a ground it is a darn nice way to isolate the power domains, stopping the digital noise from affecting the frontend – especially nice when using batteries for the frontend (for ad-hoc tooling, which is what I often do). TI ISO67xx and ISO77xx also do automatic voltage level translation, too.
And I'm not ashamed to admit that they also act as a nice barrier when I do something utterly stupid, like accidentally supplying 12V instead of 5V to the target device; that only blows the target device and the isolator (and its frontend/drivers), not my computers or the rest of the USB adapter. My-own-bacon protectors!
What is slcan?
It is the name of the Linux serial line CAN driver that supports CAN adapters using serial and USB-serial, implementing only sending and receiving frames in hardware, and talk to the host using LAWICEL ASCII protocol. Summary of the differences among various adapters
here.
You can use either the LAWICEL ASCII protocol to talk to these, or use the
slcand daemon from the
github.com/linux-can/can-utils package to bridge them to socketCAN. With
slcand in use, you can fully use them via socketCAN.
Note that you don't normally need those sources, as most Linux distros have these packaged, usually as
can-utils. The code is not architecture-specific, and should work on all architectures supported by the Linux kernel.
The advantage of my own controller would be that I can pre-disect CAN open for example and possibly carry out other functions that benefit from the real time nature of a micro controller. I don't know if time stamping is done well by cheap adapters but as I already have a body of CAN code written up I know I have all of this and some functions could be done by the micro instead of the main system having loads more messages delivered to it and having to work it all out. The main PC device would just see the actual data and status of the CAN system.
Sure! If you use a microcontroller with native USB (like my current favourite, Teensy 4.x), you can program it to use the LAWICEL ASCII protocol, or your own.
I personally prefer to use multiple USB serial endpoints, so I can use one for the datagrams in some fixed format (noting that Teensy 4.x supports USB 2.0 High Speed, so you have
plenty of bandwidth, and datagrams can be up to 512 bytes long), and the other for command and control.
I may also need the odd button or encoder input
Just add an USB HID endpoint exposing itself as a keyboard or joystick or gamepad, and the kernel will handle them, providing to your Qt application as events.
Or you could use a three USB serial endpoints, and use the third one for UI events.
Because the endpoints are separate character devices (that you can give persistent names using a trivial udev rule), it also means you can use a separate thread for each, using Qt events to propagate events to the main UI thread, or mutex-protected shared variables (just don't keep it locked for long). This makes a
huge difference in responsivity and performance, especially with multi-core SBCs. Keeping the main Qt UI thread to just the UI events and not doing any "work" in it at all makes the UI extremely responsive even on slower SBCs; and using separate threads for the different types of communications buses not only allows you to use more of the cores, but it also lets the kernel schedule the treads optimally. When data becomes available, the thread can be scheduled immediately, without waiting for the "next time slice". It is a pattern that works extremely well with Linux SBCs.
Consider an example UI + MCU, where you have a single UI pushbutton controlling a LED or a relay.
The bad way to do this is to have the UI button handler write a command to the MCU whenever it is pushed on or off.
The optimal (but over-engineered for such trivial example) way to do this is to have two worker threads. One thread sends commands to the MCU, and the other receives (status) responses from the MCU. There are three commands: Query, On, and Off. Each command is associated with a "line number", or rather an arbitrary numeric identifier chosen by the host, and will result in the MCU responding with a status block containing that identifier. For this simple example, the status block is trivial, as it only contains the identifier and the state of the LED or relay.
When the program starts, the writer thread sends a Query command. (Note that it is best to start the second worker, reader first, and only send that Query when the reader thread is ready; you can trivially use a Semaphore for this. A mutex won't do, because they need to be unlocked by the same thread that locked them.)
When the reader thread receives the status block, it uses the Qt (thread-safe) sendEvent() to send the status update event to your main UI window. In the window class implementation, you have a handler receiving the event, updating the button state to reflect whether it is on or off. Default the button state to disabled (and not reacting to clicking), because before the first state event, we don't know whether it is on or off, and which command clicking on the button would generate.
Thereafter, whenever the button is pressed, it sends an event or queues a message to the writer thread to send an On or Off command, based on its current state.
Clicking on the button does not change its state (becoming pressed or released). This means that quickly clicking on the button twice, too quickly for the MCU to respond in between, will result in two On commands, or two Off commands; never On + Off nor Off + On.
It is only when the reader thread posts the event to the UI thread that the UI thread updates the button state to reflect the device state.
One might think that having the UI button react to the press/click with such a latency (which is only a couple of milliseconds for such trivial cases, but can be human noticeable for more complex actions, especially those that require a remote response –– say a response from the engine management system) would annoy the user, but in practice, such a button tends to feel
much more real: you can
trust its state to reflect the real world, instead of the UI.
The numbering of commands and responses is useful when some commands involve remote work or timeouts. Then, the two worker threads maintain a list of pending commands with timestamps (and timeout times) –– In Linux, best use
CLOCK_BOOTTIME for real-world timeout times –– and one of them, or perhaps a third thread, does the cleanup/removal and sending the timeout/lost event to the UI thread.
In Linux, if you decrease the per-thread stack size to something reasonable (I use 4*PTHREAD_STACK_MIN), the cost of a thread is very small; basically just some RAM, even on small SBCs. Using many rather than multiplexing work to a single thread is not only more effective (because you then use more of multi-core processor), but can also simplify the code, as long as you use thread-safe inter-thread communication methods; semaphores for async state changes, mutex locks, and condvars (with an associated mutex) for lists, queues, and arrays. Qt does provide some that are thread-safe, plus mutexes, semaphores and condvars, but you can also use the pthread ones between worker threads. You only need to use the Qt thread-safe ones when talking between the UI thread and a worker thread. The Linux process schedulers are pretty darn well written, and even on a dual-core SBC having a dozen threads in a single application is no problem at all. If well written along the lines I described above, where threads block on waiting for a message or input or output (instead of polling!), they tend to be faster and use the resources more fully than single-threaded applications.
Of course, that means you need to be proficient with multithreaded/parallel processing. For me, it was just a change in mindset, releasing my expectations about the order of things, and moving to an event-based thinking model. It works exceptionally well for user interfaces, too.
Apologies for the wall of text, but I had lots to say. Hopefully at least some of it is useful! If not, I hope you skipped most of it.