Flash writes a precompiled image over USB-C in Chrome or Edge. The .ino below is the source — you do not need Arduino IDE unless you want to change the sketch.
A round conference badge. Face A is a big first name, smaller last name, title, and a battery pip. Tap to flip to Face B: a QR-like mark, an @handle the program invents from your name, battery percent, and SAY HI.
There is no on-screen keyboard. You set the name over serial before you walk into the hall. The name survives a reboot.
What you will learn
How strings (text) are stored and split on a space.
How serial is a conversation: you type a command, the board stores it.
How a handle like @adalovelace can be built with a loop.
How a battery percentage is estimated from a voltage.
How settings are saved so they outlive a reset.
Meet the Desk Pal puck
This project uses the Waveshare ESP32-S3-Touch-LCD-1.28 — a round gadget about the size of a drink coaster.
A 240×240 color screen (think 240 dots across and 240 down).
A touch glass, so a finger is a mouse.
A tiny motion sensor (IMU) that feels shakes and flips.
Wi-Fi that only speaks 2.4 GHz. School guest networks that are 5 GHz only will be invisible to it.
A USB-C port for power and for loading the program.
The program you load is a .ino file — that is Arduino-style C++. You do not need to understand every line. This guide teaches the ideas, then shows the few lines that make each idea real.
Note
Word to know — firmware. That is just the program living on the chip. Flashing firmware is like installing an app, except the chip can only run one app at a time.
The finished gadget
Desk Pal conference badge on Desk Pal puck (ESP32-S3).
Build it
1
Print the bezel
0.2 mm, 3 walls. Add a printed tab or a 1.5 mm hole if you want a lanyard. Contrast PLA on the tab reads as a clip.
2
Flash the puck
Flash means copy the program onto the chip. Easiest path: use the attached conference-badge.bin with the on-page Flash to board button (Chrome or Edge, USB-C). To change the code later: Arduino IDE → ESP32S3 Dev Module, OPI PSRAM, 16 MB flash, USB CDC On Boot — or unzip platformio.zip next to the .ino and run python3 -m platformio run -t upload. The TFT setup must include USE_HSPI_PORT or the round screen stays black.
3
Set your name
Serial 115200, then name Your Name and title Your Title. Tap to flip faces. Wear it USB-down so the name is upright. The published default is Ada Lovelace on purpose — not a real person at your school.
Lesson 1 — Text is data too
A string is a sequence of characters. "Ada Lovelace" is a string. The program finds the first space and splits: first name Ada, last name Lovelace. If you only send one word, the last name stays empty and the layout still works.
Computers are picky. name is the command; everything after the first space is the payload. That pattern — verb plus argument — is how a lot of tools work (git commit, color cyan, range 10).
Verb + argument
serial-example.txt
name Ada Lovelace
title Engineer
flip
status
Lesson 2 — Building an @handle with a loop
Face B needs a handle. The firmware walks every letter of first+last name, keeps letters and digits, lowercases them, and sticks @ on the front. Spaces and punctuation drop out. Ada Lovelace becomes @adalovelace.
That is a filter loop: look at each character, decide keep or skip. You will write this kind of loop for cleaning user input forever.
Keep letters, ditch the rest
conference-badge.ino
handle = "@";
for each character c in firstName + lastName:
if c is A-Z: append lowercase
if c is a-z or 0-9: append it
// spaces, hyphens, emoji: skip
Lesson 3 — Battery is a voltage, percent is a guess
A LiPo cell is about 3.3 V empty and 4.2 V full. The board divides that voltage so the chip’s analog pin can read it safely, then the program maps the voltage onto 0–100%. If you are on USB with no pack, the pip hides.
Percent is an estimate, not a chemistry lab. Still: you used a sensor, scaled a number, and showed it. That is measurement.
Lesson 4 — Save files on a chip
persist() writes the name and title into Preferences. Unplug, plug back in, Ada is still Ada (or you are still you). RAM forgot; flash remembered.
Try this
Set a two-word name and a one-word name. Watch Face A’s layout change.
Predict the handle for Mary-Jane Watson before you type it.
Type flip without touching the glass.
Talking to the board (serial)
Your computer can talk to the board over the same USB-C cable. That text chat is called serial. In Arduino IDE or PlatformIO, open the Serial Monitor at 115200 baud (that number is the speed — both sides must match).
Type a word, press Enter, and the board answers. Try help first. Then try status.
Commands for this project:name … · title … · flip · status · help
Tip
If you see nothing, pick the right USB port, set 115200, and press the board’s RESET once. On a Mac the port often looks like /dev/cu.usbmodem….
Warning
Always set the name over serial before a real event. There is no keyboard on the glass.
The complete program (reference)
Everything above taught the ideas. Below is the full sketch so you can search, copy, and tinker. It is long on purpose — that is a real program, not a toy snippet. Scroll inside the box. The downloadable .ino is the same file.
Wi-Fi and API fields are placeholders (YOUR_WIFI_SSID_HERE, YOUR_WIFI_PASSWORD_HERE). Never paste a real password into a public copy.
conference-badge.ino (complete)
conference-badge.ino
// Conference / name badge for Waveshare ESP32-S3-Touch-LCD-1.28
// 1.28" round GC9A01, CST816S touch, QMI8658 IMU, LiPo charger + GPIO1 battery ADC
// (Amazon B0CM68M8LR). 200k/100k divider: V = 3.3 / 4096 * 3 * adc.
//
// Face A: large first name, smaller last name, title, battery pip.
// Face B: tap to flip — QR-like mark, @handle, battery %, "SAY HI".
// Edit on device is Serial-only (no long-press editor):
// name Ada Lovelace
// title Engineer
// Persist in Preferences. Soft idle: slow breathing ring + orbit dots.
//
// Serial 115200: name, title, flip, status, help
// Screen stays USB-down (no IMU auto-rotate). Tap still flips the face.
//
// Arduino IDE: ESP32S3 Dev Module, OPI PSRAM, 16MB flash, USB CDC On Boot.
// TFT_eSPI: apply board-sketches/conference-badge/firmware/TFT_eSPI_Setup.h
// Flash: cd board-sketches/conference-badge/firmware && pio run -t upload
// Sim: python .cursor/skills/board-firmware-sim/scripts/serve_sim.py board-sketches/conference-badge
#include <Arduino.h>
#include <Wire.h>
#include <TFT_eSPI.h>
#include <Preferences.h>
#include <math.h>
#include "../../../shared/s3_puck_pins.h"
#ifndef DEFAULT_NAME
#define DEFAULT_NAME "Ada Lovelace"
#endif
#ifndef DEFAULT_TITLE
#define DEFAULT_TITLE "Engineer"
#endif
static const uint32_t BADGE_NAME_MAX = 40;
static const uint32_t TITLE_MAX = 32;
static const uint32_t FLIP_LOCK_MS = 280;
static const uint32_t BAT_PERIOD_MS = 1500;
static const float BAT_V_EMPTY = 3.30f;
static const float BAT_V_FULL = 4.20f;
static const float BAT_V_PRESENT = 2.80f;
static const float BREATH_SEC = 3.6f;
// Flip these if touch coordinates feel mirrored on the real board.
static const bool TOUCH_FLIP_X = false;
static const bool TOUCH_FLIP_Y = false;
TFT_eSPI tft;
TFT_eSprite spr(&tft);
Preferences prefs;
static String fullName;
static String title;
static String firstName;
static String lastName;
static String handle;
static bool faceB = false;
static bool touchOk = false;
static uint8_t imuAddr = 0;
static bool imuOk = false;
static volatile bool touchIrq = false;
static bool fingerDown = false;
static bool longPressFired = false;
static int16_t touchX = 0, touchY = 0, touchX0 = 0, touchY0 = 0;
static uint32_t fingerDownAt = 0;
static uint32_t flipLockUntil = 0;
static int batAdc = 0;
static float batteryV = 0;
static int batteryPct = 0;
static bool batteryPresent = false;
static uint32_t nextBatteryAt = 0;
static String serialLine;
static uint16_t col(uint8_t r, uint8_t g, uint8_t b) { return spr.color565(r, g, b); }
static uint16_t COL_BG;
static uint16_t COL_FIELD;
static uint16_t COL_INK;
static uint16_t COL_DIM;
static uint16_t COL_GOLD;
static uint16_t COL_RED;
static uint16_t COL_OK;
static void initColors() {
COL_BG = col(8, 12, 22);
COL_FIELD = col(12, 20, 40);
COL_INK = col(246, 241, 232);
COL_DIM = col(148, 156, 176);
COL_GOLD = col(255, 194, 74);
COL_RED = col(220, 56, 56);
COL_OK = col(64, 196, 120);
}
static void splitName(const String &full) {
String s = full;
s.trim();
const int sp = s.indexOf(' ');
if (sp < 0) {
firstName = s;
lastName = "";
} else {
firstName = s.substring(0, sp);
lastName = s.substring(sp + 1);
firstName.trim();
lastName.trim();
}
}
static void rebuildHandle() {
String raw = firstName + lastName;
handle = "@";
for (unsigned i = 0; i < raw.length() && handle.length() < 18; i++) {
const char c = raw[i];
if (c >= 'A' && c <= 'Z') handle += (char)(c - 'A' + 'a');
else if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) handle += c;
}
if (handle.length() < 2) handle = "@hi";
}
static void persist() {
prefs.putString("name", fullName);
prefs.putString("title", title);
}
static void applyName(const String &value, bool save) {
fullName = value;
fullName.trim();
if (fullName.length() > (int)BADGE_NAME_MAX) fullName = fullName.substring(0, BADGE_NAME_MAX);
if (!fullName.length()) fullName = DEFAULT_NAME;
splitName(fullName);
rebuildHandle();
if (save) persist();
}
static void applyTitle(const String &value, bool save) {
title = value;
title.trim();
if (title.length() > (int)TITLE_MAX) title = title.substring(0, TITLE_MAX);
if (save) persist();
}
static void flipFace() {
faceB = !faceB;
flipLockUntil = millis() + FLIP_LOCK_MS;
}
static int batPercentFromV(float v) {
float p = (v - BAT_V_EMPTY) * (100.0f / (BAT_V_FULL - BAT_V_EMPTY));
if (p < 0) p = 0;
if (p > 100) p = 100;
return (int)(p + 0.5f);
}
static void updateBattery(uint32_t now) {
if ((int32_t)(now - nextBatteryAt) < 0) return;
nextBatteryAt = now + BAT_PERIOD_MS;
batAdc = analogRead(S3_BAT_ADC_PIN);
const float v = 3.3f / (float)(1 << 12) * 3.0f * (float)batAdc;
batteryV = batteryV == 0 ? v : (batteryV * 0.7f + v * 0.3f);
batteryPresent = batteryV >= BAT_V_PRESENT;
batteryPct = batteryPresent ? batPercentFromV(batteryV) : 0;
}
// ---------------------------------------------------------------------- IMU
static bool imuWrite(uint8_t reg, uint8_t val) {
Wire.beginTransmission(imuAddr);
Wire.write(reg);
Wire.write(val);
return Wire.endTransmission() == 0;
}
static bool imuReadBytes(uint8_t reg, uint8_t *buf, uint8_t n) {
Wire.beginTransmission(imuAddr);
Wire.write(reg);
if (Wire.endTransmission(false) != 0) return false;
if (Wire.requestFrom((int)imuAddr, (int)n) != n) return false;
for (uint8_t i = 0; i < n; i++) buf[i] = Wire.read();
return true;
}
static bool imuProbe(uint8_t addr) {
imuAddr = addr;
uint8_t id = 0;
if (!imuReadBytes(S3_QMI_WHO_AM_I, &id, 1)) return false;
return id == S3_QMI_ID;
}
static bool imuBegin() {
if (!imuProbe(0x6B) && !imuProbe(0x6A)) {
imuAddr = 0;
return false;
}
imuWrite(S3_QMI_CTRL1, 0x60); // auto-increment
imuWrite(S3_QMI_CTRL2, 0x23); // ±8g
imuWrite(S3_QMI_CTRL3, 0x43); // ±256 dps
imuWrite(S3_QMI_CTRL7, 0x03); // accel + gyro on
delay(20);
return true;
}
// ------------------------------------------------------------------- touch
static void touchReset() {
pinMode(S3_TP_RST_PIN, OUTPUT);
pinMode(S3_TP_INT_PIN, INPUT_PULLUP);
digitalWrite(S3_TP_RST_PIN, LOW);
delay(12);
digitalWrite(S3_TP_RST_PIN, HIGH);
delay(80);
}
static bool touchWrite(uint8_t reg, uint8_t val) {
Wire.beginTransmission(S3_CST816S_ADDR);
Wire.write(reg);
Wire.write(val);
return Wire.endTransmission() == 0;
}
static bool touchBegin() {
Wire.beginTransmission(S3_CST816S_ADDR);
Wire.write(0xA7);
if (Wire.endTransmission(false) != 0) return false;
if (Wire.requestFrom((int)S3_CST816S_ADDR, 1) != 1) return false;
Wire.read();
touchWrite(0xFE, 0x01);
touchWrite(0xFA, 0x60);
return true;
}
static bool readTouch(uint8_t *fingers, int16_t *x, int16_t *y) {
Wire.beginTransmission(S3_CST816S_ADDR);
Wire.write(0x02);
if (Wire.endTransmission(false) != 0) return false;
if (Wire.requestFrom((int)S3_CST816S_ADDR, 5) < 5) return false;
*fingers = Wire.read() & 0x0F;
const uint8_t xh = Wire.read();
const uint8_t xl = Wire.read();
const uint8_t yh = Wire.read();
const uint8_t yl = Wire.read();
int16_t px = ((xh & 0x0F) << 8) | xl;
int16_t py = ((yh & 0x0F) << 8) | yl;
if (TOUCH_FLIP_X) px = S3_LCD_SIZE - 1 - px;
if (TOUCH_FLIP_Y) py = S3_LCD_SIZE - 1 - py;
*x = px;
*y = py;
return true;
}
static void IRAM_ATTR onTouchIrq() { touchIrq = true; }
static void onTap() {
if ((int32_t)(millis() - flipLockUntil) < 0) return;
flipFace();
}
static void handleTouch(uint32_t now) {
if (!touchOk) return;
const bool irq = touchIrq || digitalRead(S3_TP_INT_PIN) == LOW;
touchIrq = false;
if (!irq && !fingerDown) return;
uint8_t fingers = 0;
int16_t x = 0, y = 0;
const bool ok = readTouch(&fingers, &x, &y);
const bool down = ok && fingers > 0;
if (down) {
touchX = x;
touchY = y;
if (!fingerDown) {
fingerDown = true;
fingerDownAt = now;
touchX0 = x;
touchY0 = y;
longPressFired = false;
} else if (!longPressFired && (now - fingerDownAt) > 700 &&
abs(x - touchX0) < 24 && abs(y - touchY0) < 24) {
// Long-press is intentionally a no-op — edit via Serial / #defines.
longPressFired = true;
}
return;
}
if (fingerDown) {
fingerDown = false;
if (longPressFired) return;
const int16_t dx = touchX - touchX0;
const int16_t dy = touchY - touchY0;
if (abs(dx) <= 40 && abs(dy) <= 40 && (now - fingerDownAt) < 600) onTap();
}
}
// ------------------------------------------------------------------- draw
static int fitSize(const String &text, uint8_t font, uint8_t maxSize, int maxW) {
for (int s = maxSize; s >= 1; s--) {
spr.setTextFont(font);
spr.setTextSize(s);
if (spr.textWidth(text) <= maxW) return s;
}
return 1;
}
static int drawFitted(const String &text, int x, int y, uint8_t font, uint8_t maxSize, int maxW,
uint16_t color) {
if (!text.length()) return 0;
const int s = fitSize(text, font, maxSize, maxW);
spr.setTextFont(font);
spr.setTextSize(s);
spr.setTextDatum(MC_DATUM);
spr.setTextColor(color);
spr.drawString(text, x, y);
return s;
}
static uint32_t hashName() {
uint32_t h = 2166136261u;
for (unsigned i = 0; i < fullName.length(); i++) {
h ^= (uint8_t)fullName[i];
h *= 16777619u;
}
for (unsigned i = 0; i < title.length(); i++) {
h ^= (uint8_t)title[i];
h *= 16777619u;
}
return h;
}
static bool inFinder(int x, int y, int ox, int oy) {
return x >= ox && x < ox + 5 && y >= oy && y < oy + 5;
}
static bool finderOn(int x, int y, int ox, int oy) {
const int lx = x - ox;
const int ly = y - oy;
const int ring = (lx == 0 || lx == 4 || ly == 0 || ly == 4);
const int core = (lx >= 2 && lx <= 2 && ly >= 2 && ly <= 2);
return ring || core;
}
static bool qrModule(int x, int y, uint32_t h) {
if (inFinder(x, y, 0, 0)) return finderOn(x, y, 0, 0);
if (inFinder(x, y, 8, 0)) return finderOn(x, y, 8, 0);
if (inFinder(x, y, 0, 8)) return finderOn(x, y, 0, 8);
if ((y == 6 && x < 8) || (x == 6 && y < 8)) return (x + y) & 1;
uint32_t v = h ^ (uint32_t)(x * 131 + y * 17);
v *= 16777619u;
return ((v >> 24) & 1) != 0;
}
static void drawQr(int cx, int cy, uint16_t ink, uint16_t paper) {
const int n = 13;
const int cell = 6;
const int side = n * cell;
const int x0 = cx - side / 2;
const int y0 = cy - side / 2;
spr.fillRoundRect(x0 - 6, y0 - 6, side + 12, side + 12, 6, ink);
spr.fillRoundRect(x0 - 3, y0 - 3, side + 6, side + 6, 4, paper);
const uint32_t h = hashName();
for (int y = 0; y < n; y++) {
for (int x = 0; x < n; x++) {
if (qrModule(x, y, h)) {
spr.fillRect(x0 + x * cell, y0 + y * cell, cell - 1, cell - 1, ink);
}
}
}
}
static void drawBreathRing(uint32_t now) {
const float t = now / 1000.0f;
const float s = 0.5f + 0.5f * sinf(t * (2.0f * 3.14159265f / BREATH_SEC));
const uint8_t r = (uint8_t)(190 + s * 65);
const uint8_t g = (uint8_t)(130 + s * 60);
const uint8_t b = (uint8_t)(28 + s * 28);
const uint16_t c = col(r, g, b);
const int thick = 3 + (int)(s * 2.5f);
for (int i = 0; i < thick; i++) spr.drawCircle(120, 120, 116 - i, c);
for (int i = 0; i < 3; i++) {
const float ang = t * 0.55f + i * (2.0f * 3.14159265f / 3.0f);
const int x = 120 + (int)(cosf(ang) * 110.0f);
const int y = 120 + (int)(sinf(ang) * 110.0f);
spr.fillCircle(x, y, 2, COL_GOLD);
}
}
static void drawBatPip(int cx, int cy) {
const int w = 34;
const int h = 13;
const uint16_t frame = COL_INK;
const uint16_t fill = !batteryPresent ? COL_DIM : (batteryPct < 20 ? COL_RED : COL_GOLD);
spr.drawRoundRect(cx - w / 2, cy - h / 2, w, h, 3, frame);
spr.fillRect(cx + w / 2, cy - 3, 3, 6, frame);
const int inner = batteryPresent ? ((w - 4) * batteryPct / 100) : (w - 4);
if (inner > 0) {
spr.fillRect(cx - w / 2 + 2, cy - h / 2 + 2, inner, h - 4, fill);
}
}
static void drawFaceA() {
String first = firstName;
String last = lastName;
first.toUpperCase();
last.toUpperCase();
const int firstSize = drawFitted(first, 120, 86, 2, 5, 188, COL_INK);
if (last.length()) {
int lastMax = firstSize > 1 ? firstSize - 1 : 1;
if (lastMax > 3) lastMax = 3;
drawFitted(last, 120, 132, 2, lastMax, 188, COL_INK);
}
spr.drawFastHLine(70, 154, 100, COL_GOLD);
String t = title;
t.toUpperCase();
int titleMax = 2;
if (firstSize <= 2) titleMax = 1;
drawFitted(t, 120, 176, 2, titleMax, 176, COL_GOLD);
drawBatPip(120, 208);
}
static void drawFaceB() {
drawQr(120, 92, COL_INK, COL_FIELD);
spr.setTextDatum(MC_DATUM);
spr.setTextFont(2);
spr.setTextSize(1);
spr.setTextColor(COL_DIM);
spr.drawString(handle, 120, 154);
spr.setTextColor(batteryPresent ? (batteryPct < 20 ? COL_RED : COL_GOLD) : COL_DIM);
spr.setTextSize(2);
if (batteryPresent) {
char buf[12];
snprintf(buf, sizeof(buf), "%d%%", batteryPct);
spr.drawString(buf, 120, 176);
} else {
spr.drawString("USB", 120, 176);
}
spr.setTextColor(COL_INK);
spr.setTextSize(2);
spr.drawString("SAY HI", 120, 206);
}
static void drawBadge(uint32_t now) {
spr.fillSprite(TFT_BLACK);
spr.fillCircle(120, 120, 120, COL_BG);
spr.fillCircle(120, 120, 112, COL_FIELD);
if (faceB) drawFaceB();
else drawFaceA();
drawBreathRing(now);
spr.pushSprite(0, 0);
}
// ------------------------------------------------------------------ serial
static void printStatus() {
char batBuf[12];
if (batteryPresent) snprintf(batBuf, sizeof(batBuf), "%d%%", batteryPct);
else snprintf(batBuf, sizeof(batBuf), "USB");
Serial.printf("face=%s name=\"%s\" title=\"%s\" handle=%s bat=%.2fV %s adc=%d touch=%d imu=%d\n",
faceB ? "B" : "A", fullName.c_str(), title.c_str(), handle.c_str(), batteryV, batBuf,
batAdc, touchOk ? 1 : 0, imuOk ? 1 : 0);
}
static void printHelp() {
Serial.println("name <first last> | title <text> | flip | status | help");
Serial.println("name Ada Lovelace");
Serial.println("title Engineer");
}
static void handleSerial() {
while (Serial.available()) {
const char c = (char)Serial.read();
if (c == '\r') continue;
if (c != '\n') {
if (serialLine.length() < 96) serialLine += c;
continue;
}
serialLine.trim();
if (serialLine.length()) {
const int sp = serialLine.indexOf(' ');
String cmd = sp < 0 ? serialLine : serialLine.substring(0, sp);
String arg = sp < 0 ? "" : serialLine.substring(sp + 1);
cmd.toLowerCase();
arg.trim();
if (cmd == "name") {
if (!arg.length()) Serial.println("usage: name Ada Lovelace");
else {
applyName(arg, true);
Serial.printf("name=%s handle=%s\n", fullName.c_str(), handle.c_str());
}
} else if (cmd == "title") {
if (!arg.length()) Serial.println("usage: title Engineer");
else {
applyTitle(arg, true);
Serial.printf("title=%s\n", title.c_str());
}
} else if (cmd == "flip") {
flipFace();
Serial.printf("face=%s\n", faceB ? "B" : "A");
} else if (cmd == "status") {
printStatus();
} else if (cmd == "help") {
printHelp();
} else {
Serial.println("? (try help)");
}
}
serialLine = "";
}
}
void setup() {
Serial.begin(115200);
pinMode(S3_LCD_BL_PIN, OUTPUT);
digitalWrite(S3_LCD_BL_PIN, HIGH);
tft.init();
tft.setRotation(0);
tft.fillScreen(TFT_BLACK);
spr.setColorDepth(16);
if (spr.createSprite(S3_LCD_SIZE, S3_LCD_SIZE) == nullptr) {
Serial.println("sprite alloc failed");
while (true) delay(1000);
}
initColors();
Wire.begin(S3_I2C_SDA_PIN, S3_I2C_SCL_PIN);
Wire.setClock(400000);
touchReset();
touchOk = touchBegin();
if (touchOk) attachInterrupt(digitalPinToInterrupt(S3_TP_INT_PIN), onTouchIrq, FALLING);
imuOk = imuBegin();
analogReadResolution(12);
analogSetPinAttenuation(S3_BAT_ADC_PIN, ADC_11db);
prefs.begin("badge", false);
applyName(prefs.getString("name", DEFAULT_NAME), false);
applyTitle(prefs.getString("title", DEFAULT_TITLE), false);
nextBatteryAt = 0;
updateBattery(millis());
Serial.println(touchOk ? "CST816S ready" : "CST816S not found — tap via Serial flip");
Serial.println(imuOk ? "QMI8658 ready" : "QMI8658 not found");
Serial.printf("badge ready name=%s title=%s (type help)\n", fullName.c_str(), title.c_str());
}
void loop() {
const uint32_t now = millis();
handleSerial();
handleTouch(now);
updateBattery(now);
drawBadge(now);
delay(33);
}
Board and housing
Desk Pal puck (ESP32-S3)Badge bezel + clip tabCAD of the snap-fit shell.