At work, we still have a Windows 98 computer that we use for programming a few very old products that we no longer sell but still support. I tried to do some research so we could move this process onto a newer computer, but I wasn’t able to find a suitable tool. Thus, I’m stuck maintaining a Windows 98 computer until we no longer have to support the product. I do any necessary development work on a different computer, but I still need to get the program binaries onto the Windows 98 machine. The computer doesn’t have a floppy or CD drive, and Windows 98 didn’t come with a USB mass storage driver. Honestly, all of those solutions would be inconvenient for me anyway.

It’s super insecure, but you can enable file sharing on the Windows 98 computer and connect to it from Linux. Programming binaries to old microcontrollers is literally the only thing this computer is used for, so I’m not too concerned about the security pitfalls of SMB.

There are several tools designed for accessing an SMB share from Linux. Most modern desktop environments provide an interface to access Windows file shares. From the command line, I have used smbclient from Samba, which also works great in most situations. When we recently added subnets to our work network and the Windows machine was on a different subnet from my development machine, I had to come up with a solution to connect to it because it no longer showed up automatically. I didn’t want to set up a WINS server to enable cross-subnet Windows file sharing just for this one little application. I couldn’t get smbclient to connect to the Windows 98 computer properly across subnets in this scenario, so this blog post will focus on how to use Linux’s built-in CIFS support so you can use the standard “mount” command, which does work across subnets if you do it right.

First of all, you will need to ensure you have mount.cifs installed. In Ubuntu 16.04, you can type the following command to install it:

sudo apt install cifs-utils

Here is a sample mount command:

sudo mount -t cifs //192.168.2.7/MYSHARE /tmp/windows -oservern=MYSERVER,guest,vers=1.0

Here is an explanation of each parameter:

  • -t cifs
    • This specifies to the mount command that we’re trying to mount a CIFS share.
  • //192.168.2.7/MYSHARE
    • This is the SMB server and share you are trying to mount. 192.168.2.7 is the IP address of the server. MYSHARE is the name of the share on the server. Note that I am using forward slashes here, not backslashes like you would expect on Windows.
  • /tmp/windows
    • This is the local directory to mount the share on. Make sure you create the directory before trying to run this command.
  • -oservern=MYSERVER…
    • “-o” specifies options. Everything following is a comma-separated list of options:
    • servern=MYSERVER
      • This option specifies that the name of the server you are connecting to is MYSERVER. You have to specify the name of the server you’re trying to connect to if you try to mount a Windows 98 share, or else it won’t work. So this option is super important.
    • guest
      • This specifies that we’re trying to connect as a guest. Otherwise, you can use the username= and password= options. It’s probably not a good idea to use the password= option though, because it leaves your password visible in plain text; there is a credentials= option that works a lot better by letting you specify a filename containing credentials.
    • vers=1.0
      • This tells the kernel to use the SMBv1 protocol. It used to be the default before Linux 4.13 came out, but now Linux defaults to a newer protocol for security reasons.

There are many other parameters that you may need to use for various reasons. You can get information about all of the available parameters by typing this command into your terminal:

man mount.cifs

Note that prior to Linux 4.13, I did not have to specify the protocol version as 1.0. It just worked. But with Linux 4.13 and later, it defaults to a newer protocol version for security reasons. If you try to connect to a Windows 98 share without specifying protocol version 1.0, the mount command will hang and eventually fail. I was able to determine what was going on by looking at a Wireshark trace, which then led me to the mount.cifs manpage to discover how to specify an older protocol version.

I have noticed that if you try to replace an existing file, you will get an error. So in my experience, you have to delete the existing file first. This is not a big deal in my use case.

To unmount it when you’re done:

sudo umount /tmp/windows

Hope this helps someone out there!

After upgrading my Ubuntu server from 14.04 to 16.04, I noticed that legacy AppleTalk clients connecting to my netatalk (afpd) server showed an empty volume list; I could no longer access my “Home Directory” share that showed up by default in 14.04.

After some research, I discovered that the 14.04 to 16.04 upgrade brought netatalk from version 2.2.2 up to 2.2.5. It turns out that 2.2.5 has a bug which causes AppleTalk clients to not see anything from /etc/netatalk/AppleVolumes.default; only /etc/netatalk/AppleVolumes.system is read. Here is a link to the bug report I posted.

You could rebuild netatalk 2.2.5 with the latest unreleased patches as I mentioned in my first comment of the bug report above, or you can do what I did: just put your volumes into /etc/netatalk/AppleVolumes.system instead. I’m unsure if the “~” share works properly when it’s in /etc/netatalk/AppleVolumes.system, so I hardcoded it to my home directory path just in case. It works fine!

Recently I had an idea: what if I maintained a local server to host my Ubuntu install ISOs so I could load them through the PXE boot capability available on all of my computers? I would never need to create an Ubuntu install USB drive ever again — instead, I could just tell the BIOS or UEFI to boot through the network card, pick the ISO I want from a list, and boot right into it. Every x86/amd64 computer I’ve ever used (with the exception of Macs) has had an option in the BIOS/UEFI for booting through the Ethernet port, but it’s something I’ve never been able to try, and tutorials on it are usually old and/or incomplete. I finally decided to try to figure it out. I got it working after a lot of trial and error.

I found plenty of tutorials, but none of them had everything that I needed to know in order to get this setup working. Most of the tutorials that were Ubuntu-specific applied to really old releases (10.04 and below), and generally didn’t support both UEFI and BIOS simultaneously. I can verify that the process I figured out will work for both legacy BIOS and modern UEFI systems, and it will properly boot into an Ubuntu 16.04 desktop live ISO. Specifically, I have tested it with an Ubuntu MATE live ISO, but it should work with all other variants too.

dnsmasq

These instructions will use dnsmasq, which is a nice little DNS/DHCP/TFTP server program that you can install in Linux. If you are using something like DD-WRT, there is a good chance you already have dnsmasq! However, for these instructions it’s apparently important that you use the latest version as of this writing, which is 2.76. I didn’t test with earlier versions, but according to sources online, version 2.76 contains bug fixes for PXE booting with UEFI. So to start, you need to make sure you have dnsmasq 2.76. If your server is an Ubuntu 16.10 server, you can simply install dnsmasq with apt–it’s already the latest version. Otherwise, you may need to manually grab the .deb from 16.10, or compile it yourself. Keep in mind that desktop versions of Ubuntu run dnsmasq in the background for DNS, so you may have to set up something special if you want to run this server from a desktop install of Ubuntu without interfering with the existing dnsmasq. I’m not 100% sure–I did this on a server install so that wasn’t an issue.

If you already have a different DHCP server program, you can probably adapt these instructions to work with it, but you’re on your own to figure out the required syntax.

Proxy DHCP or not?

There is a decision you will have to make which will affect how you configure dnsmasq. Do you want to run dnsmasq as a normal DHCP server, or a proxy DHCP server? Let me explain the choices. As a forewarning, this may take a while to fully explain, but it’s all useful information.

When I say “normal DHCP server”, what I mean is that dnsmasq will replace your existing DHCP server. So you would set up dnsmasq to assign IP addresses to all of your computers and devices on your network, and turn off the DHCP server built into your home router. This new DHCP server provided by dnsmasq will recognize when your computer is trying to netboot, and will tell it to use TFTP to download a file from a server to boot. As you will see shortly, this is probably your best option. It’s understandable, though, if you don’t want to mess around with the DHCP server that you already have which is working just fine with no problems whatsoever. If you’re in that camp, there is another option available: a proxy DHCP server.

If you already have a DHCP server on your network that you can’t configure for PXE but you still want to use (e.g. the DHCP server in your home router), you can set up dnsmasq as a proxy DHCP server that works alongside your existing DHCP server. Your existing DHCP server will still be responsible for assigning IP addresses to all of your computers and devices, but the proxy DHCP server will provide the necessary information that PXE clients need for booting (the TFTP server IP address and file path). You will not have to change any configuration on your existing DHCP server; the proxy just provides additional information that all PXE-enabled BIOS/UEFI implementations are supposed to support. PXE was designed to support this use case, and every computer I’ve tested so far works with it just fine.

With both of these options available, the choice may seem obvious to you: use it as a proxy DHCP server! It’s a low-risk choice because you’re not messing with the configuration of anything else already on your network. In theory, this would be a great idea. In practice, though, there are problems with booting in proxy mode with UEFI. It’s not actually dnsmasq’s fault at all. It’s not the PXE protocol’s fault, and it’s not even the fault of your computer’s UEFI implementation. The fault lies in shim and grub. Let me explain further.

shim is a bootloader that serves a single basic purpose: to load grub. The reason it exists is because it’s possible to have it signed by Microsoft so that it can load on a UEFI computer that has Secure Boot enabled. So for Ubuntu, Canonical provides a version of shim signed by Microsoft that trusts binaries signed by Canonical. This signed shim can then load a version of grub that has been signed by Canonical, and grub will load a signed kernel, and so forth. The trick with shim is that it needs to know where to find grub. It attempts to auto-detect how it was loaded, and looks for a file called grubx64.efi next to itself. This works great when it was loaded from a hard drive or SSD. If it was loaded through netboot, it will try to load grubx64.efi from the same TFTP server and directory from which it was loaded, which it determines by looking at the original information provided by the DHCP server. Unfortunately, shim doesn’t currently detect this information properly when it is booted from a PXE proxy server. UEFI provides the information necessary to figure it out, but shim doesn’t currently look for it.

You could be like me and say, “who cares about Secure Boot?” Instead, just disable Secure Boot on your computers, and boot directly to grub instead of using shim. The problem is that grub has the exact same issue, and it needs to know how it was booted in order to figure out where to get grub.cfg. There is probably a way to work around this problem by embedding an intermediate grub.cfg into your grub binary (using grub-mkstandalone) that knows where to look for the real grub.cfg, but you won’t be using shim so you won’t support clients with Secure Boot enabled.

