diff --git a/desktop/README.md b/desktop/README.md index ace1853..26f8480 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -93,7 +93,7 @@ The same binary works without a GUI: `--appimage-extract` and launch `squashfs-root/AppRun` instead. - **`vendor/libnspire-sys`** at the repo root is a locally patched crate wired in via `[patch.crates-io]`. It raises the CX II NNSE handshake retry limit - (10 → 30). The build will not reproduce upstream behaviour without it. -- **Known upstream limitation:** libnspire's CX II support has been frozen since - 2020. Against current CX II firmware, data transfers may return `Busy` or hang. - This is independent of the Tauri version and is not a defect in this port. + (10 → 30), fixes the `readPacket` checksum length bug that made CX II + transfers fail with a reported `Busy`, and adds NNSE retransmission. The build + will not reproduce upstream behaviour without it. See + [`nlink-cli/README.md`](../nlink-cli/README.md) for the details. diff --git a/nlink-cli/README.md b/nlink-cli/README.md index 819a516..e41164f 100644 --- a/nlink-cli/README.md +++ b/nlink-cli/README.md @@ -41,11 +41,25 @@ cargo build --release tells it to leave TI devices alone. Restart it after adding the rule: `sudo systemctl start ModemManager`. -## Known limitation -libnspire's CX II ("NavNet SE") protocol support has been **frozen upstream since 2020** -(`lights0123/libnspire`, last commit 2020-09-30; crates.io `libnspire-sys` 0.3.4 is the newest). -Against **current (2026) CX II firmware** the USB link and handshake work — real filenames -are transferred — but **multi-packet stream transfers are unreliable** and long operations -(e.g. listing a folder with many entries) can fail mid-transfer with `Busy`. Short transfers -are more likely to complete. This is a library/firmware-era mismatch, not a local -configuration problem. +## 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. diff --git a/vendor/libnspire-sys/libnspire/src/cx2.cpp b/vendor/libnspire-sys/libnspire/src/cx2.cpp index a0e14da..5403291 100644 --- a/vendor/libnspire-sys/libnspire/src/cx2.cpp +++ b/vendor/libnspire-sys/libnspire/src/cx2.cpp @@ -48,6 +48,18 @@ int gettimeofday(struct timeval *t, void *timezone) { // Windows... #undef min +/* Default timeout for reads that are just waiting for the calculator to say + something on its own schedule. */ +#define NSP_CX2_READ_TIMEOUT 60000 +/* How long to wait for an ack before assuming the packet (or its ack) was + lost and sending it again. Kept generous: slow operations (flash writes, + OS install) can legitimately take a while to be acknowledged, and a + needless retransmission is more expensive than a little patience. */ +#define NSP_CX2_ACK_TIMEOUT 2000 +/* Total time to keep retransmitting one packet before giving up with BUSY. + TI's own stack derives its retry count the same way, as total/ack. */ +#define NSP_CX2_ACK_TOTAL_TIMEOUT 60000 + enum Address { AddrAll = 0xFF, AddrMe = 0xFE, @@ -155,14 +167,14 @@ static uint16_t compute_checksum(const uint8_t *data, uint32_t size) return acc; } -static bool readPacket(libusb_device_handle *handle, NNSEMessage *message, int maxlen) +static bool readPacket(libusb_device_handle *handle, NNSEMessage *message, int maxlen, unsigned int timeout = NSP_CX2_READ_TIMEOUT) { if(maxlen < sizeof(NNSEMessage)) return false; int transferred = 0; memset(message, 0, sizeof(NNSEMessage)); - int r = libusb_bulk_transfer(handle, 0x81, reinterpret_cast(message), maxlen, &transferred, 60000); + int r = libusb_bulk_transfer(handle, 0x81, reinterpret_cast(message), maxlen, &transferred, timeout); if(r < 0 || transferred < sizeof(NNSEMessage)) @@ -191,7 +203,11 @@ static bool readPacket(libusb_device_handle *handle, NNSEMessage *message, int m dumpPacket(message); #endif - if(compute_checksum(reinterpret_cast(message), transferred) != 0xFFFF) + /* Checksum over the length the packet declares, not over what the bulk + transfer happened to hand back. The calculator can return more bytes + than the packet occupies, and after the continuation loop above + `transferred` only holds the size of the final chunk anyway. */ + if(compute_checksum(reinterpret_cast(message), completeLength) != 0xFFFF) return false; return true; @@ -409,7 +425,13 @@ int packet_send_cx2(struct nspire_handle *nsp_handle, char *data, int size) int len = sizeof(NNSEMessage) + size; NNSEMessage *msg = reinterpret_cast(malloc(len)); + if(!msg) + return -NSPIRE_ERR_NOMEM; + /* misc and unknown are covered by the checksum, so they have to be set to + something deterministic rather than left as malloc garbage. */ + msg->misc = 0; + msg->unknown = 0; msg->service = StreamService; msg->src = AddrMe; msg->dest = AddrCalc; @@ -419,34 +441,50 @@ int packet_send_cx2(struct nspire_handle *nsp_handle, char *data, int size) memcpy(getPacketData(msg), data, size); - int ret = -NSPIRE_ERR_SUCCESS; - if(!writePacket(handle, msg)) - ret = -NSPIRE_ERR_BUSY; - else + const int maxlen = sizeof(NNSEMessage) + 1472; + NNSEMessage * const message = reinterpret_cast(malloc(maxlen)); + if(!message) { - const int maxlen = sizeof(NNSEMessage) + 1472; - NNSEMessage * const message = reinterpret_cast(malloc(maxlen)); + free(msg); + return -NSPIRE_ERR_NOMEM; + } - bool acked = false; - for(int i = 40; i-- && !ret && !acked;) + /* Either the packet or its ack can go missing, and on current CX II + firmware that happens often enough that a single attempt makes long + transfers (a large directory listing, say) fail routinely. Retransmit + the same seqno until it is acked, which is what TI's own stack does. */ + const int attempts = NSP_CX2_ACK_TOTAL_TIMEOUT / NSP_CX2_ACK_TIMEOUT; + bool acked = false; + + for(int attempt = 0; attempt < attempts && !acked; attempt++) + { + /* Bit 3 marks a retransmission, so the calculator can drop the + duplicate if it did receive the original and only the ack was + lost. The seqno deliberately stays the same. */ + msg->reqAck = attempt ? 0x9 : 0x1; + + if(!writePacket(handle, msg)) + continue; + + /* Service whatever arrives until the ack shows up or we run out of + patience and send again. */ + while(readPacket(handle, message, maxlen, NSP_CX2_ACK_TIMEOUT)) { - if(!readPacket(handle, message, maxlen)) - continue; - handlePacket(nsp_handle, message); if(message->dest == AddrMe && message->service == (StreamService | AckFlag) && message->seqno == msg->seqno) + { acked = true; + break; + } } - - if(!acked) - ret = -NSPIRE_ERR_BUSY; - - free(message); } + int ret = acked ? -NSPIRE_ERR_SUCCESS : -NSPIRE_ERR_BUSY; + free(message); + free(msg); return ret;