RetroFed Forum
Your home away from modern life.
HotSync on Linux with a Handspring Visor: what broke, why, and how to fix it (short answer: it isn't your distro's fault)
Post Reply
AuthorMessage
TheCynicalSaint [they/them, he/him] lemmy.ml TheCynicalSaint@lemmy.ml
Joined: 2024-03-10
Quote

I picked up a thrift-store Handspring Visor and spent a couple of weeks fighting it on Linux. It syncs now. This is everything that bit me, in the order it bit me, so nobody else has to spend a fortnight on it.

Tested on: Fedora 44 Workstation, kernel 7.2.x, iMac18,1 (Intel). Device is a Handspring Visor, USB ID 082d:0100.


TL;DR

If you read nothing else:

  1. The visor kernel driver will steal your device and break WebUSB sync. Blacklist it, and if it's already loaded, sudo modprobe -r visor. This is the single most common cause of "it used to work and now it doesn't."
  2. The Visor only appears on USB while the HotSync button is held. Docked in the cradle and idle, it is invisible. This is device firmware, not a bug.
  3. Skip pilot-link / jpilot. They're 20-year-old, unmaintained, and don't build cleanly on a modern distro.
  4. Use palm-sync via a browser WebUSB app (the web demo or PilotBridge). This is what actually works.

Is this Fedora-specific?

No, and this is the part I got wrong when I started. I assumed my problems were a Fedora quirk. They're not. Almost everything below is any Linux, because it's about the kernel's USB serial driver and a device that predates modern USB expectations.

Genuinely distro-specific (Fedora-flavoured):

  • Package names. sudo dnf install pilot-link — on Debian/Ubuntu it's apt install pilot-link jpilot.
  • ModemManager. Fedora ships it enabled and it probes USB serial devices on plug-in. It's a classic source of "port is busy" bind errors. On Debian/Ubuntu it's usually disabled by default. I wasted time on this one; the fix is a udev rule telling ModemManager to ignore the device.
  • Whether the visor module auto-loads for your device depends on your udev/modemmanager setup. On my box it auto-bound; on yours it might not. You have to check.

Universal — will hit you on any distro:

  • The visor driver stealing the device. visor.ko has been in mainline Linux for ~20 years and is present everywhere. The blacklist + modprobe -r fix is the same on Arch, Ubuntu, openSUSE, whatever.
  • HotSync-button-only enumeration. That's the Visor's firmware.
  • udev rules. systemd-udev is systemd, so every modern distro.
  • pilot-link / jpilot being abandoned. Universal.
  • palm-sync not being on npm. Universal.
  • WebUSB permission model. Universal. (Caveat: you need a Chromium-based browser. Firefox's WebUSB support is incomplete. This is a browser limitation, not a distro one.)

So: if you're on Ubuntu and you modprobe visor to poke at your Palm, you have created the exact same problem I had.


Problem 1: "It doesn't show up in lsusb"

This one is expected behaviour and it cost me hours.

The Visor only enumerates on the USB bus while the HotSync button is actually pressed. Dock it in the cradle, walk away, and lsusb shows nothing. No kernel driver, no device node, nothing.

Every tutorial on the internet — mine included, for the first day — tells you to run the sync command and then press HotSync when prompted. That is exactly backwards. The port doesn't exist until the button goes down.

# 1. PRESS AND HOLD the HotSync button on the cradle
# 2. while holding it, in a terminal:
lsusb | grep -i handspring
ls /dev/ttyUSB*
# 3. only then start your sync

Verify the HotSync timing like this: if you press the button before the host is listening, the Visor shows a "HotSync Error" on its own screen. That's your clue that the sequencing is wrong.

The upstream palm-sync docs confirm this explicitly: "most Palm devices won't actually show up on the computer until you start a HotSync." So if you feel stupid, don't — everyone does.


Problem 2: The visor driver steals the device (the big one)

This is the one that matters. If your sync worked last week and doesn't now, this is almost certainly why.

Linux has a built-in serial driver called visor for old Handspring/Palm hardware. It does exactly what you'd expect: claims the device and exposes it as /dev/ttyUSB0 and /dev/ttyUSB1.

That's fine if you want to use pilot-link. It's fatal if you're using WebUSB, because WebUSB cannot claim a device that a kernel driver already owns. No amount of fixing permissions will help. The device is already spoken for.

You can confirm it did it:

dmesg | grep -i visor
# ... visor: USB HandSpring Visor converter now attached to ttyUSB0

And check whether it's loaded:

lsmod | grep visor

The fix

Blacklist it so it never auto-loads:

# /etc/modprobe.d/blacklist-visor.conf
blacklist visor

Important gotcha that bit me: a blacklist only prevents automatic loading. If you (or a tutorial, or an old session of yours) ever ran sudo modprobe visor, the module stays loaded until reboot or until you explicitly remove it. The blacklist does nothing about an already-loaded module. This is exactly how my working setup silently broke overnight — a backup session the previous evening had loaded it, and a day later it grabbed the device on plug-in and killed WebUSB.

So if the blacklist is already in place and it's still broken:

sudo modprobe -r visor

Then confirm it's gone and that no ttyUSB nodes are being created:

lsmod | grep visor          # should be empty
dmesg | tail -20 | grep -i visor

Sanity check that the driver really is bound to your device, so you know the blacklist is actually necessary:

modinfo visor | grep -i 082d
# alias: usb:v082Dp0100d*dc*dsc*dp*ic*isc*ip*in*   <-- yes, it auto-binds to the Visor

Problem 3: pilot-link and jpilot are a dead end

I tried these first because they're what every old tutorial recommends. Don't.

pilot-link: works, sort of, but the CLI defaults to a usb: / usb:any port that assumes libusb. With the visor driver loaded, that path fails with:

pi_bind error: usb:any No such file or directory
Exiting with status SYNC_ERROR_BIND

You can force it at a serial port instead (pilot-xfer -p /dev/ttyUSB0 -l), but that only works with the driver loaded — which is exactly the thing that breaks the modern approach. Also note the port number is not stable: I saw /dev/pilot0/1 become /dev/pilot2/3 between reconnects, which is why a stable /dev/pilot* symlink via udev is worth having if you go this route.

jpilot: the GUI. It ignores the PILOTPORT environment variable and reads its own config file, which hardcodes port usb:any in ~/.jpilot/jpilot.rc (or similar). You have to edit the file, and the GTK2 build is long dead. Building 2.1.0 from source on a modern distro means:

sudo dnf install gcc make gtk3-devel pilot-link-devel intltool

and it still wants GTK >= 3.24.12, not GTK2 — the configure error tells you this only after you've already installed the wrong thing. Plus a cosmetic Failed to load module "pk-gtk-module" warning on every launch that's harmless (packagekit-gtk3-module silences it).

Verdict: the whole pilot-link/jpilot path is a 2004-era solution to a problem that now has a proper answer. It's not that it's Fedora-broken, it's that it's time-broken.

Can I use Wine/Bottles with Palm Desktop instead? No, not for USB HotSync. Palm Desktop needs real USB access and Wine doesn't do USB passthrough for sync. Bottles is just a Wine frontend, so it doesn't help either. Serial cradles from the Graffiti era might work over Wine with the serial port forwarded, but that's a museum piece.


Problem 4: Getting non-root access (for WebUSB)

palm-sync talks to the device over raw libusb. Both the CLI and the browser need write access to the USB device node, and the default udev rules won't give it to you.

Grab the upstream rules file from the palm-sync repo — it's a big table covering dozens of Palm-era devices, and it already includes yours:

sudo cp 60-palm-os-devices.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules && sudo udevadm trigger

The relevant line is just:

# Visor, Treo 300
SUBSYSTEMS=="usb", ACTION=="add", ATTRS{idVendor}=="082d", ATTRS{idProduct}=="0100", MODE="0666"

MODE="0666" is blunt but it's what upstream ships and what the project documents. If you'd rather not world-writable device nodes, restrict it to the plugdev group and add yourself to that group instead.

Tell ModemManager to leave it alone while you're there:

# /etc/udev/rules.d/99-handspring-ignore.rules
SUBSYSTEM=="usb", ATTR{idVendor}=="082d", ATTR{idProduct}=="0100", ENV{ID_MM_DEVICE_IGNORE}="1"

Verify permissions after pressing HotSync (note the node is named by bus/device number, not vendor ID — grepping the device node filename for 082d finds nothing and will confuse you):

lsusb | grep -i handspring
ls -la /dev/bus/usb/001/*        # look for the Device number from lsusb above
# want: crw-rw-rw-  <your user> <your user>

crw-rw-rw- and no kernel driver attached = you're ready.


Problem 5: Installing palm-sync itself

If you want the CLI, be aware: it is not on npm, despite the README telling you to npm install -g palm-sync. You get:

npm error 404 Not Found - GET https://registry.npmjs.org/palm-sync
npm error 404  'palm-sync@*' is not in this registry.

It's on GitHub only, at version 0.2.1, and the repo ships no prebuilt dist — so a source install means installing the toolchain and building the TypeScript yourself.

You probably don't need the CLI. If you just want to sync, use a browser:

Both need a Chromium-based browser (WebUSB). And the sequence is the same as always: dock it, press and hold HotSync, then hit connect while it's still enumerated. If the device drops off before you connect, press HotSync again to re-enumerate.

If you do want the CLI and can't be bothered with sudo-based npm, npx --yes palm-sync runs it straight from the cache.


The workflow that actually works

# one-time setup
sudo modprobe -r visor                                        # in case it's loaded
echo "blacklist visor" | sudo tee /etc/modprobe.d/blacklist-visor.conf
sudo cp 60-palm-os-devices.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules && sudo udevadm trigger

Then, every time:

  1. Visor in the cradle, plugged into a real USB port on the machine (old cradles and hubs are flaky).
  2. Press and hold HotSync.
  3. Open the web demo or PilotBridge, hit connect, pick the Visor.
  4. Sync.

If it stops working one day with no changes on your end, run lsmod | grep visor before anything else. That's almost certainly it.


Known unresolved issue: sync hangs at the SLP handshake

Flagging this because it's the one thing I did not solve, and I'd rather be honest about it than pretend.

With PilotBridge the connection gets further than it used to — it enumerates, claims the interface, reads the port list, establishes the connection, and starts the handshake:

palm-sync:usbFound device 082d:0100 - Visor, Treo 300
palm-sync:usbFound 4 endpoints on interface 0:
palm-sync:usbClaimed interface 0
palm-sync:usb<<< {"numPorts":2,"ports":[{"functionType":"GENERIC","portNumber":1},
                                          {"functionType":"HOT_SYNC","portNumber":2}]}
palm-sync:usbConnection configuration: {"inEndpoint":2,"outEndpoint":2}
palm-sync:syncConnection established
palm-sync:usbStarting handshake
palm-sync:sync-serverConnected!
palm-sync:slp<<< beefed030302000effaf
palm-sync:slp--- Found SLP signature after skipping 0 bytes

…and then it stops dead.

For the record, so nobody re-derives it: the two GetExtConnectionInfo failed with status stall lines earlier in the log are normal and not the bug. The Visor only implements GetConnectionInfo, and palm-sync falls back to it correctly. If you see those stalls followed by a successful numPorts: 2 read, ignore them.

The real problem is that only a 10-byte SLP datagram arrives (the 0xBEEF signature and a truncated header) where a complete one should be 26 bytes. The remaining payload never arrives, the read stream waits forever, and the CMP WAKEUP handshake never happens—which is why you see no palm-sync:cmp or palm-sync:padp lines at all. Their absence is the tell.

I checked upstream and there is no existing issue for this—searching the jichu4n/palm-sync tracker for "SLP", "082d", or "Visor" turns up nothing relevant (only #477, a closed USB chunked-write fix, and #1). So as far as I can tell this is unreported. If you hit it, the raw USB byte stream is the thing to capture, and someone with the patience to file it upstream would be doing the community a favour.

The upstream web demo synced successfully for me before this appeared, so if PilotBridge gives you trouble, try the plain web demo before assuming your hardware is at fault.


Hardware notes, for the thrift-store crowd

  • The Visor is a serial-protocol-over-USB device pretending to be a modern USB gadget. It shows up with 4 bulk endpoints (16-byte packet size on endpoint 1, 64-byte on endpoint 2) and HotSync runs over endpoint 2.
  • Old cradles are genuinely flaky. Plug directly into the machine, not through a hub, and reseat the USB cable if the device doesn't enumerate. I had several sessions lost to a cable that only half-worked.
  • The Visor only enumerates on button press, which means you also can't tell "cradle is unplugged" apart from "button not pressed" at a glance. When in doubt, hold the button and check lsusb.

Happy to answer questions. If someone gets further than I did on that SLP hang, please post it.

4   Original
Post Reply
Back to top