I'm trying to implement a virtual serial port driver for my ham radio projects which require emulating some serial port devices and I need to have a "backend" to translate the commands received by the virtual serial port into some network-based communications. I think the best way to do that is to subclass IOUserSerial
? Based on the available docs on this class (https://developer.apple.com/documentation/serialdriverkit/iouserserial), I've done the basic implementation below. When the driver gets loaded, I can see sth like tty.serial-1000008DD
in /dev
and I can use picocom
to do I/O on the virtual serial port. And I see TxDataAvailable()
gets called every time I type a character in picocom
.
The problems are however, firstly, when TxDataAvailable()
is called, the TX buffer is all-zero so although the driver knows there is some incoming data received from picocom
, it cannot actually see the data in neither Tx/Rx buffers.
Secondly, I couldn't figure out how to notify the system that there are data available for sending back to picocom
. I call RxDataAvailable()
, but nothing appears on picocom
, and RxFreeSpaceAvailable()
never gets called back. So I think I must be doing something wrong somewhere. Really appreciate it if anyone could point out how should I fix it, many thanks!
VirtualSerialPortDriver.cpp
:
constexpr int bufferSize = 2048; using SerialPortInterface = driverkit::serial::SerialPortInterface; struct VirtualSerialPortDriver_IVars { IOBufferMemoryDescriptor *ifmd, *rxq, *txq; SerialPortInterface *interface; uint64_t rx_buf, tx_buf; bool dtr, rts; }; bool VirtualSerialPortDriver::init() { bool result = false; result = super::init(); if (result != true) { goto Exit; } ivars = IONewZero(VirtualSerialPortDriver_IVars, 1); if (ivars == nullptr) { goto Exit; } kern_return_t ret; ret = ivars->rxq->Create(kIOMemoryDirectionInOut, bufferSize, 0, &ivars->rxq); if (ret != kIOReturnSuccess) { goto Exit; } ret = ivars->txq->Create(kIOMemoryDirectionInOut, bufferSize, 0, &ivars->txq); if (ret != kIOReturnSuccess) { goto Exit; } IOAddressSegment ioaddrseg; ivars->rxq->GetAddressRange(&ioaddrseg); ivars->rx_buf = ioaddrseg.address; ivars->txq->GetAddressRange(&ioaddrseg); ivars->tx_buf = ioaddrseg.address; return true; Exit: return false; } kern_return_t IMPL(VirtualSerialPortDriver, HwActivate) { kern_return_t ret; ret = HwActivate(SUPERDISPATCH); if (ret != kIOReturnSuccess) { goto Exit; } // Loopback, set CTS to RTS, set DSR and DCD to DTR ret = SetModemStatus(ivars->rts, ivars->dtr, false, ivars->dtr); if (ret != kIOReturnSuccess) { goto Exit; } Exit: return ret; } kern_return_t IMPL(VirtualSerialPortDriver, HwDeactivate) { kern_return_t ret; ret = HwDeactivate(SUPERDISPATCH); if (ret != kIOReturnSuccess) { goto Exit; } Exit: return ret; } kern_return_t IMPL(VirtualSerialPortDriver, Start) { kern_return_t ret; ret = Start(provider, SUPERDISPATCH); if (ret != kIOReturnSuccess) { return ret; } IOMemoryDescriptor *rxq_, *txq_; ret = ConnectQueues(&ivars->ifmd, &rxq_, &txq_, ivars->rxq, ivars->txq, 0, 0, 11, 11); if (ret != kIOReturnSuccess) { return ret; } IOAddressSegment ioaddrseg; ivars->ifmd->GetAddressRange(&ioaddrseg); ivars->interface = reinterpret_cast<SerialPortInterface*>(ioaddrseg.address); SerialPortInterface &intf = *ivars->interface; ret = RegisterService(); if (ret != kIOReturnSuccess) { goto Exit; } TxFreeSpaceAvailable(); Exit: return ret; } void IMPL(VirtualSerialPortDriver, TxDataAvailable) { SerialPortInterface &intf = *ivars->interface; // Loopback // FIXME consider wrapped case size_t tx_buf_sz = intf.txPI - intf.txCI; void *src = reinterpret_cast<void *>(ivars->tx_buf + intf.txCI); // char src[] = "Hello, World!"; void *dest = reinterpret_cast<void *>(ivars->rx_buf + intf.rxPI); memcpy(dest, src, tx_buf_sz); intf.rxPI += tx_buf_sz; RxDataAvailable(); intf.txCI = intf.txPI; TxFreeSpaceAvailable(); Log("[TX Buf]: %{public}s", reinterpret_cast<char *>(ivars->tx_buf)); Log("[RX Buf]: %{public}s", reinterpret_cast<char *>(ivars->rx_buf)); // dmesg confirms both buffers are all-zero Log("[TX] txPI: %d, txCI: %d, rxPI: %d, rxCI: %d, txqoffset: %d, rxqoffset: %d, txlogsz: %d, rxlogsz: %d", intf.txPI, intf.txCI, intf.rxPI, intf.rxCI, intf.txqoffset, intf.rxqoffset, intf.txqlogsz, intf.rxqlogsz); } void IMPL(VirtualSerialPortDriver, RxFreeSpaceAvailable) { Log("RxFreeSpaceAvailable() called!"); } kern_return_t IMPL(VirtualSerialPortDriver,HwResetFIFO){ Log("HwResetFIFO() called with tx: %d, rx: %d!", tx, rx); kern_return_t ret = kIOReturnSuccess; return ret; } kern_return_t IMPL(VirtualSerialPortDriver,HwSendBreak){ Log("HwSendBreak() called!"); kern_return_t ret = kIOReturnSuccess; return ret; } kern_return_t IMPL(VirtualSerialPortDriver,HwProgramUART){ Log("HwProgramUART() called, BaudRate: %u, nD: %d, nS: %d, P: %d!", baudRate, nDataBits, nHalfStopBits, parity); kern_return_t ret = kIOReturnSuccess; return ret; } kern_return_t IMPL(VirtualSerialPortDriver,HwProgramBaudRate){ Log("HwProgramBaudRate() called, BaudRate = %d!", baudRate); kern_return_t ret = kIOReturnSuccess; return ret; } kern_return_t IMPL(VirtualSerialPortDriver,HwProgramMCR){ Log("HwProgramMCR() called, DTR: %d, RTS: %d!", dtr, rts); ivars->dtr = dtr; ivars->rts = rts; kern_return_t ret = kIOReturnSuccess; Exit: return ret; } kern_return_t IMPL(VirtualSerialPortDriver, HwGetModemStatus){ *cts = ivars->rts; *dsr = ivars->dtr; *ri = false; *dcd = ivars->dtr; Log("HwGetModemStatus() called, returning CTS=%d, DSR=%d, RI=%d, DCD=%d!", *cts, *dsr, *ri, *dcd); kern_return_t ret = kIOReturnSuccess; return ret; } kern_return_t IMPL(VirtualSerialPortDriver,HwProgramLatencyTimer){ Log("HwProgramLatencyTimer() called!"); kern_return_t ret = kIOReturnSuccess; return ret; } kern_return_t IMPL(VirtualSerialPortDriver,HwProgramFlowControl){ Log("HwProgramFlowControl() called! arg: %u, xon: %d, xoff: %d", arg, xon, xoff); kern_return_t ret = kIOReturnSuccess; Exit: return ret; }