mirror of
https://github.com/lights0123/n-link.git
synced 2026-08-07 18:23:28 +00:00
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>
This commit is contained in:
parent
f6410b5591
commit
a0d955d3e7
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
76
vendor/libnspire-sys/libnspire/src/cx2.cpp
vendored
76
vendor/libnspire-sys/libnspire/src/cx2.cpp
vendored
|
|
@ -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<unsigned char*>(message), maxlen, &transferred, 60000);
|
||||
int r = libusb_bulk_transfer(handle, 0x81, reinterpret_cast<unsigned char*>(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<uint8_t*>(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<uint8_t*>(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<NNSEMessage*>(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<NNSEMessage*>(malloc(maxlen));
|
||||
if(!message)
|
||||
{
|
||||
const int maxlen = sizeof(NNSEMessage) + 1472;
|
||||
NNSEMessage * const message = reinterpret_cast<NNSEMessage*>(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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue