This open-source project is available on GitHub at: https://github.com/vhp8rc7p/hackster/tree/main/MyPalletizerRoboFlow
Watch the demo here:
The "Is Claude Mad at Me?" SyndromeDo you find yourself running "/usage" every 5 minutes? Are you constantly checking if you're about to hit that 5-hour limit? You're not alone. We call it "Claude Code Anxiety." I saw the amazing "Clawdmeter" project—a dedicated M5Stack dashboard just for tracking token usage. It was beautiful, but then I looked at my desk. My 4-DOF robot arm myPalletizer 260 has a screen. It has Bluetooth. It has a literal "head." Why build a new dashboard when I can turn my robot into a living, breathing, usage-tracking sidekick? The goal was simple: stop typing "/usage" and start letting the robot tell us how we're doing.
The Library Stack Challenge: Old vs. NewThe myPalletizer 260 Basic firmware is open-source (which is great!), but it hasn't been updated in years and relies on the standard Arduino IDE/libraries. The original Clawdmeter project, on the other hand, was built with a much more modern stack: PlatformIO, ESP-IDF, and LVGL for graphics.
This meant I couldn't just drop the Clawdmeter code in. While I used it as a blueprint for the logic and API payloads, I had to manually reimplement the UI rendering and BLE setup from scratch using the older Arduino libraries available for the M5Stack Basic. It was a true porting effort from the ground up.
Adapting a "Premium" UI to the M5StackI had to port the high-res AMOLED look to the M5Stack's 320x240 SPI screen using only the M5Stack.h library—basically drawing by hand with basic primitives.
Nailing the Color Scheme: Instead of guessing "orange, " I pulled the exact hex codes from Anthropic’s brand identity and converted them to RGB565 format for the M5Stack (e.g., #d97757 becomes 0xDBAB). I also had to mix and match font sizes (like size 3 for the big percentages) to keep it readable from across the desk.
Layout Math (320x240): I set up constants for margins and panel widths (PANEL_W, PANEL_H) so the layout stays tidy instead of hardcoding absolute pixel coordinates. Since M5Stack.h is low-level, I wrapped drawRoundRect and fillRoundRect into helper functions like drawPanel and drawPill to make the UI code cleaner.
Visualizing "Anxiety": The Usage Bar Logic: To make the data readable at a glance, I wrote a helper function (pct_color) that returns a different RGB565 color based on the usage integer: Sage Green for normal use, Terra-cotta when you're pushing it, and a harsh Red when you hit the "Anxiety Zone."
Flicker-Free UI Updates: One of the most annoying things about simple LCD projects is screen flicker when refreshing data. I used a simple state check (changed = (usage_data.ok != last_ok)) to ensure the M5.Lcd.fillRect commands only fire when new BLE data actually arrives, keeping the UI rock solid.
The Dual Animation System (Pixels & Steel)This is where the project really comes to life, combining screen graphics with physical servo movement.
The Screen (Pixel Art Animation): A huge shoutout to @amaanbuilds and the claudepix library for the amazing pixel-art Clawd animations! To fit a square animation on a landscape screen, I used some scaling math. A "pixel" on the physical M5Stack screen is just a tiny LED dot. If we drew the 20x20 Clawd animation 1:1, it would be a microscopic speck! To scale it up, I took each "logical" pixel and drew it as a 12x12 block of physical pixels (20 blocks * 12px = 240px). Centering it on the 320px wide screen just required a simple 40px X-offset.
Memory Optimization (Why not a GIF?): You might wonder, "Why not just put a GIF on an SD card?" While possible, it's inefficient for an ESP32. Decoding a GIF requires heavy libraries and burns CPU cycles. Plus, reading from an SD card and writing to the screen forces them to share the SPI bus, causing lag. Instead, I stored the frames as lightweight 2D byte arrays in PROGMEM and drew them instantly using fillRect.
// Rendering the 20x20 pixel-art frame by frame
void ClaudeMode::drawSplash() {
const uint8_t *frame = cur_frames[current_frame];
for (int gy = 0; gy < GRID_SIZE; gy++) {
for (int gx = 0; gx < GRID_SIZE; gx++) {
uint8_t idx = pgm_read_byte(&frame[gy * GRID_SIZE + gx]);
uint16_t color = anim_palette[idx];
M5.Lcd.fillRect(
SPLASH_X + gx * CELL_SIZE,
SPLASH_Y + gy * CELL_SIZE,
CELL_SIZE, CELL_SIZE, color);
}
}
}The Core Illusion of Animation: At its most basic level, an animation is simply drawing a specific arrangement of pixels at a specific timestamp. In my code, this boils down to three pieces: an array of pixel maps (frames), an array of durations (hold times), and a current_frame counter.
Making it Move: I set up a 2D array (anim_poses) containing a sequence of joint angles. When Claude gets to work, the robot loops through these poses to simulate a 'nodding' or 'working' motion. I used direct joint angle control rather than complex inverse kinematics (IK)—it's simpler and guarantees the arm won't hit its own base.
// Poses for the "Thinking" animation (J1, J2, J3, J4, Speed)
static const float anim_poses[ANIM_FRAME_COUNT][5] = {
{ 0, 45, 30, 0, 40}, // 0 center neutral
{ 15, 50, 25, 20, 35}, // 1 lean right, nod
{ 30, 55, 20, 40, 35}, // 2 more right
{ 40, 60, 15, 60, 30}, // 3 right peak
// ... continues for 16 frames
};Syncing Screen and Steel: Every time the animation timer ticks, the code does two things: it draws the next frame (drawSplash()) and immediately sends the joint angles to the arm (myCobot.writeAngles()). From a software perspective they are synced, but in reality, the physical servos cannot move as fast as the screen refreshes. Because we are firing new coordinates before the arm has finished its previous move, the resulting physical motion is noticeably jerky—but it works!
The original project used Linux-specific tools (bluetoothctl and D-Bus). Since I'm on a Mac, I rewrote the daemon in Python using the bleak library.
Asynchronous Architecture: BLE can be finicky. Using Python's asyncio lets the daemon maintain the connection in the background while the main loop sleeps between 15s polls.
GATT Service Discovery: The script scans for the specific Service UUID (4c41555a...) instead of just trusting the name, then dynamically finds the RX characteristic.
Keychain Security (macOS Specific): To avoid storing API keys in plain text, I used subprocess to call security find-generic-password. This pulls your Claude token straight from the macOS Keychain. (Code Snippet: Show the read_token() Python function)
Polling the API every 15s is fine for usage bars, but it feels laggy for physical reactions. The next iteration will use Claude Code's hooks.json to send an instant 'Thinking' signal over BLE the millisecond you hit enter. I also want to add a physical "Stop" button on the robot that actually kills the Claude process via BLE TX back to the host.
Developers are welcome to participate in our User Case Initiative and showcase your innovative projects: https://www.elephantrobotics.com/en/call-for-user-cases-en/.




Comments