A harness for large language models, to make reverse engineering USB devices with vendor specific protocols easier.
The problem
Hardware that still works, with nothing written down about how it talks
Think about a piece of hardware from fifteen years ago that still works and still plugs in, but has no driver that anyone can find, because the protocol it speaks was never published anywhere. If you want your own software to talk to it, there is nothing to look the answer up in, so you have to sit down and work the language out for yourself.
That work is slow, and it goes roughly like this. You run lsusb to find which bus the thing is on, start a usbmon capture, plug the device in, click one button on it, unplug it, and stop the capture. Then you open a file of raw packets and stare at bytes until you can see where a command starts, which parts of it never change, which byte is a counter, and which trailing byte is a checksum calculated over everything before it. Then you do the whole thing again for the next button.
It is expert work and it is slow, and almost nobody has the time for it, which is how equipment that still functions ends up retired. Nothing can talk to it any more, and whatever anyone once knew about how it worked is lost with it.
Before
Every command the device understands is somewhere in here, and nothing marks where one starts or what any byte means.
After
- outbulkendpoint 028 bytes
- inbulkendpoint 818 bytespaired with the line above
- outcontrolendpoint 00setup packet
The same traffic as typed records, with the direction, the transfer type and the endpoint named, and each request joined to the response that answered it.
The gap
Why an AI could not already do this
At first this looked like exactly the kind of pattern-finding a language model should be good at, but it turned out not to be, for three reasons that we ended up designing the whole tool around.
The first is that the recording is binary. What you get out of usbmon is a pcapng file, which is a container of raw USB packets, and a language model cannot open one and see what is inside it. Somebody has to turn that traffic into a form it can reason over, without flattening away the detail that carries the answer.
The second is that if you simply ask a model to guess at a protocol, it will produce something that looks right whether or not it is, and that is the failure mode that matters here. A confident wrong answer about a checksum is worse than no answer at all, because you cannot tell the two apart until you send the command and the device ignores you.
The third is that capture is physical. Somebody has to plug the device in, press its buttons and unplug it, at the right moments, while a recording is running, and no amount of model capability does that part for you.
Research
We spent the first weeks reading about USB
Nobody on the team had written USB software before, and a tool that claims to read a protocol is worthless if the people building it do not understand the bus underneath. So the first stretch of the project was reading rather than building, and I ran it as a deliberate phase with its own outcome rather than as background study people did on their own time.
What we had to learn was how the bus actually works, what the four transfer types are and which ones carry the traffic we cared about, how enumeration goes when a device is plugged in, what the descriptors do and do not contain, and how a URB is laid out once usbmon has recorded it. We worked through the same sources together and collected them on the project wiki, so that a new person joining could start where we started instead of going looking.
That reading is what made the scope decisions possible later on, because you cannot sensibly decide to leave isochronous transfers out of a tool until you understand what they carry and who actually needs them. It is also what let us read somebody else's prior art and judge it, rather than copying it.
This is what we read, and it is all on the project wiki.
- USB in a NutShell, chapters 1 to 5, the most accessible introduction to the bus and the one our sponsor pointed us at first.
- USB 2.0 specification, chapters 8 and 9, the wire protocol and the device framework, kept as the normative reference for anything we needed to be exact about.
- Writing a USB driver, from the Linux kernel documentation, for how the kernel side works, which is what our captures are recording.
- The pcap-ng format, on the Wireshark wiki, the container we had to parse before any of the rest was possible.
- usbrevue, prior art in Python, read for its URB field layouts and its approach to parsing captures.
- The Model Context Protocol specification, required reading before the milestone where we built the server.
- The MCP Python SDK and its example servers, which is what we built our own server against.
I do not think you can judge what a tool should automate until you have sat down and done that work yourself.
Retrospective
If I were setting this up again I would go further in that direction rather than less. I would run two sessions on how USB reverse engineering is actually practiced by the people who do it, then one where we try to reverse engineer something with no tool at all, and then a discussion of what we learned from that and which workflow would help a real user the most. The manual attempt is the part I would protect, because it is where the requirements come from.
Requirements gathering
Doing it by hand is where the requirements came from
Our test device was a two-channel relay on a CH340 serial converter, which was cheap and physical and satisfying to work with, because you can hear it click the moment you get a command right.
Working it by hand is where our requirements actually came from, because it taught us the shape the tool had to have. It needed one capture per event rather than one long recording, so that the plug-in enumeration handshake sits in its own file, the unplug handshake sits in another, and each distinct action the device can perform sits in one of its own. Once they are separated like that you can compare them, and the differences between them are the commands. If you record everything into a single file instead, you are back to staring at bytes with no way in.
The decision that mattered
The agent never touches the capture directly
This is the design choice I would defend hardest, because it is the reason anyone can trust what the tool tells them.
Everything the agent can do is a fixed operation with a name. It can load a capture, list the devices in it, pull a range of packets, start and stop a recording, and each of those returns structured data rather than prose. Every claim it makes traces back to a specific range of packets in a specific file, and because the operations are fixed, any one of them can be checked by hand against the capture with no agent involved. It has no way around those tools to read the file for itself, so it cannot hand-wave about what is in there, and designing it that way meant we could not hand-wave either.
We also decided against generating driver code, which was the obvious feature to add and, I think, the wrong one. A generated driver looks finished, and that hides how much of the protocol underneath it is still a guess. What the tool produces instead is a structured device record listing what it observed, what it concluded from that, and which packets support each conclusion. For a device with nothing published about it anywhere, that record is the first documentation it has ever had.

Architecture and implementation
From a live bus to a written record
bsu-tool, which is short for Behavioral Sleuth for USB, is a command line tool and an MCP server. The command line is there for a person to use directly and the MCP server is there for an agent, and both of them drive the same four stages underneath.
Capture
usbmon records the bus while you use the device, one capture per event, out to standard pcapng. lsusb finds the target.
Decode
URBs become typed records, with the capture length validated, descriptors parsed, endpoints counted, and requests paired to the responses that answered them.
Analyze
Repeated byte sequences surface as candidate commands, separating the positions that stay fixed from the ones that vary. Devices are keyed by vendor and product, not a bus address that changes on replug.
Write it down
Out comes a structured device record, with every conclusion pointing back at the packets behind it.

The codebase
What is in the repository
The package is laid out the same way the data moves through it. Each layer below is its own module, and the two interfaces at the bottom drive the same session, so a person at the command line and an agent over MCP get the same answers from the same code.
- Capturesniff_command.py, detect_device.py, usb_enum.py
Records one bus to a pcap-ng file, and works out which attached device is yours by taking a snapshot before and after you plug it in.
- Parsepcapng_reader.py
Opens the pcap-ng container and hands out the raw packet blocks inside it.
- Decodeurb_decoder.py
Turns the raw bytes of each packet into a typed record, and pairs every request with the response that answered it. Control, bulk and interrupt are handled. Isochronous raises instead of decoding badly.
- Sessionsession.py
Holds the loaded capture in memory, along with the devices and endpoints found in it and the markers that tie a physical action to a moment in the recording.
- Interface__main__.py, mcp/server.py, mcp/tools/
The command line for a person, with parse, sniff, detect-device and mcp as its subcommands, and the MCP server for an agent, with one module per group of tools it is allowed to call.
Verification and validation
We pointed it at a relay and it read the protocol back to us
A tool like this is easy to describe and hard to believe, so we decided the demo had to be a real decode from live captures, with nothing prepared in advance.
Our other reference device was a Goodix fingerprint reader, which is much closer to the kind of target the tool exists for, because it is common hardware with no public protocol behind it. Its captures are published in the repository.
Validation worked by running the analyzer against hardware whose driver is already public and comparing what it told us to the documented answer, so that we were checking our output against a known ground truth rather than against our own expectations. If it cannot rediscover a protocol that somebody has already written down, then it has no business being trusted on one that nobody has.

Scope management
Deciding what not to build
Scope creep is the thing that kills a project like this, and working with AI makes it worse rather than better, because a feature that used to cost a week now costs an afternoon and so every idea looks cheap enough to say yes to. The job was to hold the line on what bsu-tool is, which is a harness for reverse engineering, and to keep everything that is not that out of the term.
What bsu-tool does
- Reads control, bulk and interrupt transfers. These are the three transfer types that carry the commands on the kind of device we care about.
- Targets vendor-specific protocols. Devices with no published specification, where there is nothing to look up.
- Checks itself against known devices. Hardware with a public driver is the ground truth we validate against.
What it leaves alone, on purpose
- Isochronous transfers. Audio and video streaming. Real work, and a term of its own.
- Standard device classes. Keyboards, storage, audio and the like already have specifications, so decoding them proves nothing.
- Writing drivers. bsu-tool helps you understand a protocol. It does not generate code from that understanding, and a generated driver would hide how much of the protocol is still a guess. That is a project for whoever picks this up next.
Data handling
Captures come off real devices, so they get sanitized
A USB capture is a recording of a real device attached to a real person's machine, so every capture in the repository is sanitized before it goes in.
We also keep device serial numbers out of the identifiers we key on, because an identifier propagates into every packet record and every saved session, and a serial number is a fingerprint for one particular unit. Vendor and product are enough to know what a device is, and they do not single out whose it was.
Process and delivery
How a change got from an idea to main
Seven people on a codebase that another team would inherit, most of them writing a lot of their code with an AI assistant. That combination needs a process strict enough that the quality does not depend on who is paying attention that week. This is the path every change took.
- It starts as a GitHub issue. Nothing gets built that is not written down first, and the issue is where the scope of the change gets agreed before anyone opens an editor.
- It gets its own branch, named for the issue. Nobody commits to main directly. The branch name carries the issue number, so a year from now you can trace any line of code back to the conversation that asked for it.
- Four checks run locally before a push. Formatting and linting with ruff, static type checking with pyright, the test suite with pytest, and a docstring check that fails if any public function is missing its documentation. A pre-commit hook runs them so nobody has to remember.
- It goes up as a pull request and a teammate reads it. Every change is reviewed by at least one other person, and the rule I held to hardest was that nothing an AI wrote merges until a human has read every line of it.
- CI runs the same four checks again.On every push, in a clean environment, so a change that only works on one person's machine cannot get in.
- Merging closes the issue. Which keeps the board honest, because the only way an issue closes is with the code that resolves it.
The cadence around it
Work was planned in sprints against four milestones. The first was the foundation, the capture reader, the decoder and the requirements specification, with CI in place from the start. The second was the MCP server and the session model. The third was protocol analysis and the end-to-end demo, which is what the whole project was pointed at. The fourth was polish and delivery, and we planned it from the beginning as the milestone we would cut from if we had to. Our two faculty sponsors reviewed progress every week, on an agenda we set.
Handoff and maintainability
Leaving it in a state someone else can pick up
The project continues after we graduate, so the last stretch was spent on the things that let a stranger start where we stopped. That meant the README and the user guide, the wiki with the decisions log and the reading list, the sanitized reference captures published in the repository so anyone can check our work, and the open-source release itself, dual licensed under MIT and Apache 2.0.
The idea I would most like someone to take up is a collection of device records kept in the repository, so that every device somebody decodes with this becomes documentation the next person can read instead of working it out again.
The walkthrough

