n-link/nlink-cli/README.md
Josip Jureta a0d955d3e7
fix(cx2): checksum NNSE packets over their declared length (#2)
Long CX II operations — listing a folder with many entries, downloading a
file of any size — failed with a reported `Busy`.

readPacket checksummed `transferred`, the byte count libusb returned, rather
than `completeLength`, the length the packet declares. The calculator can
hand back more bytes than the packet occupies (a 64-byte packet arriving as
65 was what showed up in tracing), and the stray byte failed the checksum.
The packet was dropped and so never acked, the calculator retransmitted the
same sequence number indefinitely, and the receive loop gave up after 40
reads. `transferred` was also wrong after the multi-chunk continuation loop,
where it holds only the final chunk's size — the reason large stream
transfers were unreliable.

Also add NNSE retransmission to packet_send_cx2. A lost packet or lost ack
was previously fatal, since the packet was written exactly once. Retries now
carry the "not the first try" bit (reqAck bit 3) that TI's own stack sets,
keeping the same seqno so the calculator can drop a duplicate. This path did
not trigger in testing — every packet acked on the first attempt once the
checksum was fixed — so its timeouts are deliberately generous to avoid
retransmitting during slow flash writes.

While here, zero `misc` and `unknown` instead of sending malloc garbage
inside a checksummed header, and handle allocation failure.

Verified against CX II firmware (0451:e022): `ls /` returns all 51 entries
across 8 runs, a 193 KB download is byte-identical across two runs, and
mkdir/rmdir round-trips.

Note the error was never actually Busy. The libnspire crate (0.2.3) matches
its positive NSPIRE_ERR_* constants against libnspire's negative return
codes, so every error falls through to the libusb table and is mislabelled;
-NSPIRE_ERR_INVALPKT and LIBUSB_ERROR_BUSY are both -6. That mismapping is
upstream in the crate and is not addressed here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 16:35:23 -04:00

3.2 KiB

nlink-cli — standalone TI-Nspire CX II link CLI (Linux)

A GUI-free command-line build extracted from n-link, for transferring files to/from a TI-Nspire CX II (CAS) over USB on Linux.

Built because:

  • TiLP2 (distro libticables 1.3.6) does not support the CX II (USB PID e022) — its probe reports "no devices found".
  • The prebuilt n-link 0.1.6 .deb won't run on Ubuntu 26.04 (needs the retired libssl.so.1.1 + WebKit2GTK 4.0). n-link is a Tauri/WebKit app.

This CLI reuses only n-link's self-contained cli.rs (upload/download/ls/mkdir/rmdir/ copy/move/upload-os), dropping Tauri/WebKit entirely.

Build

# one-time system deps
sudo apt-get install -y libusb-1.0-0-dev pkg-config
# Rust toolchain via rustup, then:
cargo build --release
# binary: target/release/n-link-cli

Cargo.toml uses a local [patch.crates-io] override of libnspire-sys (../vendor/libnspire-sys) whose CX II NNSE handshake retry limit is raised from 10.

Run

./target/release/n-link-cli ls /
./target/release/n-link-cli download "/MyFile.tns" ./
./target/release/n-link-cli upload ./MyFile.tns "/"
./target/release/n-link-cli --help

System setup required (Linux)

  1. udev access for the CX II (/etc/udev/rules.d/70-ti-nspire.rules):
    SUBSYSTEM=="usb", ATTRS{idVendor}=="0451", ENV{ID_MM_DEVICE_IGNORE}="1"
    
    (plus group/uaccess for PID e022) — then udevadm control --reload-rules && udevadm trigger.
  2. ModemManager grabs the interface on plug; the ID_MM_DEVICE_IGNORE rule above tells it to leave TI devices alone. Restart it after adding the rule: sudo systemctl start ModemManager.

The Busy error on CX II (fixed)

Long operations against current CX II firmware — listing a folder with many entries, downloading a file of any size — used to fail with Busy. Two bugs in the vendored libnspire, both fixed in vendor/libnspire-sys/libnspire/src/cx2.cpp:

  1. readPacket checksummed the wrong number of bytes. It used the byte count the bulk transfer returned instead of the length the packet declares. The calculator can hand back more bytes than the packet occupies (a 64-byte packet arriving as 65), and the stray byte failed the checksum. The packet was then dropped and never acked, so the calculator retransmitted it forever until the receive loop gave up. The same variable was also wrong after a multi-chunk read, where it held only the last chunk's size — this is what made large stream transfers unreliable.
  2. No retransmission. A lost packet or lost ack was fatal; the packet was sent once and never repeated. It now retransmits with the "not the first try" bit (reqAck bit 3) that TI's own stack uses, keeping the same sequence number.

Note that the error was never really Busy: the libnspire crate (0.2.3, not this vendored C library) matches its positive NSPIRE_ERR_* constants against libnspire's negative return codes, so every error falls through to the libusb table and is mislabelled. -NSPIRE_ERR_INVALPKT is -6, and so is LIBUSB_ERROR_BUSY. A reported Busy is really Invalid packet received; Timeout is really NACK received, and so on. That mismapping is upstream in the crate and is not fixed here.