The easiest thing to do is just not use proxy mode for now if you want to support UEFI clients, especially UEFI clients with Secure Boot enabled. I will still explain how to set up proxy mode at the end of this post if you’re interested, but just know that if you do it, you will need to spend a bit more time figuring out how to get grub to grab its config file and any other supporting files over TFTP when netbooting a UEFI computer. I didn’t bother to figure this out, though, because…

There is actually a third option that may work OK for you if you were hoping you could use a proxy server: use dnsmasq as a normal DHCP server alongside your existing DHCP server, but tell it to assign IPs outside of the range that your existing DHCP server provides, and also tell it to ignore non-PXE clients. That way, it will allow your other server to handle all normal DHCP leases, but it will still be able to respond with the boot info when it sees a PXE client. Your other DHCP server will also respond to the PXE client’s boot request, but I believe the PXE client will ignore your other server because it doesn’t provide any PXE boot info. It works OK in my testing, anyway!

You may be thinking, “Doug, you’re crazy! Two DHCP servers on the same subnet?” I know it sounds weird, but it’s safe to have two DHCP servers on the same network as long as they don’t assign any overlapping IP addresses. Some people even recommend using such a setup intentionally for redundancy. For example, if your existing DHCP server assigns addresses from 192.168.1.100 to 192.168.1.199, you could safely tell your new DHCP server to assign addresses from 192.168.1.200 to 192.168.1.249 with no conflicts at all–normal DHCP clients will use whichever server responds first. It would definitely be wise to ensure your new DHCP server assigns the same default gateway and DNS servers that the existing server assigns, but if you’re configuring it to only respond to PXE clients, it’s probably not a huge deal because the PXE boot process probably isn’t going to need them on a simple home network.

Do initial setup

Regardless of what type of DHCP server you’re going to run, there is a bunch of common setup to do. Let’s get started!

TFTP directory

First of all, let’s create a TFTP directory on your server, and change it so it’s owned by you (replace “doug” with your username):

sudo mkdir /tftpboot
sudo chown doug:doug /tftpboot

Download and extract the live install ISO

Now, we need to download the install ISO. You can store it wherever you want, but the location you choose won’t end up mattering; we’re going to extract the contents of this ISO into a directory on your computer. Start by downloading the Ubuntu .iso that you want to make bootable. In my case, I downloaded Ubuntu MATE 16.04.2 LTS:

wget http://cdimage.ubuntu.com/ubuntu-mate/releases/16.04.2/release/ubuntu-mate-16.04.2-desktop-amd64.iso

After grabbing the ISO, you should mount it loopback so you can extract files from it:

mkdir /tmp/iso
sudo mount -oloop,ro ubuntu-mate-16.04.2-desktop-amd64.iso /tmp/iso

Now, we need to do several things. First of all, we need to put the kernel and initrd into your TFTP directory so TFTP clients will be able to access it. I like to make a directory in the TFTP server for each ISO I’m hosting:

mkdir /tftpboot/ubuntu-mate-16.04.2-desktop-amd64
cp /tmp/iso/casper/{vmlinuz.efi,initrd.lz} /tftpboot/ubuntu-mate-16.04.2-desktop-amd64/

Note that even though the kernel is named with a .efi extension, it will still boot a normal BIOS system just fine. The bootloader will know how to handle the file either way.

Now, you need to save a copy of all of the contents of the ISO somewhere locally that you can serve with NFS. I prefer to make a directory on the root of my drive for each ISO, but do what you want. Afterward, you can unmount the ISO.

sudo mkdir /ubuntu-mate-16.04.2-desktop-amd64
sudo cp -R /tmp/iso/* /tmp/iso/.disk /ubuntu-mate-16.04.2-desktop-amd64/
sudo umount /tmp/iso

Set up an NFS server

Set up your system as an NFS server so the PXE clients will be able to load the contents of the ISO that you extracted:

sudo apt-get install nfs-kernel-server

Add the following line to /etc/exports (I’m unsure which of the options chosen here are absolutely necessary, but they worked for me):

/ubuntu-mate-16.04.2-desktop-amd64           *(ro,sync,no_wdelay,insecure_locks,no_root_squash,insecure,no_subtree_check)

And finally, restart the NFS server so it recognizes your changes:

sudo service nfs-kernel-server restart

At this point, you are serving the contents of the ISO inside the directory /ubuntu-mate-16.04.2-desktop-amd64 with NFS, and you no longer need the .iso file. Note that some people might prefer to just keep the .iso file mounted loopback at all times instead of extracting the files from it. If you prefer to do it that way, you can probably set something up in /etc/fstab to automatically mount it, and then set up /etc/exports to share the mounted directory instead. Whatever you prefer!

Now, we need to set up some bootloaders that the PXE clients will load. We will use PXELINUX for BIOS clients and shim/grub for UEFI clients. Later on, we will set up dnsmasq to decide which bootloader to serve based on how the client identifies itself.

PXELINUX

For legacy BIOS clients, we are going to use PXELINUX. This is a very popular PXE bootloader based on SYSLINUX. It only supports BIOS clients, so this will only handle the BIOS half of things.

Start out by downloading SYSLINUX, which includes PXELINUX. Extract pxelinux.0 and a few libraries from it and place them into the TFTP server directory:

wget https://www.kernel.org/pub/linux/utils/boot/syslinux/syslinux-6.03.tar.xz
tar -xJf syslinux-6.03.tar.xz
cp syslinux-6.03/bios/core/pxelinux.0 /tftpboot/
cp syslinux-6.03/bios/com32/lib/libcom32.c32 /tftpboot/
cp syslinux-6.03/bios/com32/libutil/libutil.c32 /tftpboot/
cp syslinux-6.03/bios/com32/elflink/ldlinux/ldlinux.c32 /tftpboot/
cp syslinux-6.03/bios/com32/menu/vesamenu.c32 /tftpboot/

pxelinux.0 will be the file that is sent to BIOS PXE boot clients. After loading, it will be responsible for grabbing the other library files (ending in .c32) from the TFTP server and then it will display a list of menu choices, which we will define in a config file. Create the directory that will contain the config file:

mkdir /tftpboot/pxelinux.cfg

Now, create the file /tftpboot/pxelinux.cfg/default, which will contain the commands needed to boot into the live environment from the NFS share of the Ubuntu ISO contents. In the example below, your NFS server is 192.168.1.2:

DEFAULT vesamenu.c32
MENU TITLE Network boot

LABEL ubuntu-mate-16.04.2-desktop-amd64
  MENU LABEL ubuntu-mate-16.04.2-desktop-amd64
  KERNEL ubuntu-mate-16.04.2-desktop-amd64/vmlinuz.efi
  APPEND initrd=ubuntu-mate-16.04.2-desktop-amd64/initrd.lz root=/dev/nfs boot=casper netboot=nfs nfsroot=192.168.1.2:/ubuntu-mate-16.04.2-desktop-amd64 splash -- 

You can add as many “LABEL, MENU LABEL, KERNEL, APPEND” groups as you want to provide multiple selectable options.

shim and grub

For modern UEFI clients, we can’t use PXELINUX. Apparently, SYSLINUX also supports UEFI netbooting, so you could use it, but I read that the current version as of this writing (6.03) is very buggy with UEFI PXE booting. Instead, let’s use grub. Using grub will also allow you to use shim and support Secure Boot if you care about that.

Download Ubuntu’s version of shim-signed, and copy it into your TFTP directory. There are many ways to obtain it, such as downloading the dpkg file and extracting shim.efi.signed out of it; here is an easier way that works as of this writing:

wget https://launchpad.net/ubuntu/+archive/primary/+files/shim-signed_1.19~16.04.1.tar.xz
tar -xJf shim-signed_1.19~16.04.1.tar.xz
cp shim-signed-1.18~16.04.1/shim.efi.signed /tftpboot/shim.efi

Do the same with a signed version of grub, which Ubuntu’s signed shim will be able to load in a Secure Boot environment:

wget http://archive.ubuntu.com/ubuntu/dists/xenial/main/uefi/grub2-amd64/current/grubnetx64.efi.signed
mv grubnetx64.efi.signed /tftpboot/grubx64.efi

Create the directory that will contain grub’s config file:

mkdir /tftpboot/grub

And finally, create the file /tftpboot/grub/grub.cfg, which will contain the commands needed for booting into the Ubuntu live environment. Like before, the NFS server in this example is 192.168.1.2.

menuentry "ubuntu-mate-16.04.2-desktop-amd64" {
  linux ubuntu-mate-16.04.2-desktop-amd64/vmlinuz.efi root=/dev/nfs boot=casper netboot=nfs nfsroot=192.168.1.2:/ubuntu-mate-16.04.2-desktop-amd64 splash -- 
  initrd ubuntu-mate-16.04.2-desktop-amd64/initrd.lz
}

You can add as many “menuentry” items as you want to provide multiple selectable options.

Set up dnsmasq

We are now finished setting up the TFTP directory. All bootloaders and config files are in place. Now, we just need to set up the DHCP/TFTP server and tell it to boot BIOS clients using pxelinux.0, and UEFI clients using shim.efi. First, install dnsmasq 2.76 or greater using whatever method is most convenient for you. Based on which strategy you chose earlier, go to the subsection that matches the setup you want, and set up your dnsmasq.conf based on the given example.

Replace your existing DHCP server

Here is a (mostly) complete dnsmasq.conf file that sets up a full DHCP server. Note you will probably need to add additional options for the default gateway and DNS servers, but I’ve only supplied the parts of the config file necessary for PXE booting to work properly. Find a normal dnsmasq tutorial for info on the options to add for fully setting up a DHCP server. In the file below, it’s assumed that the server running dnsmasq is 192.168.1.2 and it is assigning IP addresses in the range of 192.168.1.50 to 192.168.1.99. Also, note that the configuration includes support for EFI32, which is a pretty uncommon architecture and will probably require a different build of grub and shim, if anyone has even made such a thing. You probably won’t have to worry about EFI32, so you can completely remove the lines referring to it if you want.

Another interesting tidbit is that this configuration doesn’t actually use PXE; it just uses the normal TFTP server and filename fields that are a part of DHCP. All PXE clients I’ve ever tested (both BIOS and UEFI) support this without any problems though. The reason I did it this way is because I discovered during testing that UEFI clients don’t seem to work properly when you actually use PXE with this setup. I never figured out why, but since it works to just set the DHCP boot server and filename, there’s no need for PXE.

# Don't function as a DNS server
port=0

# Log lots of extra information about DHCP transactions
log-dhcp

# Enable the built-in TFTP server
enable-tftp

# Set the root directory for files available via TFTP
tftp-root=/tftpboot

# Disable re-use of the DHCP servername and filename fields as extra
# option space. That's to avoid confusing some old or broken DHCP clients
dhcp-no-override

# Inspect the vendor class string and match the text to set the tag
dhcp-vendorclass=BIOS,PXEClient:Arch:00000
dhcp-vendorclass=UEFI32,PXEClient:Arch:00006
dhcp-vendorclass=UEFI,PXEClient:Arch:00007
dhcp-vendorclass=UEFI64,PXEClient:Arch:00009

# Set the boot filename based on the matching tag from the vendor class (above)
dhcp-boot=pxelinux.0,,192.168.1.2
dhcp-boot=net:UEFI32,shim.efi,,192.168.1.2
dhcp-boot=net:UEFI,shim.efi,,192.168.1.2
dhcp-boot=net:UEFI64,shim.efi,,192.168.1.2

# Give out IPs from 192.168.1.50 to 192.168.1.99
dhcp-range=192.168.1.50,192.168.1.99

If for some reason you’re directly booting into grub instead of shim, replace all instances of shim.efi with grubx64.efi instead.  This won’t work with Secure Boot enabled, so I’d definitely recommend using shim for maximum compatibility.

Set up a proxy DHCP server

This config file sets you up as a PXE proxy server. Like the above example, it is assumed that the dnsmasq server’s IP address is 192.168.1.2. I am also assuming that the other DHCP server is 192.168.1.1. Unlike the above example though, this example does actually use PXE, and it works properly with both BIOS and UEFI. Unfortunately, as I explained earlier in this post, shim and grub won’t play nicely because they won’t be able to automatically detect that they were loaded from a PXE proxy. Because of that problem, this configuration is mostly useless if you need to support UEFI. I’m just providing it for reference, especially to show the difference between setting up normal DHCP TFTP server/filename booting and actual PXE booting. If shim and grub are ever fixed to support PXE proxies properly in the future, this configuration will be much more useful.

The “Test PXE” and “Testing PXE” text can be set to whatever you want.

# Don't function as a DNS server
port=0

# Log lots of extra information about DHCP transactions
log-dhcp

# Enable the built-in TFTP server
enable-tftp

# Set the root directory for files available via TFTP
tftp-root=/tftpboot

# Disable re-use of the DHCP servername and filename fields as extra
# option space. That's to avoid confusing some old or broken DHCP clients
dhcp-no-override

# Set the boot file and server IP based on the architecture
pxe-service=x86PC, "Test PXE", pxelinux.0, 192.168.1.2
pxe-service=BC_EFI, "Test PXE", shim.efi, 192.168.1.2
pxe-service=X86-64_EFI, "Test PXE", shim.efi, 192.168.1.2

# PXE menu. The first part is the text displayed to the user. The second is
# the timeout, in seconds.
pxe-prompt="Testing PXE", 1

# We are proxying for the DHCP server 192.168.1.1
dhcp-range=192.168.1.1,proxy

Add a second DHCP server with a different IP range (and ignore non-PXE clients)

This method should allow you to preserve your existing DHCP server while still supporting PXE. Because of the problems with shim and grub mentioned above, I’d personally recommend using this method instead of the proxy method if you don’t want to mess with your existing DHCP server. Use the same configuration from the “Replace your existing DHCP server” section, but add the following lines to the config file, just after the dhcp-no-override line:

# Set a tag if it's a PXE client
dhcp-match=set:IsPXEClient,60,"PXEClient"
# Ignore non-PXE requests, to allow the existing DHCP server to handle them instead.
dhcp-ignore=tag:!IsPXEClient

And of course, update the dhcp-range setting to make sure it doesn’t overlap with your existing DHCP server’s IP address range.

As described earlier, the dnsmasq server in this configuration will ignore non-PXE clients, so your existing DHCP server will be fully in charge of assigning IP addresses to your computers and devices, with the exception of PXE boot attempts. Both your existing server and the dnsmasq server will respond to PXE requests, but your server should “win” because the existing DHCP server won’t respond to the PXE request with any of the required PXE information. I can’t guarantee this will work with every PXE client, but it seems to work fine with every computer I’ve tried to netboot.

Run dnsmasq and try it out

At this point, you have set up dnsmasq using one of the three strategies listed above. Now it’s time to try it out! Ensure that dnsmasq will start up and run. While testing, I like to leave the service stopped and instead run it manually with the -d flag so I can see all of the debug output:

sudo service dnsmasq stop
sudo dnsmasq -d

Now, tell a computer on your network to boot! Usually your computer has a boot menu that lets you pick various boot options. Sometimes the F12 key works, sometimes the Esc key works. Or, try making a VMware virtual machine and booting it without a hard drive. In many computers, you have to enable the PXE boot ROM or the “Networking Stack” in order to make it work. There’s almost always an option somewhere! Remember, this only works over Ethernet; if you have built-in Wi-Fi, it’s probably not going to work unless you have a really fancy BIOS that knows how to connect to a Wi-Fi network — I’ve never heard of such a thing.

If everything works OK, you should end up at a menu screen served by PXELINUX on a BIOS computer or grub on a UEFI computer. Pick the Ubuntu option you added to the config file earlier, and press enter to begin booting!

Sometimes, downloading the kernel and initrd can be slow. If you see a black screen and nothing’s happening, don’t panic. Open up Wireshark and see if anything’s happening, and be patient. In some cases, such as a VMware VM set up for EFI, grub takes forever to load the kernel and initrd over TFTP and sits with a black screen for a long time. dnsmasq doesn’t actually print out anything about a TFTP file transfer in progress; it only prints out a message after the file transfer succeeds. At some point, Ubuntu will load and attempt to mount the ISO contents over NFS.

Once I know that everything is working OK, then I kill the debug dnsmasq with ctrl-C and run it as a service instead (assuming I have it installed that way):

sudo service dnsmasq start

You might want to remove some of the debug options from the dnsmasq config file such as log-dhcp once you know everything is working OK, but keep in mind if you ever need to do any troubleshooting, it will be useful to turn it back on.

After installing, fix /etc/network/interfaces

When you install Ubuntu from an NFS mount, it does something weird: it sets up /etc/network/interfaces to manage your Ethernet card instead of allowing NetworkManager to handle it. It’s really easy to fix though. After booting into your new Ubuntu install, edit /etc/network/interfaces and remove the lines relating to your Ethernet card. The only two non-comment lines left when you’re finished should be:

auto lo
iface lo inet loopback

This is just a minor annoyance that is easy to solve. It’s probably possible to customize the Ubuntu install scripts to fix this automatically, but I spent way too much time figuring out how to netboot. I don’t feel like solving that tiny problem now. Post a comment on the blog if you figure out how to do it and I’ll add instructions to this post.

All done!

Not too bad, right? I hope this post can serve as a reference tool for sysadmins, developers, power users, etc. who want to netboot install modern Ubuntu systems. Maybe I didn’t search hard enough, but I couldn’t find any reference online with all of this information in one place. Let me know if you have any suggestions on ways I can improve these instructions, or if things change as time goes on! Maybe someday, shim and grub will support PXE proxy servers and I won’t have to tell people to steer clear of that option.

Special thanks

It wouldn’t be right if I didn’t give credit to the following sites that helped me figure out how to make this work and also helped me understand why things weren’t working properly:

As someone who works with a lot of development boards, most of which come with a USB-to-serial chip of some kind, I deal with an annoying problem pretty often in Linux (currently, Ubuntu 16.04). When I plug the development board into my computer, I’m blocked from doing anything with the USB serial port it creates. The first error I usually get is that permission was denied:

$ cat /dev/ttyUSB0
cat: /dev/ttyUSB0: Permission denied

This is a common problem which is easy to solve — the problem is that the port’s permissions and ownership don’t allow me to access it:

$ ls -l /dev/ttyUSB0
crw-rw---- 1 root dialout 188, 0 Oct 25 13:45 /dev/ttyUSB0

I usually just add myself to the dialout group after I install Ubuntu to get around this issue. But…this isn’t what this blog post is about. The problem is that after I fix this, log out, log back in, and plug the board back into my computer, I run into this next problem:

$ cat /dev/ttyUSB0
cat: /dev/ttyUSB0: Device or resource busy

What’s interesting about this problem is that if I wait for 15-30 seconds and try again, it works. The problem goes away on its own. I did some sleuthing with the “lsof” command and figured out why this happens. The reason is that ModemManager opens the port when you plug it in, checking to see if a modem is attached to the serial port. I personally find this really annoying.

Luckily, it’s pretty easy to fix with a udev rule. udev already comes with several default rules which prevent ModemManager from looking at some USB serial devices (see the files /lib/udev/rules.d/77-mm-usb-device-blacklist.rules and /lib/udev/rules.d/77-mm-usb-serial-adapters-greylist.rules). Following the pattern in these files, you can create your own blacklist rule which you can put in /etc/udev/rules.d. In this example, I’m going to prevent an imaginary USB-to-serial adapter with USB vendor ID 0x1234 and product ID 0x5678 from being checked by ModemManager.

Create the file /etc/udev/rules.d/99-sample-usb-blacklist.rules and enter the following content, all on a single line:

ACTION=="add", SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ATTRS{idVendor}=="1234", ATTRS{idProduct}=="5678", ENV{ID_MM_DEVICE_IGNORE}="1"

That’s all there is to it. This rule checks for a device in the subsystem “usb” with a DEVTYPE of “usb_device” and a matching USB PID/VID combination. If it finds a matching device, it sets an environment variable called ID_MM_DEVICE_IGNORE to 1, which will tell ModemManager to ignore it. Reload the udev rules with the following command:

sudo udevadm control --reload-rules

Now unplug and replug your development board (or USB-to-serial adapter or whatever it is) and you should notice that ModemManager didn’t interfere.

Luckily, this problem is not as common today as it used to be. Modern versions of ModemManager come with a default rule that “greylists” all USB devices with the FTDI vendor ID so they are only checked if you manually scan for modems–see the 77-mm-usb-serial-adapters-greylist.rules file I mentioned earlier. Most development boards that I’ve seen use FTDI chips, so that rule takes care of them. I still occasionally run into this problem with non-FTDI chips or older versions of Ubuntu, though, and the rule pattern I gave above should fix it.

Note that the ID_MM_DEVICE_IGNORE environment variable needs to be set on the USB device, and not the tty device. In other words, your rule needs to say SUBSYSTEM==”usb” and not SUBSYSTEM==”tty”. I have spent an embarrassing amount of time trying to troubleshoot a rule that didn’t work because of this requirement. If you already have another rule that sets custom permissions on the tty device, you need to create a separate rule to add the ID_MM_DEVICE_IGNORE environment variable to the actual USB device rather than the tty device.

Have you ever wanted to be able to call a C++ Qt library from Python? It turns out it’s pretty easy to do. This tutorial explains how to do it in Mac OS X, but it’s slightly out of date. I’d like to provide the world with a way to make it work with Ubuntu 14.04 out of the box. My instructions are based on that tutorial and the PySide Binding Generation Tutorial. Let’s check it out. Note: This mechanism seems to work for wrapping Qt 4 libraries. I don’t think PySide works with Qt 5, at least in Ubuntu 14.04. Keep that in mind. Everything we do will be with Qt 4.

We’re going to create a library called libdougtest. It’s going to contain a QObject subclass called TestClass. We will create a Python wrapper for it called libDougTestPy, which we will eventually be able to refer to as DougTestPy to follow Python’s library naming conventions. Let’s get started.

Install some prerequisites

There are many things you need installed:

sudo apt-get install build-essential qt-sdk python-pyside* libshiboken-dev shiboken pyside-tools libpyside-dev

I got a warning saying I should install a Phonon backend when I did this, so I also installed the package phonon-backend-gstreamer just in case.

Create a project directory

We’re going to start out by creating a directory for this entire project. Let’s call it  dougtest_project:

mkdir dougtest_project
cd dougtest_project

All work we do will be inside of this directory. The reason I’m naming it dougtest_project instead of just dougtest is because I’m going to create another directory inside of it called dougtest for the C++ library itself.

Create libdougtest

Now, let’s create the good old C++ library that we want to wrap. This is pretty easy, since you just follow the standard Qt way of creating a library. For simplicity, I’ve provided a barebones library containing a single class. We are going to create a directory inside of dougtest_project called dougtest which will be the home for this library:

mkdir dougtest

First of all, create the project file:

dougtest/dougtest.pro:

QT += core
TARGET = dougtest
TEMPLATE = lib

HEADERS += testclass.h
SOURCES += testclass.cpp

Now, create the header and source files:

dougtest/testclass.h:

#ifndef TESTCLASS_H
#define TESTCLASS_H

#include <QObject>

class TestClass : public QObject
{
    Q_OBJECT
public:
    TestClass(QObject *parent = 0);
    virtual ~TestClass();
 
    void doSomething();

private:
    int value;
};

#endif

dougtest/testclass.cpp:

#include "testclass.h"

TestClass::TestClass(QObject *parent) :
    QObject(parent),
    value(0)
{
 
}

TestClass::~TestClass()
{
}

void TestClass::doSomething()
{
    value++;
    qDebug("Testing: %d", value);
}

This concludes the creation of the library. Let’s make sure it compiles:

cd dougtest
qmake-qt4
make

If you’re successful, you should see libdougtest.so.1.0.0 with several symlinks. Change back to the main directory:

cd ..

OK, we have successfully created the library that we’re going to wrap. We haven’t seen anything special yet; the fun is coming now.

Create files for shiboken

Next, we’re going to create some files that shiboken will use during the wrapping process. shiboken is a binding generator for creating Python bindings for C++ libraries, which is exactly what we want.

mkdir data

Create the following two files:

data/global.h:

#undef QT_NO_STL
#undef QT_NO_STL_WCHAR

#ifndef NULL
#define NULL 0
#endif

#include <QtCore/QtCore>

#include <pyside_global.h>
#include <testclass.h>

data/typesystem.xml:

<?xml version="1.0"?>
<typesystem package="DougTestPy">
    <load-typesystem name="typesystem_core.xml" generate="no"/>
    <object-type name="TestClass"/>
</typesystem>

These files specify the classes to wrap, and will be passed as parameters to shiboken, as you will see shortly.

Create wrapped library project file

Next, we need to create a Qt project that will generate the wrapper library. Create the directory DougTestPy (inside of the main dougtest_project directory):

mkdir DougTestPy

Create the file DougTestPy/DougTestPy.pro:

TEMPLATE = lib
QT += core

# Need to have access to header files from the library being wrapped.
INCLUDEPATH += ../dougtest

# PySide include paths
QMAKE_CXXFLAGS += $$system(pkg-config --cflags pyside)
INCLUDEPATH += $$system(pkg-config --variable=includedir pyside)/QtCore

# PySide dependencies
LIBS += $$system(pkg-config --libs pyside)

# Link against the library being wrapped
LIBS += -L../bin/ -ldougtest

# Generate the wrapper library in the bin directory
TARGET = ../bin/DougTestPy

# Here are all the sources that go with it
SOURCES += \
    DougTestPy/dougtestpy_module_wrapper.cpp \
    DougTestPy/testclass_wrapper.cpp

This script may be confusing because it refers to source files that we haven’t created yet. This is the naming scheme that shiboken will use when we use it to create the wrapper code.

Create the build script and run it

We are finally ready to create a shell script that will call shiboken and do everything that needs to be done. I hope this isn’t too complicated. It’s very possible that a Makefile would work too, but this is what I have working.

Create the file build.sh inside the main dougtest_project directory:

#!/bin/sh

# Absolute path to this script, e.g. /home/user/bin/foo.sh
SCRIPT=$(readlink -f "$0")
# Absolute path this script is in, e.g. /home/user/bin
SCRIPTPATH=$(dirname "$SCRIPT")

WRAPPER_NAME="DougTestPy"
WRAPPED_NAME="dougtest"

QT_INC=$(pkg-config --variable=includedir QtCore)/..
QTCORE_INC=$(pkg-config --variable=includedir QtCore)
PYSIDE_INC=$(pkg-config --variable=includedir pyside)
QTTYPESYSTEM=$(pkg-config --variable=typesystemdir pyside)

# I'm assuming the wrapped library is built in-place; if not,
# change WRAPPED_BIN_DIR to the location of the final binaries.
WRAPPED_SRC_DIR="${SCRIPTPATH}/${WRAPPED_NAME}"
WRAPPED_BIN_DIR="${SCRIPTPATH}/${WRAPPED_NAME}"

cd $SCRIPTPATH

# Clean up the old build to ensure we start fresh. If we don't do this,
# old stale stuff can be left behind, which can cause crashes in the
# library after you make changes.
rm -rf ./bin ./${WRAPPER_NAME}/Makefile ./${WRAPPER_NAME}/${WRAPPER_NAME}

# Copy the latest C++ library build into the bin directory.
mkdir -p bin
cp -R ${WRAPPED_BIN_DIR}/lib${WRAPPED_NAME}* ./bin/

# Now work inside the wrapper directory
cd $WRAPPER_NAME

# Create the wrappers for the library
shiboken ../data/global.h \
    --include-paths=$WRAPPED_SRC_DIR:$QT_INC:$QTCORE_INC:$PYSIDE_INC \
    --typesystem-paths=../data:$QTTYPESYSTEM \
    --output-directory=. \
    --avoid-protected-hack \
    ../data/typesystem.xml
if [ $? -ne 0 ]; then
    echo "shiboken returned error. Something is shibroken :-("
    exit 1
fi

# Build the python wrapper library
qmake-qt4
if [ $? -ne 0 ]; then
    echo "qmake-qt4 returned error."
    exit 1
fi

make
if [ $? -ne 0 ]; then
    echo "make returned error."
    exit 1
fi

# Back into the directory containing the script
cd ..

# Make meaningful symlink so Python can use the generated library
rm -rf ./bin/${WRAPPER_NAME}.so
ln -s lib${WRAPPER_NAME}.so ./bin/${WRAPPER_NAME}.so

OK, you’re ready to run this build script:

sh build.sh

Assuming everything succeeds, you should end up with a bin directory with the following contents:

  • DougTestPy.so (a symlink to libDougTestPy.so)
  • libDougTestPy.so, libDougTestPy.so.1, libDougTestPy.so.1.0, and libDougTestPy.so.1.0.0
  • libdougtest.so, libdougtest.so.1, libdougtest.so.1.0, and libdougtest.so.1.0.0

The symlink has to be created because the build process generates a library that begins with “lib”, but Python expects the library to not be prefixed with “lib”. Maybe there is a way to fix it so that qmake doesn’t add the “lib” prefix, but I couldn’t figure out how after a quick search, and this did the trick, so I let it go.

Test the library

We’re ready to test it out. Create the file test.py in the main dougtest_project directory:

import sys
sys.path.append("bin")
from PySide.QtCore import *
from DougTestPy import *

# Create a TestClass instance
tc = TestClass()

print "Created test class"

# Test the TestClass
tc.doSomething()

print "Called test class function"

tc.doSomething()

print "Called test class function again"

Now, run it. Note that I had to add the “bin” directory to Python’s path in order to use the library. We’re not done with library trickery yet though: libDougTestPy.so depends on libdougtest.so. So libdougtest.so needs to be in the system library path. The easiest way to do this temporarily is to add the bin directory to LD_LIBRARY_PATH:

export LD_LIBRARY_PATH=bin/

For a more permanent solution, you could put libdougtest.so* into /usr/local/lib and run “sudo ldconfig” to eliminate the need for that hack.

Now, run the test script:

python test.py

You should get this output:

Created test class
Testing: 1
Called test class function
Testing: 2
Called test class function again

Conclusion

I hope this was helpful. It’s really not that bad; it’s just that it requires a lot of manual setup. It would be nice if there was something that could automatically generate all the junk we had to make by hand. Maybe such a tool already exists and I’m wasting my time. Either way, I think this is interesting. Thanks again to the people who wrote the tutorials I linked above.

Note that if you use libraries other than QtCore, you will need to add them into the appropriate places in data/global.h, data/typesystem.xml, DougTestPy/DougTestPy.pro, and build.sh. Just see where the word “core” or “Core” appears, and add your extra libraries such as QtGui and QtXml in the same format.

I’ve already gotten my Willem programmer working in Windows 7 with a custom DLL, so I figured why not do the same in Wine under Linux? I meant to make directions for this earlier, but the last time I looked at this, Wine had a bug that made it really annoying to build 32-bit DLLs on 64-bit Linux. That’s all fixed now, so here we go. These instructions are tested with Ubuntu 14.04 with Wine 1.6. Some older versions of Wine had problems, so this is all I can test with.

Warning: This DLL is slow, and these instructions are fairly complicated. I don’t really want to supply prebuilt binaries, and it requires some special work that might have to be repeated when new versions of Wine are installed due to updates. I’m also too lazy to make it configurable, so you need to change a #define in the code if your parallel port is at an I/O address other than 0x378. If you’re still feeling brave, here’s what I have:

Get your parallel port address

First of all, figure out your parallel port address. Type the following command:

cat /proc/ioports | grep parport

You should see something like this:

  0378-037a : parport0

In this case, the output means my parallel port is at 0x378. Remember this address; we will customize it when we compile the DLL.

Download the DLL source code

Download the DLL source code here:

io_dll_willem_linux.tar.gz

Extract it. If your parallel port is at an address other than 0x378, open up io_main.c and update the LPT_BASE_ADDR define near the top of the code to match your parallel port address.

Install prerequisites

These packages will probably be necessary to make everything work. I hope I got all of them, but I may have forgotten something. If I forgot something, I apologize. Let me know in the comments.

sudo apt-get install build-essential wine1.6-dev gcc-multilib

Build the DLL

Inside the DLL source directory, type:

make

Assuming everything goes without error, you should find the file io.dll.so which is the DLL we will be using.

Install the DLL

Remove any io.dll from your Wine installation. It’s useless inside of Wine anyway, so there’s no need for it. Make sure there’s no io.dll file next to EpromM51.exe either. I’m trying to make sure that Wine doesn’t look anywhere else for this DLL. Now, we’re going to put the io.dll.so file in a place where Wine can find it:

sudo cp io.dll.so /usr/lib/i386-linux-gnu/wine/

I messed around, but couldn’t get this to work without putting it right in Wine’s library directory. There might be a better way to do this, but this was the best I could find.

Even if you’re on a 64-bit system, you’ll find that this DLL gets compiled as a 32-bit DLL. So /usr/lib/i386-linux-gnu/wine is always the correct location for this library, regardless of whether your system is 32-bit or 64-bit.

Set up permissions

Here’s the weird part. Raw port access doesn’t work unless you’re root, or you allow the program to have capabilities to access I/O ports. Running things as root is a bad idea, so let’s give the program capabilities to access the I/O ports. Warning: This could pose a potential security threat to your system. We’re actually going to be giving all Wine programs access to the I/O ports.

Here’s the magic command that allows Wine programs to use raw I/O:

sudo setcap cap_sys_rawio=ep /usr/bin/wine-preloader

You may or may not have to re-run this command every time you update Wine. I just don’t know. Anybody else have a better solution for direct port access? I hate having to mess with wine-preloader, but I don’t want to create a separate daemon or whatever due to the added latency.

All done

If you’re followed all these directions, it’s very possible that your Willem programmer is all ready to go now. Give it a shot. I hope this helps someone out there.

Boring details for developers

For anyone who’s interested in how I did this, I created this DLL by taking the original io.dll, fixing its header file so it doesn’t use weird typedefs that confuse winedump, and then running this command:

winedump spec io.dll -I . -t -C

After that, I ran this command:

winemaker . --nosource-fix --dll --nomfc -D__WINESRC__ --wine32

This gave me everything I needed to get this to work. I also had to fix up the generated code–for example, I commented out the line: #include “config.h” in io_main.c.

I’ve been using /etc/network/interfaces to automatically bring up a Grid Connect PCAN-USB adapter when my computer boots up (or when the adapter is hotplugged). It was working perfectly fine in Ubuntu 13.04 and 13.10.

When I upgraded from Ubuntu 13.10 to 14.04, my previous method of adding the CAN adapter to /etc/network/interfaces caused Ubuntu to take forever to boot with a “Waiting for network configuration…” message (and completely broke all of my network interfaces including both ethernet and CAN). It turns out my previous method was bad because I was using the “inet” address family instead of the “can” address family. See “man 5 interfaces” for details on this address family. I found a much better way to make it work. Here’s my new entry in /etc/network/interfaces:

allow-hotplug can0
iface can0 can static
	bitrate 250000
	up /sbin/ip link set $IFACE down
	up /sbin/ip link set $IFACE up txqueuelen 1000 type can bitrate 250000 sample-point 0.7 triple-sampling off restart-ms 500

Make sure that the three indented lines are indented using tabs. Spaces will cause the file to parse incorrectly, but I had to use spaces to make it format correctly on this blog. (BTW, I believe ifupdown is not supposed to be picky about spaces and tabs, but Network Manager is picky about it). I checked and supposedly even 12.04’s ifupdown has this CAN address family supported, so it should probably work in 12.04 (however, it’s untested by me).

I like to set a custom txqueuelen and restart-ms, neither of which seem to be supported in /etc/network/interfaces, so I added a couple of /sbin/ip commands to do it manually. I first have to bring the interface down, then set the rest of the custom options and bring it back up in the process. These commands seem to be executed after the interface has already been brought up, so that’s why I have to bring it back down. The ip command complains if I try to set options on the interface while it’s already up.

The first indented line that says “bitrate 250000” is required. Even though I end up setting the bit rate again in my manual ip command, the first line has to be there or ifup will complain. You can also set the sample point and triple sampling options with lines that say “triple off” and “samplepoint 0.7”, but since I’m specifying them in the manual command anyway, I figured it would be a waste to repeat them.

Hope this helps someone trying to get their CAN adapter to automatically go up. It’s really quite simple, but I couldn’t find any correct examples online.

Update 8/21/2014: I changed these instructions slightly to use “allow-hotplug can0” instead of “auto can0”. This ensures that it works correctly if the USB CAN adapter is not plugged in when the computer starts up. With “auto can0” you may see a long “Waiting for network configuration…” delay if you boot the system with the adapter unplugged.

I have to admit: until recently, udev completely intimidated me. I needed udev rules for changing device permissions or creating symlinks with special names, but usually I had to use Google to find somebody else’s rule. Sometimes a rule I found wouldn’t work because the udev syntax changed–for example, you need to use ATTR or ATTRS instead of SYSFS with recent versions. It still scares me a little bit, but at least I feel like I actually understand what’s going on with it now. I’d like to share a few tips and tricks I’ve picked up about making udev rules.

First of all, the man page for udev is extremely useful. I know man pages can be boring, but this one has just about everything you need to know, and it’s not too long.

Where udev rules are stored

There are two directories where you can find udev rules:

  • /etc/udev/rules.d
  • /lib/udev/rules.d

The rules in /lib/udev/rules.d are bundled rules that you probably shouldn’t be messing with. Instead, put your custom rules in /etc/udev/rules.d. You’ll notice they are named in the form “number-description.rules”. The number is used for ordering the rules, so you can pick an order that makes sense for your needs. It’s a good idea to use that same form so that you have a good idea of the order in which the rules will be parsed. This can definitely matter–I have had troubles with creating symlinks for USB serial devices if the number is too small, probably due to some bundled rule that overrides something my rule does.

If you want to override a bundled rule file that’s in /lib/udev/rules.d, you should create a file with the same name in /etc/udev/rules.d. This will cause the new file in /etc/udev/rules.d to be used instead of the file in /lib/udev/rules.d.

The format of udev rules

udev rules are basically a comma-separated list of things–conditions and assignments. If all of the conditions in a rule are true, the rule is a match and all of the assignments are performed. Similar to many programming languages, a single equal sign (=) represents an assignment, while two equal signs (==) represent a comparison. != represents a “not equals” comparison, += adds a value to a list, and := assigns a value for the final time, disallowing any higher-numbered rules from modifying whatever you assigned. Every condition or assignment is a key-value pair separated by one of the operators listed above.

Keys you can match against

This section, I believe, is the most important part to understand. If you don’t understand the intricacies of how these matches work, especially with the keys ending in S, you will probably make mistaken assumptions about how they work (I did!). You can match against these keys:

  • ACTION — what happened to the device that caused udev to be invoked. Common actions checked against are “add” and “remove”.
  • KERNEL — the name of the device as given by the kernel
  • KERNELS — the name of the device or a parent device as given by the kernel
  • SUBSYSTEM — the kernel subsystem of the device (example: tty or usb)
  • SUBSYSTEMS — the subsystem of the device or a parent device
  • DRIVER — the name of the device’s driver
  • DRIVERS — the name of the device’s driver or the name of a parent device’s driver
  • ATTR{name} — a sysfs attribute of the device
  • ATTRS{name} — a sysfs attribute of the device or a parent device
  • IMPORTANT NOTE: If you are using any of the keys above that match against a parent device (KERNELS, SUBSYSTEMS, DRIVERS, ATTRS) in a rule, the parent device you’re matching against must be the same. For example, you can’t search for ATTRS of two different parent devices in the same rule. You also can’t search for the ATTRS of one parent device and the SUBSYSTEMS of a different parent device in the same rule. You can work around this limitation by creating multiple rules and using GOTO, but you can’t do it all in the same rule. This is actually a good thing because it allows you to do things like make sure you’re looking at a USB serial number instead of a PCI serial number for example.

This is very much an incomplete list, but these are some of the more important ones.

How to find values to match against

The info above is great, but it doesn’t really help much unless you know which driver, subsystem, and event attribute you’re trying to match against. That’s where a really handy command comes into play:

udevadm info --attribute-walk --name=ttyUSB0

ttyUSB0 is an example device name in this case — a USB serial port. This attribute walk command will find all of the keys/attributes for a particular device and all of its parent devices. If you run this command on a USB serial port, you will notice it finds the tty device at the top of the output, which is actually fairly boring as far as matching goes. Then as you scroll down, you will see udevadm walk all the way up the tree of parent devices.. You will see it reach the actual USB device that provides the serial port, the USB hub it’s connected to, the USB host controller the hub is connected to, and then probably a PCI controller or something like that depending on your host system.

Here is some example output from a PL-2303-based USB to serial converter I have connected to a VMware virtual machine:

looking at device '/devices/pci0000:00/0000:00:11.0/0000:02:00.0/usb2/2-2/2-2.1/2-2.1:1.0/ttyUSB0/tty/ttyUSB0':
 KERNEL=="ttyUSB0"
 SUBSYSTEM=="tty"
 DRIVER==""

looking at parent device '/devices/pci0000:00/0000:00:11.0/0000:02:00.0/usb2/2-2/2-2.1/2-2.1:1.0/ttyUSB0':
 KERNELS=="ttyUSB0"
 SUBSYSTEMS=="usb-serial"
 DRIVERS=="pl2303"
 ATTRS{port_number}=="0"

looking at parent device '/devices/pci0000:00/0000:00:11.0/0000:02:00.0/usb2/2-2/2-2.1/2-2.1:1.0':
 KERNELS=="2-2.1:1.0"
 SUBSYSTEMS=="usb"
 DRIVERS=="pl2303"
 ATTRS{bInterfaceClass}=="ff"
 ATTRS{bInterfaceSubClass}=="00"
 ATTRS{bInterfaceProtocol}=="00"
 ATTRS{bNumEndpoints}=="03"
 ATTRS{supports_autosuspend}=="1"
 ATTRS{bAlternateSetting}==" 0"
 ATTRS{bInterfaceNumber}=="00"

looking at parent device '/devices/pci0000:00/0000:00:11.0/0000:02:00.0/usb2/2-2/2-2.1':
 KERNELS=="2-2.1"
 SUBSYSTEMS=="usb"
 DRIVERS=="usb"
 ATTRS{bDeviceSubClass}=="00"
 ATTRS{bDeviceProtocol}=="00"
 ATTRS{devpath}=="2.1"
 ATTRS{idVendor}=="067b"
 ATTRS{speed}=="12"
 ATTRS{bNumInterfaces}==" 1"
 ATTRS{bConfigurationValue}=="1"
 ATTRS{bMaxPacketSize0}=="64"
 ATTRS{busnum}=="2"
 ATTRS{devnum}=="4"
 ATTRS{configuration}==""
 ATTRS{bMaxPower}=="100mA"
 ATTRS{authorized}=="1"
 ATTRS{bmAttributes}=="80"
 ATTRS{bNumConfigurations}=="1"
 ATTRS{maxchild}=="0"
 ATTRS{bcdDevice}=="0300"
 ATTRS{avoid_reset_quirk}=="0"
 ATTRS{quirks}=="0x0"
 ATTRS{version}==" 1.10"
 ATTRS{urbnum}=="20"
 ATTRS{ltm_capable}=="no"
 ATTRS{manufacturer}=="Prolific Technology Inc."
 ATTRS{removable}=="unknown"
 ATTRS{idProduct}=="2303"
 ATTRS{bDeviceClass}=="00"
 ATTRS{product}=="USB-Serial Controller"

looking at parent device '/devices/pci0000:00/0000:00:11.0/0000:02:00.0/usb2/2-2':
 KERNELS=="2-2"
 SUBSYSTEMS=="usb"
 DRIVERS=="usb"
 ATTRS{bDeviceSubClass}=="00"
 ATTRS{bDeviceProtocol}=="00"
 ATTRS{devpath}=="2"
 ATTRS{idVendor}=="0e0f"
 ATTRS{speed}=="12"
 ATTRS{bNumInterfaces}==" 1"
 ATTRS{bConfigurationValue}=="1"
 ATTRS{bMaxPacketSize0}=="8"
 ATTRS{busnum}=="2"
 ATTRS{devnum}=="3"
 ATTRS{configuration}=="VMware Virtual USB Hub"
 ATTRS{bMaxPower}=="0mA"
 ATTRS{authorized}=="1"
 ATTRS{bmAttributes}=="e0"
 ATTRS{bNumConfigurations}=="1"
 ATTRS{maxchild}=="7"
 ATTRS{bcdDevice}=="0100"
 ATTRS{avoid_reset_quirk}=="0"
 ATTRS{quirks}=="0x0"
 ATTRS{version}==" 1.10"
 ATTRS{urbnum}=="48"
 ATTRS{ltm_capable}=="no"
 ATTRS{removable}=="unknown"
 ATTRS{idProduct}=="0002"
 ATTRS{bDeviceClass}=="09"
 ATTRS{product}=="VMware Virtual USB Hub"

looking at parent device '/devices/pci0000:00/0000:00:11.0/0000:02:00.0/usb2':
 KERNELS=="usb2"
 SUBSYSTEMS=="usb"
 DRIVERS=="usb"
 ATTRS{bDeviceSubClass}=="00"
 ATTRS{bDeviceProtocol}=="00"
 ATTRS{devpath}=="0"
 ATTRS{idVendor}=="1d6b"
 ATTRS{speed}=="12"
 ATTRS{bNumInterfaces}==" 1"
 ATTRS{bConfigurationValue}=="1"
 ATTRS{bMaxPacketSize0}=="64"
 ATTRS{authorized_default}=="1"
 ATTRS{busnum}=="2"
 ATTRS{devnum}=="1"
 ATTRS{configuration}==""
 ATTRS{bMaxPower}=="0mA"
 ATTRS{authorized}=="1"
 ATTRS{bmAttributes}=="e0"
 ATTRS{bNumConfigurations}=="1"
 ATTRS{maxchild}=="2"
 ATTRS{bcdDevice}=="0311"
 ATTRS{avoid_reset_quirk}=="0"
 ATTRS{quirks}=="0x0"
 ATTRS{serial}=="0000:02:00.0"
 ATTRS{version}==" 1.10"
 ATTRS{urbnum}=="37"
 ATTRS{ltm_capable}=="no"
 ATTRS{manufacturer}=="Linux 3.11.0-12-generic uhci_hcd"
 ATTRS{removable}=="unknown"
 ATTRS{idProduct}=="0001"
 ATTRS{bDeviceClass}=="09"
 ATTRS{product}=="UHCI Host Controller"

looking at parent device '/devices/pci0000:00/0000:00:11.0/0000:02:00.0':
 KERNELS=="0000:02:00.0"
 SUBSYSTEMS=="pci"
 DRIVERS=="uhci_hcd"
 ATTRS{irq}=="18"
 ATTRS{subsystem_vendor}=="0x15ad"
 ATTRS{broken_parity_status}=="0"
 ATTRS{class}=="0x0c0300"
 ATTRS{consistent_dma_mask_bits}=="32"
 ATTRS{dma_mask_bits}=="32"
 ATTRS{local_cpus}=="ff"
 ATTRS{device}=="0x0774"
 ATTRS{msi_bus}==""
 ATTRS{local_cpulist}=="0-7"
 ATTRS{vendor}=="0x15ad"
 ATTRS{subsystem_device}=="0x1976"
 ATTRS{d3cold_allowed}=="0"

looking at parent device '/devices/pci0000:00/0000:00:11.0':
 KERNELS=="0000:00:11.0"
 SUBSYSTEMS=="pci"
 DRIVERS==""
 ATTRS{irq}=="0"
 ATTRS{subsystem_vendor}=="0x15ad"
 ATTRS{broken_parity_status}=="0"
 ATTRS{class}=="0x060401"
 ATTRS{consistent_dma_mask_bits}=="32"
 ATTRS{dma_mask_bits}=="32"
 ATTRS{local_cpus}=="ff"
 ATTRS{device}=="0x0790"
 ATTRS{msi_bus}=="1"
 ATTRS{local_cpulist}=="0-7"
 ATTRS{vendor}=="0x15ad"
 ATTRS{subsystem_device}=="0x0790"
 ATTRS{d3cold_allowed}=="0"

looking at parent device '/devices/pci0000:00':
 KERNELS=="pci0000:00"
 SUBSYSTEMS==""
 DRIVERS==""

Let’s pretend we want to match against this device in order to give the device node in /dev permissions that allow it to be read and written by all users on the computer. Unfortunately, this PL-2303-based serial adapter does not seem to provide a serial number, so we’ll have to match against every PL-2303 device that uses the product and vendor ID that this device uses. (Side note: FTDI USB to serial chipsets such as the FT232RL also include a unique serial number, so you can identify a particular USB to serial converter dongle by its serial number. The PL-2303 chipset, at least in this case, does not seem to provide that capability)

First of all, notice that one of the parent devices (/devices/pci0000:00/0000:00:11.0/0000:02:00.0/usb2/2-2/2-2.1) contains the vendor and product IDs of the USB dongle:

  • ATTRS{idVendor}==”067b”
  • ATTRS{idProduct}==”2303″

That will be how we will identify this particular device. This is not the device we want to change the permissions of though; we want to change the permissions of the device at the top of the output that belongs to the “tty” subsystem. That is the actual character device shown in /dev for the port.

Everything above is all of the information we need. Let’s get started creating the rule. I’m going to put it in /etc/udev/rules.d/99-test-usb-dongle.rules. First of all, we will specify that we want to match a device that has a subsystem of tty:

SUBSYSTEM=="tty"

This will ensure we’re changing the permissions of the actual tty device rather than any of the parent USB or PCI devices. Next, we want to match the USB dongle by its USB product and vendor IDs. The USB dongle is represented by several parent devices. In order to match a parent device’s attributes, we will have to use the items that end in S (e.g. ATTRS, KERNELS, SUBSYSTEMS). In this case, we will use the idVendor and idProduct attributes of the device I talked about above:

SUBSYSTEM=="tty", ATTRS{idVendor}=="067b", ATTRS{idProduct}=="2303"

Luckily, both of the ATTRS{} values we need to match belong to the same parent device, which is exactly how it has to work with udev. Now that we’ve matched ATTRS{} contained in the device “/devices/pci0000:00/0000:00:11.0/0000:02:00.0/usb2/2-2/2-2.1”, we can’t match the ATTRS{} or other “ending in S” keys of any other parent device. In this case, we luckily don’t have to worry about that. In other cases, it may be useful to be able to match against keys of multiple parent devices. If you ever run into that kind of situation, you can use LABEL and GOTO to jump to another rule that does another check for other properties of a different parent device. I’m not going to talk about that in this post, but if you look at some of the bundled rules you should be able to figure out how GOTO and LABEL work.

Anyway, back to what we were doing. We have completed the match portion of our rule. All that is left to do is add an assignment to the permissions when the conditions are matched:

SUBSYSTEM=="tty", ATTRS{idVendor}=="067b", ATTRS{idProduct}=="2303", MODE="0666"

This sets the permissions of the tty device to allow reading and writing for the owner, the group, and everyone else. Notice that the assignment uses a single “=” rather than a double “==” like the comparisons used. I think that difference was the biggest stumbling block when I first began looking at udev rules. I was very familiar with the difference between “=” and “==” in C, but I didn’t expect udev to use the same distinction. It turns out that it does.

That’s really all it takes. If you save that file in the location I mentioned earlier (/etc/udev/rules.d/99-test-usb-dongle.rules) and reload the udev rules, the next time you plug a PL-2303 USB dongle in, it should have correct permissions.

To reload the udev rules type the following command:

sudo udevadm control --reload-rules

That’s really all there is to it. Now I’d like to do a more complicated example:

More complicated example

Here’s a rule I created for giving certain USB-serial adapters a different name. If a USB-serial adapter with the serial number “12345678” is plugged in, I want a symlink of the form “ttyBlah#” to be created to point to it, where # is an integer. So if I plug three of them in, I want them to be called ttyBlah0, ttyBlah1, and ttyBlah2. This seems straightforward, but there might be other USB-serial adapters plugged in so I can’t just copy the kernel’s device number. For example, let’s pretend five USB-serial adapters are plugged in, named ttyUSB0 through ttyUSB4. ttyUSB0, ttyUSB1, and ttyUSB4 have the serial number “12345678” and ttyUSB2 and ttyUSB3 are other things that don’t use that serial number. I want my symlinks to be called ttyBlah0, ttyBlah1, and ttyBlah2. Notice that I can’t just blindly copy the number over because I want the third one to be called ttyBlah2, not ttyBlah4. It turns out udev can do this with a little bit of help.

I know this idea of having multiple USB devices with the same serial number seems goofy, but I actually did this in the real world. I programmed several FTDI chips to have the same serial number. It’s more convenient than getting a new USB device ID and not knowing if the Linux kernel is going to support it out of the box. The main reason I did it, though, was not Linux-related: it helps prevent Windows from creating a new COM port number each time a device with a different serial number is plugged in. In certain scenarios this behavior is undesirable.

Let’s get to the fun part. Here’s my example rule:

ACTION=="add", KERNEL="ttyUSB[0-9]*", SUBSYSTEMS=="usb", ATTRS{serial}=="12345678", PROGRAM="/home/doug/symlink-number.sh ttyBlah", SYMLINK+="ttyBlah%c"

This rule makes use of several new ideas. First of all, I have to match a device with a kernel name of ttyUSB followed by zero or more digits. This should match all USB serial ports. A parent device needs to be in the USB subsystem. This same parent device also needs to have a serial number of 12345678. The reason we’re checking for a subsystem of USB is to ensure that we don’t match against a device that has a PCI serial number (for example) of 12345678 instead. It’s probably overkill since ttyUSB devices are only going to be USB devices anyway, but it’s a good example of how the “ending in S” keys work. Anyway, we’re making sure we match against a ttyUSB* device which has a USB serial number of 12345678. Those three rules combined should match my three adapters that have that serial number.

If they match, we have two assignments. The first assignment is to PROGRAM. If you assign a value to PROGRAM, the value is interpreted as a command to run. The command is executed and the program’s output to stdout can be used in various ways in udev. This script (which we will see later) will print out the next available number to use for ttyBlah. Note that I passed a single argument to my script: ttyBlah. As you’ll see, I made this script generic so it could be used for numbering various different symlinks.

The second assignment uses the += operator, which as I said earlier adds a value to a list. The SYMLINK variable is a list of names of symlinks to create that will point to the matched device. We use += in case another rule has already set up a symlink for this same port.

You’ll notice that the symlink will be named ttyBlah%c. Several of the assigned values (one of which is SYMLINK) can be given names with printf-style formatters to add extra information. %c will be replaced with the stdout output from the program that was executed from the PROGRAM assignment. Another example of the available printf-style substitutions is %b which is the name of the parent device that was matched with the “ending in S” keys.

OK, so that’s pretty straightforward. Finally, I’ll give you the contents of my symlink-number.sh script (make sure you chmod +x it!):

#!/bin/sh

SYMPREFIX=$1

for i in `seq 0 100`
do
    if [ ! -e /dev/${SYMPREFIX}${i} ]; then
        echo ${i}
        exit 0
    fi
done

echo Unknown

This is a really crude script, but basically it searches for the next free index between 0 and 100. If for some reason it can’t find a free one, it prints out Unknown. This is actually a pretty lame way to write a script and there are probably better ways to do it. I was just lazy. I’ll never have more than about 5 USB-serial adapters plugged in at a time, so the crude technique doesn’t really bother me.

Conclusion

Don’t be scared by udev. It’s really not that bad. Just sit down, read the very helpful man page, and don’t be afraid to use udevadm’s attribute-walk feature to help you figure out what you need to do.

The other day, I was working on a TI AM3517-based Linux device and I needed to open a serial port at a weird baud rate (31250 bps). The AM3517 is one of TI’s Sitara processors, which are very similar to the popular OMAP chips. The AM3517 is definitely physically capable of doing this baud rate — it starts out with a 48 MHz clock, then it oversamples by either 16X or 13X (thus giving you a 3 MHz or 3.692308 MHz base clock), and then you can supply an integer divisor at that point. With the 16X oversampling, a divisor of 96 would give you 48,000,000 / 16 / 96 = 31250 baud. There was no question that the AM3517 could do the job.

Although the hardware supports it, Linux made it difficult to get the job done. Well, I guess it’s more accurate to say that the combination of Linux and the C library made it difficult.

First of all, the standard way of setting the serial speed is to do the following:

struct termios tio;
tcgetattr(fd, &tio);
cfsetispeed(&tio, B115200);
cfsetospeed(&tio, B115200);
/* do other miscellaneous setup options with the flags here */
tcsetattr(fd, TCSANOW, &tio);

