An Afternoon Skipping ROS2 Entirely to Talk to the Board in Raw Hex

No launch files, no nodes, no topics. Just a Type-C cable, a serial terminal, and every byte typed by hand.
Every ROS2 driver I've used for MentorPi's motors, servos, and LEDs has always sat between me and the actual hardware — clean topics, clean service calls, clean abstraction. Which is great for building things, and genuinely bad for actually understanding what's happening underneath. So one afternoon, I closed every ROS2 terminal, plugged a USB Type-C cable straight into the RRC Lite controller, opened a serial assistant tool at 1,000,000 baud in HEX mode, and decided to talk to the board the way it actually listens — one raw byte packet at a time.
Experiment 1: Understanding the Frame Structure
Before sending anything, I read through what the packet structure was actually supposed to look like: 0xAA 0x55 [Function code] [Data length] [Parameter...] [Checksum CRC]. The two fixed leading bytes, 0xAA 0x55, aren't part of the payload at all — they're a sync marker, and once I actually thought about why that matters, it stopped being a throwaway detail. Serial data arrives as one continuous stream of bytes with no inherent boundaries; without a reliable, distinctive marker at the start of every packet, the receiving side has no clean way to know "a new packet just started here" versus "this byte is still part of whatever came before it." Picking two fixed bytes unlikely to appear by coincidence at the start of legitimate payload data is what lets the parser resynchronize cleanly even if a previous packet got corrupted or truncated mid-stream.
Experiment 2: The LED Command, and the Genuinely Startling Moment It Actually Worked

First real test: PACKET_FUNC_LED, sending the exact byte sequence AA 55 01 07 01 64 00 64 00 05 00 37 — a command meant to blink the board's LED five times. I typed it into the serial assistant's hex send box, double-checked every pair of characters against the reference once more purely out of nervousness, and hit send.
The LED blinked. Five times, cleanly, exactly as specified. I've built plenty of things that eventually work after some debugging, but there's a specific kind of small delight in watching a real physical light respond to a dozen bytes I typed by hand into a text box, with absolutely no driver, no ROS2 node, no abstraction of any kind standing between my keystrokes and the actual hardware reacting. It's such a simple result, and it still felt disproportionately satisfying.
Experiment 3: The Buzzer, and Learning Checksum Math the Hard Way

Emboldened by the LED working, I moved on to PACKET_FUNC_BUZZER, intending to change the buzzer frequency parameter from the reference example. This is where the checksum stopped being a detail I could copy-paste past and became something I actually had to compute myself. Change any parameter byte in the packet, and the CRC checksum at the end has to change to match — the board isn't going to accept a packet where the payload and the checksum disagree, and it turns out it won't tell you why either. It just... says nothing back.
First attempt, changed the frequency parameter, forgot to recompute the checksum, sent it — silence. No buzz, no error message, nothing. Second attempt, computed what I thought was the correct checksum by hand, still silence. Went back and recomputed byte by byte, comparing against the working LED example's checksum calculation method line by line to make sure I actually understood the algorithm rather than half-remembering it, found an arithmetic slip in my second attempt, fixed it, sent a third time — and the buzzer sounded, at the new frequency, correctly. Nothing dramatic about this discovery, but it's exactly the kind of lesson that only actually sinks in when getting it wrong produces silence rather than a helpful error — the board isn't going to hold your hand here, and neither, it turns out, was I holding my own particularly well on the first two attempts.
Experiment 4: Motor Control, and Windows Calculator Becomes a Serious Tool
Motor control via PACKET_FUNC_MOTOR introduced a new problem that had nothing to do with checksums and everything to do with data representation: motor speed parameters needed to be sent as 4-byte little-endian floating point values, and I wanted to send a simple, clean -1.0 as a test value.
There is no elegant way to do this by hand without a tool that actually understands IEEE 754 float representation, so I ended up doing something that felt slightly absurd for how well it actually worked: opened Windows Calculator, switched it into Programmer mode, and used its floating-point conversion display to get the actual 4-byte hex representation of -1.0 — then had to remember to reverse the byte order for little-endian before dropping those bytes into the packet's parameter section. Got it wrong the first time by forgetting the byte-order reversal entirely, sending the bytes in the order Calculator displayed them (big-endian-style) rather than reversed — the motor did something, but not a clean -1.0 worth of anything, more like whatever nonsense value that misordered byte pattern actually represented as a float. Reversed the byte order, resent, and the motor spun at the correct,clean speed. A genuinely humbling reminder that "just send the float" involves an entire representation-format decision that a driver library normally makes invisibly on your behalf, every single time, without you ever having to think about it.
The Moment It All Clicked

Sitting back after the motor test actually worked, the thing that actually landed wasn't any single command — it was realizing, all at once, that the ROS2 driver package I use every single day for this exact hardware is, underneath all its clean topics and service interfaces, doing precisely this: building these exact same 0xAA 0x55-prefixed packets, computing these exact same checksums, packing these exact same little-endian floats, just automatically and correctly, every time, without me ever needing to think about a single byte of it.
I'd used that driver for weeks without a second thought about what was actually crossing the wire underneath it. Spending an afternoon typing the raw bytes by hand — including the silence after a bad checksum and the wrong motor speed after a byte-order mistake — didn't just teach me the protocol. It gave me a genuinely different kind of respect for how much invisible, correct, boring work a good driver abstraction is quietly doing every single time I call a clean, simple ROS2 service instead.