That’s great, except only some standard baud rates have constants: B9600, B19200, B38400, B57600, B115200, etc. There’s no B31250 constant, and you can’t just pass a number to cfsetispeed() and cfsetospeed(). It doesn’t work that way because the speeds are really combinations of bits that get set in the c_cflag member of the termios struct. So essentially, the standard POSIX tcsetattr/tcgetattr API is kind of lame.

I already knew there were ways to get around that problem. I started out by trying the old method that setserial uses:

struct serial_struct serialsettings;
ioctl(fd, TIOCGSERIAL, &serialsettings);
serialsettings.flags &= ~ASYNC_SPD_MASK;
serialsettings.flags |= ASYNC_SPD_CUST;
serialsettings.custom_divisor = 96;
ioctl(fd, TIOCSSERIAL, &serialsettings);

After running that, opening the port with a baud rate of 38400 is supposed to actually use the custom divisor. It’s an ugly way to do it, but it works on a Linux 2.4-based device that I’ve used in the past.

It compiled just fine, but first of all, as soon as my program tried to do the above code, the kernel (the device I’m using has Linux 2.6.37) warned me that setting a custom divisor is deprecated. Worse yet, the TIOCSSERIAL ioctl failed with an error of EINVAL. This is because the kernel’s OMAP serial driver (drivers/serial/omap-serial.c, or drivers/tty/serial/omap-serial.c on newer kernels) doesn’t support that call: serial_omap_verify_port() function returns -EINVAL. So deprecated or not, it just plain doesn’t work with the OMAP serial driver.

I did some looking and dug up a discussion about a replacement for the old, deprecated TIOCSSERIAL method. I also found some other promising leads, including a post on StackOverflow asking the same question.

The discussion about a replacement comes to a conclusion: if the CBAUDEX bit in the c_cflag member of the termios structure is set without any of the other bits from the other B#### constants, then you can put an integer value for the desired baud rate into the c_ispeed and c_ospeed members in the struct. Then, I found a draft patch for this change. The patch includes a new constant called BOTHER (that’s B-OTHER, not “bother!”) defined as the same thing as CBAUDEX by itself.

The patch ended up changing–nowadays, you’ll find that in the kernel, there’s a struct termios and then a struct termios2. termios2 is the new struct that implements the new speed fields. I looked in my glibc header file bits/termios.h, and glibc’s header file actually includes the c_ispeed and c_ospeed members in the standard termios struct. However, the actual code that talks to the Linux kernel, at least in my case, was copying the termios data into an old-style termios struct and using the old-style ioctl that doesn’t look at c_ispeed and c_ospeed.

So after a ton of messing around, I figured out what it takes to do a new-style termios call to set a custom baud rate on newer (2.6+) versions of Linux:

#include <asm/termios.h>

struct termios2 tio;
ioctl(fd, TCGETS2, &tio);
tio.c_cflag &= ~CBAUD;
tio.c_cflag |= BOTHER;
tio.c_ispeed = 31250;
tio.c_ospeed = 31250;
/* do other miscellaneous setup options with the flags here */
ioctl(fd, TCSETS2, &tio);

After doing that, everything worked fine and the OMAP UART worked perfectly at 31250 baud, confirmed with an oscilloscope.

I ran into some #include hell with asm/termios.h, especially if I already had the normal termios.h included elsewhere, so you might have to copy relevant struct definitions into your own code if you can’t get it to work (I know, nasty!). The other thing to keep in mind is that the definition of NCCS might be different between your libc and kernel, so you need to make sure you’re using the kernel’s definition of NCCS. I believe asm/termbits.h has the correct NCCS and structures defined.

Make sure you don’t do another normal tcsetattr() call after the TCSETS2 ioctl, because I’m guessing it’ll probably revert the custom baud rate back to a standard baud rate if you do. I’m not 100% sure on that though.

Anyway, I hope this helps someone. In the end, it wasn’t really that complicated to get working, but it took a lot of research and playing around to understand how to make it work. Maybe I’m missing something, but it seems like glibc and the kernel don’t play well together in this case. It’s also possible that glibc supports this all perfectly and my glibc is compiled incorrectly or something, but either way, manually doing the new-style ioctl like I did above seems to work.

For a few years now, I’ve been fighting a weird problem: X-CTU (which is a software utility provided by Digi for programming XBee modules) is only available for Windows. I do most of my development in Linux so X-CTU is always a pain to work with. It does run pretty well under Wine if you need to use it with Linux or Mac OS X, but when you run it under Wine, it doesn’t detect any serial ports:

X-CTU1

Now of course, we all know that you have to add a symlink in your ~/.wine/dosdevices directory to link Wine to your computer’s serial ports:

# ls -l ~/.wine/dosdevices/
total 0
lrwxrwxrwx 1 doug doug  8 Oct 30 21:30 a:: -> /dev/fd0
lrwxrwxrwx 1 doug doug 10 Oct 30 21:30 c: -> ../drive_c
lrwxrwxrwx 1 doug doug 10 Mar 29 18:08 com1 -> /dev/ttyS0
lrwxrwxrwx 1 doug doug 10 Mar 29 18:08 com2 -> /dev/ttyS1
lrwxrwxrwx 1 doug doug  8 Oct 30 21:30 d:: -> /dev/sr0
lrwxrwxrwx 1 doug doug  1 Oct 30 21:30 z: -> /

But even after doing that, X-CTU still doesn’t detect anything. All of the workarounds that I have found require you to define a user COM port in X-CTU:

X-CTU_UserCOM

After going through that process, you can pick the user COM port and X-CTU works perfectly fine (aside from not being able to download newer firmware versions from Digi’s site). As soon as you quit X-CTU, though, the user COM ports you have defined are gone. So whenever you re-open X-CTU, you have to redefine your user COM port. It gets old. So that’s the problem I’ve been fighting: having to manually add the user COM port every time I open X-CTU.

Today I got fed up and ran X-CTU with all of Wine’s debugging information enabled so I could get a clear idea of what X-CTU does when it first loads, in an attempt to figure out how to get the serial ports to show up. Good news: I got it working and now my serial ports show up automatically when I open X-CTU!

X-CTU_fixed

I’d like to explain how X-CTU detects attached serial ports, what Wine does in response, and finally, how you can get it working for yourself. Let’s dive in!

How X-CTU detects attached serial ports

X-CTU uses Windows’ Setup API to get a list of attached serial ports. I ran it with Wine set for full debugging and traced out the calls to Setup API functions to figure out exactly what it does. It starts out with a call to SetupDiClassGuidsFromName which, given the name of a device class (“Ports” in this case), returns a list of GUIDs that go with that class. Next, it calls SetupDiGetClassDevs with the list of GUIDs to get a list of devices that belong to the Ports class. It goes through the list of devices and requests the “friendly name” of each port by calling SetupDiGetDeviceRegistryProperty. The “friendly name” will look like one of these examples:

  • USB Serial Port (COM5)
  • Communications Port (COM1)
  • Printer Port (LPT1)
  • Blah blah blah port (COM7)

Notice how the friendly name always seems to end with (COM#) and it also includes other ports like printer ports. Well, X-CTU uses this info to detect the port — if the name of the port contains the string “(COM”, then it grabs the number directly after that string and uses it as the COM port number. It also ignores parallel ports.

So to get Wine to correctly populate the list, we need to figure out what Wine is doing in response to the three Windows functions I listed above. This information was readily available by both checking out the debug trace from earlier and also reading the Wine source code. Let’s go there now…

What Wine does when the setup API functions are called

SetupDiClassGuidsFromName searches in the registry for classes named “Port”. I don’t think it behaves exactly like an actual Windows machine, but here’s what it does on Wine. It searches for subkeys of:

HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Class

and looks for subkeys that have a class matching the name provided to it. In Wine, it finds the class with GUID {4d36e978-e325-11ce-bfc1-08002be10318}, which according to Microsoft is for COM and LPT ports. Anyway, that’s the single GUID it finds in Wine.

SetupDiGetClassDevs searches in:

HKEY_LOCAL_MACHINE\System\CurrentControlSet\Enum

for items that match the GUID we found earlier. The above key contains various keys that represent different categories. Then the categories contain keys that represent devices, and the devices contain keys that represent instances of the devices (I believe). The gist of it is that it goes three levels deep through the Enum directory to try to find anything that has a string value “ClassGUID”. If the GUID matches the GUID we found earlier, Wine decides it’s a serial port and returns it in the list of discovered devices. This is the root cause of the whole problem — nothing is put into the registry automatically by Wine for these serial ports. So we’ll definitely need to add this manually, as we’ll see later.

SetupDiGetDeviceRegistryProperty is finally used to get the friendly name for the port. It looks in the same location it looked for the ClassGUID value, but this time it looks for a string called FriendlyName — which, as you guessed it, contains a string in the format of my examples above.

Once I figured this out, I was pretty much home free. So without further adieu, here are the instructions for getting it working.

What to add to the registry

The key (no pun intended) is to add your serial ports as subkeys of:

HKEY_LOCAL_MACHINE\System\CurrentControlSet\Enum

to satisfy the functions I described in the section above:

  • Create a subkey of Enum and call it SERIAL (although the name you use doesn’t really matter–I believe it searches everything, not just the SERIAL subkey).
  • Create a subkey of SERIAL and call it COM1 (if your port is COM1 — although this name doesn’t really matter)
  • Create a subkey of the COM1 key you just created and call it COM1 also (this name doesn’t really matter either though)
  • In the final COM1 subkey you created, add two string values:
    • ClassGUID — containing the value {4D36E978-E325-11CE-BFC1-08002BE10318} (the GUID for the Ports class)
    • FriendlyName — containing a name in the format “Serial Port (COM1)” without the quotes of course.
      • Make sure the name ends with the COM port name in parentheses as in this example — (COM1). It has to be that way or it won’t work–it might appear in the list, but it will fail to open unless you do it exactly in that format.
      • This is what X-CTU actually uses to decide which port to open. The other “COM1” subkeys we added in the earlier steps aren’t checked for anything — I just named them that way for clarity while you’re browsing the registry.

You can make these modifications using regedit in Wine (type “wine regedit” in a terminal window). Here’s an example screenshot to make it clear what you have to add:

X-CTU_regedit

That’s it! You’re done. The ports should now appear automatically in X-CTU. Assuming you have also created the symlinks in ~/.wine/dosdevices for the COM ports you added, they should also be operational.

Conclusion

This is tested in Ubuntu 12.10 with Wine 1.4.1. I would imagine if you can figure out where the dosdevices folder is to stick the symlinks, it will probably work in Mac OS X as well. Your mileage may vary. Good luck!

This strategy definitely works for X-CTU, but it’s not a generic strategy that will work for any Windows program under Wine. Different programs use different methods to get a list of serial ports. Some programs may check for a different key called PortName next to FriendlyName. X-CTU in particular only checks for FriendlyName. If you’re trying to get this to work with a different Windows program, play around. Check programming tutorials to see the various methods people use to enumerate COM ports on Windows. Figure out which method your particular program is using — disassemble it, check what functions it links against, run it in Wine with debugging enabled, etc. Once you’ve figured it out, use Wine’s debugging facilities (and the Wine source code) to see what Wine is doing in response to the various functions that are called. Chances are good that it is looking into the registry and you just need to tweak your registry to give the Windows functions the results they are expecting.

I hope this helps someone out there someday!