chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:31:02 +08:00
commit 1714bea19b
128 changed files with 613380 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
.vscode/launch.json
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+134
View File
@@ -0,0 +1,134 @@
# The Sesame Robot Project
___
![License](https://img.shields.io/badge/License-APACHE2.0-yellow)
![Microcontroller](https://img.shields.io/badge/Microcontroller-ESP32-blue)
![Firmware](https://img.shields.io/badge/Firmware-C%2B%2B-blue?logo=c%2B%2B)
![IDE](https://img.shields.io/badge/IDE-Arduino-00979D?logo=arduino&logoColor=white)
![GitHub stars](https://img.shields.io/github/stars/dorianborian/sesame-robot?style=social)
![GitHub forks](https://img.shields.io/github/forks/dorianborian/sesame-robot?style=social)
<img width="100%" height="728" alt="sesame-cover" src="https://github.com/user-attachments/assets/f0cc6ad0-135b-4515-8750-900f224ed7ae" />
<p align="center">
<a href="https://www.youtube.com/watch?v=NIgoQVQF_Ng">
<img src="https://github.com/user-attachments/assets/1663e022-0680-4053-97b4-53e669a6f07d" width="49%" alt="tutorial-button">
</a>
<a href="https://discord.gg/XDXkhQd8bC">
<img src="https://github.com/user-attachments/assets/378fcb48-5b12-4b46-9dcb-452432d49913" width="49%" alt="discord-button">
</a>
</p>
___
**Greetings, from your new best friend.**
Sesame is an accessible Open-Source robotics project based on the ESP32 microcontroller system, with an emphasis on expression and movement.
This project is designed for makers and engineers of all skill levels! Sesame offers a dynamic platform designed to start working with walking robots.
To build a sesame robot, you will need basic soldering skills, $50-60 in hardware components, access to a 3D printer, and a basic understanding of Arduino IDE.
This repository contains the CAD design files, STL files, build and wiring guides, and the base/expanded firmware for the ESP32-based controller.
There is also some included debugging firmware that may be helpful in getting your Sesame up and running.
## Features
* **Quadruped Design:** Uses 8 servo motors (2 per leg) to achieve roughly 8 total degrees of freedom.
* **Emotive Display:** Features a 128x64 OLED screen acting as a reactive face that syncs with movement.
* **Fully Printable:** Designed entirely for 3D printing in PLA with minimal supports.
* **Network Connectivity:** Connect to your WiFi network for remote control and API access.
* **JSON API:** RESTful API for programmatic control from Python, JavaScript, and more.
* **Conversational Faces:** Expressive emotion library with talk variants for voice assistant projects.
* **Sesame Studio:** New animation composer software to easily create custom movements.
* **Sesame Companion App:** Python application for voice control and advanced interactions.
* **Serial CLI:** Control the robot and trigger animations via a Serial Command Line Interface or the web UI.
* **Pre-programmed Emotes:** Includes animations for Walking, Waving, Dancing, Pointing, Resting, and more.
## Watch the launch video on YouTube
<a href="https://www.youtube.com/watch?v=1UDsWkcQZhc"><img src="https://github.com/user-attachments/assets/710cb5a6-163e-47e7-a294-5e2d2ab07627" width="70%" alt="thumb-youtube"></a>
___
## Getting Started
Follow these steps to build your own Sesame Robot:
### 1. Gather Parts
Check the **[Bill of Materials (BOM)](hardware/bom/README.md)** for a complete list of required electronics and hardware.
* Microcontroller: Lolin S2 Mini (recommended for DIY builds), Sesame Distro Board V3 (Current, pre-flashed, supports Bambu Lab battery), V2 (legacy, USB-only), or ESP32-DevKitC-32E with Distro Board V1 (legacy)
* Actuators: 8x MG90 Servos
* Power: 5V 3A source (USB-C PD for S2 Mini and V2 Distro Board, or battery + buck converter; see BOM for the Bambu Lab 14500 7.4V 800mAh Li-ion Battery option)
### 2. Print Parts
Download the STLs and follow the **[Printing Guide](hardware/printing/README.md)**.
* Designed for PLA
* Minimal supports required
### 3. Build & Wire
Follow the **[Build Guide](docs/build-guide/README.md)** and **[Wiring Guide](docs/wiring-guide/README.md)** to assemble the frame and connect the electronics.
### 4. Flash Firmware
Upload the code from the **[Firmware Directory](firmware/README.md)**.
* Requires Arduino IDE
* Configure WiFi AP settings
### 5. Create Animations
Use **[Sesame Studio](software/sesame-studio/README.md)** to visually design poses and sequences for your robot.
<img width="100%" height="728" alt="sesame-wakeup-gif" src="https://github.com/user-attachments/assets/a4951195-4253-40a4-a87d-d14fad57ff5f" />
---
## Software & Firmware
### Sesame Studio
Sesame Studio is a standalone desktop application included in `software/sesame-studio/`. It allows you to:
* Visually pose the robot using a schematic interface.
* Generate C++ code for servo angles automatically.
* Sequence frames into complex animations.
[**> Go to Sesame Studio**](software/sesame-studio/README.md)
### Sesame Simulator
The Sesame Simulator, created by Jay Li, is a Rust-based 3D simulation environment for testing Sesame's movements and kinematics in a virtual space. It features:
* **Physics-based Simulation:** Test walking and balance without hardware.
* **Web-based Interface:** Run the simulator directly in your browser.
* **URDF Integration:** Accurate modeling of Sesame's physical properties.
[**> Go to Sesame Simulator**](https://one-for-all.github.io/sesame-robot-sim/)
### Sesame Companion App
The Sesame Companion App is a Python-based application that enables advanced control and interaction with your robot over your local network. It leverages the new JSON API and network mode features to provide:
* **Voice Assistant Integration:** Control Sesame with voice commands and see real-time emotional expressions.
* **Remote Control:** Command your robot from anywhere on your local network.
* **Face Control:** Change expressions dynamically based on conversation or context.
* **API Examples:** Reference implementation for building your own integrations.
The Companion App works with robots running the latest firmware with network mode enabled.
[**> Go to Sesame Companion App Repository**](https://github.com/dorianborian/sesame-companion-app)
### Firmware
The ESP32 firmware (`sesame-firmware-main.ino`) handles the kinematics, face display, and WiFi control interface.
* **Web UI:** Control the robot from your phone via the built-in Access Point.
* **Custom Faces:** Add your own bitmaps (guide in firmware docs).
[**> Go to Firmware Docs**](firmware/README.md)
---
## Contributing
This robot is a platform for building new features, cosmetics, tools, and ideas. Since the current firmware is a basic implementation, pull requests are very welcome for:
* Kinematics improvements
* New animations
* Improved Web UI/UX
* Sensor integration (Ultrasonic, Gyro, etc.)
I would also love to see forks of this project with new hardware, software, faces, etc. Be sure to send me a message if you end up building one, and I might feature you on my website or channel!
---
*Created by [Dorian Todd](https://www.doriantodd.com/). Need help with your Sesame Robot? Send me a message on Discord, my username is "starphee"*
+7
View File
@@ -0,0 +1,7 @@
# WeHub 来源说明
- 原始项目:`dorianborian/sesame-robot`
- 原始仓库:https://github.com/dorianborian/sesame-robot
- 导入方式:上游默认分支的最新快照
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
+7
View File
@@ -0,0 +1,7 @@
# Documentation
Documentation for the Sesame Robot project.
- **build-guide/** - Build guide with photos
- **images/** - Photos and diagrams
- **wiring-guide/** - Wiring instructions
+277
View File
@@ -0,0 +1,277 @@
# Build Guide
Complete build guide for the Sesame Robot.
Use this walkthrough alongside the BOM, wiring guide, and printing notes to stay organized. The table below previews each phase, its goal, and the doc to reference if you get stuck.
| Phase | Goal | Est. Time | Key References |
| ------------------------- | ------------------------------------- | ------------- | ---------------------------------------------------------------------------------------------- |
| 1. Gather parts | Print plastics and source electronics | 12 sessions | [hardware/bom](../../hardware/bom/README.md), [hardware/printing](../../hardware/printing/README.md) |
| 2. Electronics & wiring | Build ~90% of the harness | 1 session | [docs/wiring-guide](../wiring-guide/README.md) |
| 3. Hardware pre-assembly | Prep joints, feet, top cover | 1 session | This guide (Phase 3) |
| 4. Hardware main assembly | Mount motors + electronics | 12 sessions | This guide (Phase 4) |
| 5. Calibrate & finish | Flash tester, align joints, close up | 1 session | Firmware README + remaining sections |
> [!TIP]
> Treat each phase like a milestone. Snap progress photos and mark issues before moving on so troubleshooting later is painless.
<p align="center">
<a href="https://www.youtube.com/watch?v=NIgoQVQF_Ng">
<img src="https://github.com/user-attachments/assets/1663e022-0680-4053-97b4-53e669a6f07d" width="70%" alt="tutorial-button">
</a>
</p>
## Phase 1: Gathering all the parts.
**Goal:** Print the full shell set and collect every electronic, connector, and fastener before any soldering starts.
**Youll need:**
- [hardware/bom](../../hardware/bom/README.md) for electronics, power gear, and Amazon search links.
- [hardware/printing](../../hardware/printing/README.md) for STL names, orientations, and support notes.
There are 11 printed parts (internal frame, top/bottom covers, joints R1R4/L1L4) plus 68 main electronic components depending on the wiring approach.
Here's what a complete set looks like:
<img src="assets/all-hardware.png" alt="all-hardware" width="70%">
**Phase 1 checklist**
- [ ] All plastic parts printed, and cleaned up.
- [ ] MG90 servos tested quickly on a servo tester or Arduino to catch DOA units.
- [ ] Power plan decided (USB-C PD vs. battery + buck) and matching connectors sourced (2× 14500 Li-ion cells in a 2× AAA holder fit the stock battery cavity).
- [ ] Consumables stocked: solder, flux, heat-shrink, zip ties, M2 hardware.
## Phase 2: Electronics and Wiring
**Goal:** Build 90% of the harness on the bench so the frame install is quick.
1. Open the [wiring guide](../wiring-guide/README.md) and pick the section that matches your build (S2 Mini hand-wired, Distro Board V3/V2, or Distro Board V1 legacy).
2. Lay out every connector in the order shown on the wiring diagram before soldering; this keeps the data lines from getting crossed.
3. Tin and solder the rails/buck converter first, then route signal wires. Leave generous length for the motors that terminate near the hips.
4. This is optional but you can also label each servo lead (S0S7) using tape flags as soon as it is soldered. Future you will thank you.
> [!WARNING]
> Stop before permanently wiring the power switch or OLED. Those final joints happen after the electronics are seated in the frame so you can dial the cable length exactly.
**Phase 2 checklist**
- [ ] Harness built per your wiring diagram with all joints strain-relieved.
- [ ] Buck converter trimmed to 5.1V output and shrink-wrapped.
- [ ] Servo leads labeled and loosely bundled by destination.
- [ ] Power switch and OLED leads pre-cut but still un-soldered.
## Phase 3: Hardware Pre-Assembly
### Hip Joints
**Goal:** Pre-load the four hip joints (R1, R2, L1, L2) with one-sided servo horns so installation is tool-free later.
1. Lay out the four hip joint parts. Find the one-sided servo horns from the servo motor bags (ignore the included extra screws).
2. Notice the servo horn has a taller side and a shorter side. Press the shorter side into the hip joint. There should be just enough clearance to press this piece in flush.
3. While holding the horn flat, drive an M2 × 5 mm self-threading screw (the ones with a larger gap between thread spikes) through the second hole in the horn and into the plastic until secure.
<img src="assets/joint-horn-install.png" alt="install-horn" width="70%">
> [!TIP]
> Do not over-tighten self-threading screws. Since the threads are plastic, over-tightening will permanently damage the hole.
Repeat for all four hip joints:
<img src="assets/femur-joints.png" alt="hip-joints" width="70%">
> [!IMPORTANT]
> Do **not** mount these joints on the servos yet. Calibration requires every motor shaft to spin freely.
<img src="assets/dont-mount.png" alt="dont-mount" width="70%">
### Leg Joints
Slide each leg shell over its dedicated motor before the frame install. Use the built-in gap to route wires without pinching them.
<img src="assets/servo-install-leg.png" alt="servo-install-leg" width="70%">
- Feed the motor wires through the side slot.
- Push the motor into the side of the leg piece that has holes in it. Make sure the motor shaft is at the very top.
- Confirm the motor sits flush and use self-threading screws to permanently affix the motors into the leg pieces.
Repeat for the remaining legs.
<img src="assets/foot-joints.png" alt="leg-joints" width="70%">
### Top Cover & Soldering
The next step is loading the OLED and power switch into the top cover. At this stage, we also do the minimal soldering required for the build.
1. **Power Switch:** Press the battery switch firmly into the back slot of the top cover. DO NOT connect the wires yet.
<img src="assets/switch-insert.png" alt="insert-rocker-switch" width="70%">
2. **OLED Display:** Apply a small amount of solder to the pin headers on the display (tinning). Using the pre-crimped JST connector wires, match the wires to their respective marks on the display and solder them.
<img src="assets/display-soldering.png" alt="display-soldering" width="70%">
3. **Battery Connectors (Main Board):** Remove sheathing on the ends of the battery connector wires, tin them, and solder them to the battery pads on the main board in an L-shape pointing upwards. Make sure they do not go over the edge of the board, or the top cover won't fit.
<img src="assets/battery-connector-solder.png" alt="battery-connector-solder" width="70%">
4. **Install Display:** Push the wired OLED display gently into its slot inside the top cover. Insert self-threading screws into the small screw holes beside the display to clamp it permanently in place.
<img src="assets/insert-display-enclosed.png" alt="securing-display" width="70%">
**Phase 3 checklist**
- [ ] Hip joints pre-loaded with servo horns, not yet attached to motors.
- [ ] Leg pieces installed on every motor.
- [ ] Power switch and OLED display installed in the top cover.
- [ ] Battery connectors soldered to the main board.
## Phase 4: Hardware Main Assembly
**Goal:** Seat the frame motors, route wiring, and secure the electronics stack so the robot is ready for calibration.
1. **Battery Holder:** Insert the battery holder into the back of the internal frame piece. Route the red and black wires up the slot.
2. **Motor Installation:** While holding the battery wires in place, insert the four remaining side motors. The motor shaft should be facing towards the outside of the internal frame.
- Insert motors at an angle and press them the rest of the way. Make sure not to pinch any wires at the bottom.
3. **Secure Motors:** Use self-threading screws to permanently affix all four side motors to the frame.
<img src="assets/insert-frame.png" alt="insert-motors" width="70%">
<img src="assets/rotate-motor.png" alt="rotate-motor" width="70%">
Make sure the motor shafts are closest to the outer edge of the frame.
<img src="assets/install-frame-motors.png" alt="complete-motors" width="70%">
## Installing the Main Electronics
Before dropping hardware in, trim or bundle any stray wires so nothing can flop into the servo gears and double-check that every connection exits upward.
1. Pre-route any long wires upward so they naturally hug the future top cover.
2. Lower the electronics harness into the cavity while keeping the USB port toward the rear.
3. **S2 Mini build:** Screw the controller (and optional protoboard) directly into the frame using the provided mounting holes. If you built a small protoboard backplane, use the spare holes in the internal frame to secure it so the servo plugs are easy to reach later.
<img src="assets/secure-electronics.png" alt="s2-secure-electronics" width="70%">
4. **Distro Board V1 build (Legacy):** Install four M2.5 × 5 mm male-female standoffs to raise the Sesame Distro Board V1 so it clears the ESP32 DevKit. Then secure the assembly using the top screws.
<img src="assets/secure-distro-board.png" alt="secure-distro-board" width="70%">
5. **Distro Board V3/V2 build (Build Kits):** Mount by gently bringing cables towards the center nicely so that the main board can sit properly. Attach the main board to the internal frame. It has two mounting holes in the corner that you attach using self-threading screws.
<img src="assets/secure-distro-boardv2.png" alt="secure-distro-boardv2" width="70%">
> [!TIP]
> Notch = front. USB port = back. Use these two cues any time the orientation feels ambiguous.
## Calibrating & Running the Testing Firmware
**Goal:** Teach the controller where each motor sits so the walking poses land correctly.
> [!CAUTION]
> Never run calibration with joints attached. A misaligned horn can stall or strip a servo instantly. All motor shafts should be free spinning at this point.
**For S2 Mini and Distro Board V1 (Legacy) Builds:**
1. Inspect the harness to ensure no bare conductors can short during testing. Add heat-shrink or tape where needed.
2. Connect a reliable USB-C cable and flash `sesame-motor-tester.ino` from the [debugging-firmware](../../firmware/debugging-firmware/) folder using Arduino IDE. If you have never flashed an ESP32 before, pause here and follow a quick tutorial so you are comfortable resetting/entering boot mode.
3. Open the serial monitor. You should see the tester menu.
4. Command all motors to 90°. Starting from Motor 0, plug its connector into the appropriate header. The servo should immediately whirr into the 90° position. Repeat for Motors 17. Skip to Step 4 below.
**For Distro Board V3/V2 (Build Kits):**
If you are using a V3 or V2 Board from a Build Kit, it comes **pre-flashed** with the Sesame firmware! There is no need to plug into a computer and use Arduino IDE to flash firmware.
1. **Power Up:** Plug the board into a strong USB power supply (fast chargers for phones or laptop chargers work great). Note: If you are using the V2 Distro board, it currently has instabilities with the battery system and won't work on batteries, you must plug via USB-C.
2. **Connect to Portal:** Check the Wi-Fi networks on your phone or computer. The Sesame board will create an access point. Connect to it to open the controller portal. (If on desktop and the portal doesn't show, go to `sesame-root.local` in your browser. Mobile is recommended.)
3. **Initialize PWM:** Tap the "Standby" button in the portal to initialize the PWM channels. The moment you plug a motor in, it will go to its correct active position.
**General instructions continuing:**
4. **Refer to Diagram:** Use the diagram below to associate motor numbers with their physical position. Ensure the frames are roughly oriented correctly.
<img src="assets/sesame-angle-guide.png" alt="angle-guide" width="100%">
5. **Plug Motors In:** Starting with Motor 0, plug the connector into the corresponding board header.
- **Crucial:** Ensure the brown wire aligns with the ground rail! Upon plugging it in, you should hear it whir into life and hold position.
- Repeat for all 8 motors.
> [!TIP]
> 99% of the time, if your motor is moving in the wrong direction, crashing, or being sporadic, the motor is plugged into the wrong slot. Check your wiring!
## Attaching Hip & Leg Joints
Now we attach the joints while the motors are turned on and holding their "Stand" position.
1. **Hip Joints:** Select the matching hip joint for your motor. While in "Stand" mode, push the hip joint onto the motor shaft at a 45-degree angle.
2. **Test Angles:** Tap "Rest" in the portal. The hip joint should move perfectly parallel to the body. Tap "Stand" again, and it should go back to 45°. Alternate this to ensure it's placed correctly.
3. **Fasten Hip:** If it looks good, fasten the hip joint to the motor using an M2.5 machine screw through the main hole into the motor shaft. Gently tighten (don't over-torque against the motor as this may cause a brownout).
4. **Legs:** Repeat the same process with the leg joints. Check against "Rest" and "Stand", ensure nothing collides, and then affix using screws.
<img src="assets/reference-configuration.png" alt="reference-configuration" width="70%">
## Final Wiring and Top Cover
Now to clean up the robot and secure everything.
1. **Battery Connections:** Insert the battery wires into the screw terminal on the board and use a flathead screwdriver to tighten them securely.
2. **Wire Routing:** Flip the robot over. There are channels for all the wires on the underside. Securely press all of the wires into these channels.
3. **Bundling:** Separate the wires gently into two groups (left and right). Wrap a zip tie around each bundle, fold it once, and tighten the zip tie to create a tight bundle. This helps the top cover fit seamlessly.
<img src="assets/wire-routing.png" alt="wire-routing" width="70%">
<img src="assets/zip-tie-bundles.png" alt="zip-tie-bundles" width="70%">
4. **Testing the Switch:** Make sure the batteries are charged and insert them. Flip the switch and check if the Wi-Fi network appears or if the display lights up. If things aren't working, double-check connections.
> [!NOTE]
> **V2 Board Owners:** If you are using the V2 Distro board from a kit, there are instabilities with the battery system that prevent reliable battery usage. You must run your Sesame via USB-C (fast charger / laptop charger) for now. A free V3 Distro board will be provided to pre-order kit buyers!
5. **Top Cover:** Gently pack all the bundles in and slowly press the top cover down onto the electronic components. Be very gentle to avoid shorting or disconnecting anything.
6. **Securing Top Cover:** Hold the whole robot together and insert the M2 self-threading screws into the bottom of the frame where they thread securely into the mounting holes for the top cover.
<img src="assets/secure-top-cover.png" alt="secure-top-cover" width="70%">
## Bottom Cover & Final Checks
<img src="assets/insert-battery.png" alt="insert-battery" width="70%">
1. Set the bottom cover in place, checking that no wires are trapped. Use the remaining two self-threading screws into the bottom to secure it.
<img src="assets/screw-on-bottom-cover.png" alt="screw-on-bottom-cover" width="70%">
2. You can apply the included sticky pads to the feet if you like!
**Phase 4 checklist**
- [ ] Motors mounted and screwed in on hip and leg joints.
- [ ] Harness routed into channels, zip-tied without pinch points.
- [ ] Top and bottom covers installed.
> [!TIP]
> A common issue on battery power (if you upgrade) is that your Sesame robot will crash on movement. Setting the motor current delay higher in the settings may help, but upgrading to a Lithium Polymer (LiPo) cell often solves it for advanced users!
Great job! The hardware is complete.
<img src="assets/sesame-done.png" alt="sesame-done" width="70%">
## Final Checks and Flashing the Firmware
Now we just need to do a few checks to make sure everything is working and we can flash the firmware onto the microcontroller.
**Sanity checks**
1. Toggle the power switch (if you installed a battery) and confirm it fully disconnects the pack.
2. Confirm the USB-C port is accessible for future firmware updates.
3. Inspect the OLED wiring one last time to ensure SDA/SCL arent swapped.
**Flash production firmware (For DIY / S2 Mini / Distro V1)**
If you are using the V2 Build kit board, you can skip this step! It is pre-flashed.
1. Choose the code variant from the firmware folder. Customize pin definitions if you're using a different ESP32.
2. Use Arduino IDE to flash the full firmware bundle (motors + faces). Detailed notes live in [firmware/README.md](../../firmware/README.md).
3. Connect to Sesames Wi-Fi AP, load the control page, and trigger a pose to verify everything moves as expected.
Thats it. Youve built Sesame! From here you can design new poses in [Sesame Studio](../../software/sesame-studio/README.md) or tweak the hardware per [hardware/README.md](../../hardware/README.md), or whatever you want!
Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 647 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

+3
View File
@@ -0,0 +1,3 @@
# Images
Photos, diagrams, and illustrations for documentation.
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

+146
View File
@@ -0,0 +1,146 @@
# Wiring Guide
Complete wiring guides for the Sesame Robot.
---
## Choosing a Wiring Approach
The project supports multiple wiring strategies that suit different build constraints:
- **Lolin S2 Mini / hand wiring (RECOMMENDED FOR DIY BUILDS):** Uses a Lolin S2 Mini, loose headers, and point-to-point wiring. It is the lowest-cost option, easy to source, supports USB-C PD for tethered power, but demands patience to keep the harness tidy and leaves less room for mistakes. **Critical: Use 30AWG wire for data lines and 22AWG for power—larger gauge wire will make assembly nearly impossible.**
- **Sesame Distro Board V3 (INCLUDED IN NEW BUILD KITS):** The latest custom PCB with SMD components (V2 is also supported but legacy and limited to USB power due to battery brownouts), supports both USB-C PD and battery power. Pre-flashed and included with all Sesame Build Kits. Advanced to hand-solder; professional assembly recommended if ordering separately.
- **Sesame Distro Board V1 / ESP32-DevKitC-32E (LEGACY):** Uses the custom Distro Board V1 PCB stacked on an ESP32-DevKitC-32E. Now phased out but still supported. V1 has known limitations and cannot run on tethered USB-C power (battery + buck converter required).
Pick the approach that matches your component availability and comfort with managing wire bundles; the remainder of this guide dives into both workflows.
## How to wire the Lolin S2 Mini / Hand Wiring
The Lolin S2 Mini method is a traditional protoboard build. Consult the S2 Mini wiring diagram below while reading this section; it captures every motor, sensor, OLED, and button lead even if the photos only show a subset of the connections. Expect a high connection count, so plan wire lengths and colors before soldering to avoid confusion later.
<img src="s2-mini-wiring-guide-new.png" alt="S2 Mini wiring diagram" width="70%">
*Credit to @captianeverypowersr on Discord for the new S2 mini guide.*
### Prep
1. **Secure and tin** all of the connections on the board before starting
2. To connect all the 3-pin headers together and to the board, first attach them to a small section of protoboard
<img src="assets/proto-headers.png" alt="proto-headers" width="70%">
### Building Power and Ground Rails
1. Solder one wire to one end of the pins, then guide it along and remove the insulation
2. Solder this exposed wire to every middle pin (this creates the **5V rail**)
3. Do the same for the ground lane (this creates the **ground rail**)
<img src="assets/proto-power-rails.png" alt="proto-power-rails" width="70%">
### Data Lines
You can now cut eight equal-length wires for data connections.
**Wire Recommendations:**
- **Data lines:** 30AWG silicone wire
- **Power and ground:** 22AWG silicone wire
- **Important:** Things will get super cluttered if you use large gauge wire
<img src="assets/proto-datalines.png" alt="proto-data-lines" width="70%">
Alongside the eight motor data leads, the diagram also calls out the OLED connections that branch from the microcontroller.
### Packing Electronics
When packing electronics into the frame, it's difficult with the hand wiring setup because there are a lot of stray wires. My advice is to work slowly and methodically:
1. Group wires with similar destinations
2. Use zip ties and heat shrink tubing to make them as compact as possible
3. Consider connecting the power switch after finishing most other wiring, since it's attached to the top cover
<img src="assets/wire-managment.png" alt="wire-managment" width="70%">
### Safety and Testing
> [!CAUTION]
> Do not solder and de-solder connections with power connected!
**Before powering on:**
- Double check your power and ground lines before turning the robot on
- Make sure to cover any exposed wires as they can touch the pin headers and fry the ESP32 (especially when cramming the cover on)
> [!TIP]
> To make fishing out the wires for the OLED display easier, you can temporarily twist them together into a group and then guide them through the opening in the top cover.
---
## How to wire the Sesame Distro Board V1 / ESP32-DevKitC-32E
### Overview
> [!CAUTION]
> UPDATE 1/20/26: Upon further testing, the Sesame distro board V1 will work, but it has a few issues that make it a little harder to assemble and will not run on teathered power (eg. USB C). Until V2 is released, I recommend using the S2 Mini / Hand Wiring approach. If you ordered a distro board V1, it will still be supported with wiring guides and firmware for the foreseeable future <3.
This option provides a cleaner, more organized wiring solution.
Consult the distro-board wiring diagram below for exact connection locations.
> [!IMPORTANT]
> **ESP32 Pin Header Requirement:** The distro board V1 stacks on top of the ESP32-DevKitC-32E, so you need an ESP32 board **without pre-soldered pin headers**. If your board came with headers already soldered on the top, you will need to desolder all of the headers and flip them to the bottom side of the ESP32 board so the distro board can mount on top.
<img src="distro-board-wiring-guide.png" alt="distro-board wiring diagram" width="70%">
### Component Installation
**Optional components** (you can solder directly to pads instead):
- 4-pin JST connector
- 2-pin screw terminal
**Pin header installation tip:** If you're having difficulties keeping the pin headers in place to solder, try placing them in a protoboard first, then transferring them over.
### Buck Converter Setup
A buck converter takes any voltage (5V-12V) and drops it to a stable 5V for the motors and ESP32.
**If using a battery:**
1. Make sure to solder the buck converter enable pads
2. The buck converter is **required** for battery operation
3. A Bambu Lab 14500 7.4V 800mAh Li-ion Battery fits the latest internal frame perfectly. V3 requires soldering a XH2.54 pigtail directly to the board.
**Alternative power options:**
- If you're using a benchtop supply or have another regulated 5V source
- You can override the buck converter and take voltage directly from the screw terminal by soldering the override pads
### Battery Connection
> [!CAUTION]
> If your battery has an XT30 or JST RCY connector, don't cut it off and wire the battery directly to the distro board! This is unsafe and means you can't charge your battery anymore.
**Proper method:**
1. Get a female XT30 or JST RCY connector (matching your battery)
2. Wire it to two wires that lead to the power terminal
3. This allows you to safely connect and disconnect the battery
**Note on Battery Connectors:**
- **Always remove the 10440 cells from the holder and recharge them separately using a proper Li-ion charger.** Most AAA holders do not support safe Li-ion charging and attempting to charge in-holder can be dangerous.
### Sourcing the Distro Board
The Sesame Distro Board V1 is a custom PCB designed specifically for this project. It mounts on top of a ESP32-DevKitC-32E.
More information is avalible in the [PCB](../../hardware/pcb/README.md) section of the documentation.
---
## General Wiring Tips
- Work slowly and methodically
- Test connections before applying power
- Keep wire runs as short and neat as possible
- Label wires if helpful for troubleshooting
- Take photos during assembly for reference
Binary file not shown.

After

Width:  |  Height:  |  Size: 415 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 825 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 875 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 911 KiB

+847
View File
@@ -0,0 +1,847 @@
# esame Robot Firmware
This document provides technical information on the firmware architecture, control logic, and hardware abstraction layers used in the Sesame Robot.
> [!NOTE]
> The firmware is now organized into a modular structure with a main entry point and specialized header files for bitmaps, movement, and web assets. This makes customization much easier and the codebase cleaner.
## Table of Contents
- [How to Flash the Firmware](#how-to-flash-the-firmware)
- [Network Configuration](#network-configuration--connectivity)
- [API Reference](#api-reference)
- [Legacy Web Endpoints](#legacy-web-endpoints)
- [JSON API](#json-api-recommended-for-network-clients)
- [Python Examples](#python-api-example)
- [JavaScript Examples](#nodejsjavascript-example)
- [Advanced Integration Examples](#advanced-integration-examples)
- [Voice Assistant Integration](#voice-assistant-integration)
- [Home Automation](#home-automation-integration)
- [Idle Animation System](#idle-animation-system)
- [Firmware Architecture](#firmware-architecture)
- [Technical Implementation](#technical-implementation-overview)
- [Asset Pipeline &amp; Face Customization](#asset-pipeline--face-customization)
- [Hardware Abstraction Layer](#hardware-abstraction-layer-hal)
## How to Flash the Firmware
> [!NOTE]
> **Sesame Build Kit Users:** Your Distro Board V3 (and earlier V2) kits come pre-flashed with the latest firmware. You only need to flash firmware if you want to customize it or update to a newer version.
### Prerequisites
1. **Arduino IDE** (version 2.0 or higher recommended)
2. **ESP32 Board Support**: Install via Arduino IDE's Board Manager
- Open Arduino IDE
- Go to **File → Preferences**
- Add to "Additional Board Manager URLs": `https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json`
- Go to **Tools → Board → Boards Manager**
- Search for "ESP32" and install "ESP32 by Espressif Systems" (v2.0.0 or higher)
3. **Required Libraries** (install via Library Manager):
- `ESP32Servo` **v3.0.9** (recommended)
- `Adafruit SSD1306`
- `Adafruit GFX Library`
> [!IMPORTANT]
> Use **ESP32Servo v3.0.9** for this project. Newer releases currently have a known issue where writing to one servo can affect multiple channels ([madhephaestus/ESP32Servo#103](https://github.com/madhephaestus/ESP32Servo/issues/103)).
### Flashing Steps
1. **Connect your board** via USB to your computer
2. **Open the firmware**:
- Open [sesame-firmware-main.ino](sesame-firmware-main.ino) in Arduino IDE
- Make sure you have it in a folder with the same name.
- Also include all of the .h header files.
3. **Select your board**:
- Go to **Tools → Board**
- For Lolin S2 Mini: Select "LOLIN S2 Mini"
- For Sesame Distro Board V1: Select "ESP32 Dev Module"
- For Sesame Distro Board V2 or V3: Select "ESP32S3 Dev Module"
4. **Configure board settings**:
- **For Lolin S2 Mini**:
- **Upload Speed**: 921600
- **USB CDC On Boot**: "Enabled"
- **Partition Scheme**: "Default 4MB with spiffs"
- **For Distro Board V2 or V3 (ESP32-S3)**:
- **USB CDC On Boot**: "Enabled" (Required for Serial Monitor)
- **Flash Mode**: "QIO 80MHz"
- **Partition Scheme**: "Default 4MB with spiffs"
5. **Select the correct port**:
- Go to **Tools → Port** and select your ESP32's COM port
6. **Choose your board configuration** in the code:
- Open [sesame-firmware-main.ino](sesame-firmware-main.ino)
- Find the pin configuration section (around line 55-65)
- **If you built with the Lolin S2 Mini:** Uncomment the S2 Mini `servoPins` array and `I2C_SDA`/`I2C_SCL` defines. Comment out the Distro Board section.
- **If you built with the Distro Board V3:** Uncomment the V3 `servoPins` array and `I2C_SDA`/`I2C_SCL` defines. Comment out others.
- **If you built with the Distro Board V2:** Uncomment the V2 `servoPins` array and `I2C_SDA`/`I2C_SCL` defines. Comment out others.
- **If you built with the Distro Board V1:** Uncomment the V1 `servoPins` array and `I2C_SDA`/`I2C_SCL` defines. Comment out others.
7. **(Optional) Configure network mode**:
- If you want the robot to connect to your WiFi network (for API access and remote control), edit the network configuration section (around line 17-22):
```cpp
#define NETWORK_SSID "YourNetworkName" // Your WiFi network name
#define NETWORK_PASS "YourPassword" // Your WiFi password
#define ENABLE_NETWORK_MODE true // Set to true to enable
```
- Leave `ENABLE_NETWORK_MODE false` to use Access Point mode only
8. **Upload the firmware**:
- Click the **Upload** button (→) in Arduino IDE
- Wait for compilation and upload to complete
9. **Test the connection**:
- Open Serial Monitor (**Tools → Serial Monitor**, set baud rate to 115200)
- Reset the board - you should see startup messages and network information
- The serial monitor provides individual motor control for testing and troubleshooting
- **If using AP mode only**: Connect to the "Sesame-Controller-BETA" WiFi network (password: `12345678`) and navigate to any website
- **If using network mode**: Look for the "Connected to network!" message in Serial Monitor, then access via `http://sesame-robot.local` or the displayed IP address
### Troubleshooting
- **Linux USB Permissions**: If your port isn't showing up or you get "Permission Denied", you likely need to add your user to the `dialout` group: `sudo usermod -a -G dialout $USER`. Log out and back in for changes to take effect.
- **Upload fails**: Try holding the BOOT button while uploading, or try a different USB cable. For S3-based boards, the "USB CDC On Boot" setting is critical for finding the port after a reset.
- **Port not found**: Install the appropriate USB drivers (CP210x for S2 Mini, CH340 for some ESP32 boards). Windows 10/11 usually includes these.
- **Robot not moving**: Check power supply and servo connections; increase `motorCurrentDelay` in web settings if brownouts occur
- **Can't connect to network**:
- Verify `ENABLE_NETWORK_MODE` is set to `true` and SSID/password are correct
- Check Serial Monitor for connection status - look for "Connected to network!" or error messages
- Ensure your WiFi network is 2.4GHz (ESP32 does not support 5GHz)
- Try increasing the connection timeout in the code (currently 10 seconds / 20 attempts)
- **mDNS hostname not resolving**:
- On Windows, install [Bonjour Print Services](https://support.apple.com/kb/DL999)
- On Linux, ensure `avahi-daemon` is installed and running
- Try accessing via IP address instead (check Serial Monitor for the assigned IP)
- Some routers block mDNS traffic - check router settings or use IP address
- **API commands not working**:
- Ensure you're sending POST requests to `/api/command` (not GET)
- Verify `Content-Type: application/json` header is set
- Check Serial Monitor for error messages and parsed command output
- Test with a simple cURL command first to verify connectivity
## Firmware Architecture
The firmware is split into several key files to keep the logic organized and assets easy to manage:
- **[sesame-firmware-main.ino](sesame-firmware-main.ino)**: The main entry point containing the `setup()`, `loop()`, core system logic, network configuration, and API endpoints.
- **[face-bitmaps.h](face-bitmaps.h)**: A dedicated header for OLED face macros and raw bitmap data. Now includes extensive conversational faces for voice assistant integration.
- **[movement-sequences.h](movement-sequences.h)**: Definitions for all procedural movement and pose animations.
- **[captive-portal.h](captive-portal.h)**: Contains the HTML, CSS, and JS for the web-based remote control interface.
## Technical Implementation Overview
The firmware is built on the Arduino-ESP32 framework. Currently the firmware is running on a single-core event loop, and hardware-based PWM timers for precise motor control.
### PWM & Servo Kinematics
- **Timer Allocation**: The firmware uses `ESP32PWM::allocateTimer(n)` to reserve hardware timers 0-3. This prevents conflicts with other peripherals and ensuring high-resolution PWM signals (50Hz frequency). Due to the limited number of timers, you may experience network errors upon adding additional devices or calls to the firmware. For example, in a modded version of the robot, I tried adding two ESCs and their servo controll to the code, and the CPU ran out of internal timers and caused the captive portal to die. If you are experiencing network errors with your custom firmware, check the timer allocation.
- **Pulse Width Mapping**: Leg servos are mapped from degrees (0-180) to microseconds (732us to 2929us). This range is set to the maximum travel on most hobby servos with a 180 degree limit, but can be edited in the `servos[i].attach()` calls. If you are using motors with a larger range of motion, like 270 degree servos, you need to set the PWM mapping to a different length of microseconds. For 270 degree servos specifically I found (833us to 2167us) works.
- **Staggered Activation**: To prevent VCC rail collapse (brownout) caused by simultaneous inductive loads, the `setServoAngle` helper introduces a mandatory `motorCurrentDelay` (default 20ms) between sequential pulses. This delay should be tweaked to your power setup. If you have a strong dedicated power supply you can try setting it to zero. It can also be changed while running through the AP controller settings menu.
### Communication & Networking Stack
- **Dual-Mode WiFi**: The ESP32 can simultaneously operate in both Access Point mode (for direct connections) and Station mode (connected to an existing network) using `WiFi.mode(WIFI_AP_STA)`.
- **SoftAP & Captive Portal**: The ESP32 initializes an Access Point using `WiFi.softAP()`. A `DNSServer` listens on UDP Port 53, using a wildcard "*" redirect to map all DNS queries to the internal gateway (`192.168.4.1`).
- **mDNS Service Discovery**: The firmware broadcasts `sesame-robot.local` via Multicast DNS (mDNS) using the `ESPmDNS` library, allowing network discovery without hardcoded IP addresses.
- **RESTful API Surface**: The `WebServer` handles both legacy URL-parameter endpoints and modern JSON-based API:
- `/cmd?go=[dir]`: Legacy movement control
- `/cmd?pose=[name]`: Legacy pose triggers
- `/api/status`: JSON status endpoint (GET)
- `/api/command`: JSON command endpoint (POST) - supports face-only updates and combined face+movement commands
- `/getSettings` / `/setSettings`: Parameter configuration
- **Face-Only Command Support**: The `/api/command` endpoint intelligently detects face-only requests (no `command` field) and updates the display without triggering movement animations.
- **Non-Blocking Control Flow**: Instead of `delay()`, the firmware uses a custom `pressingCheck(String cmd, int ms)` function. This function polls `server.handleClient()` and `dnsServer.processNextRequest()` during animation frames, allowing for real-time interruptibility (e.g., immediate stop on button release). This pressingCheck protocol can be used for motion commands like walking to play each motion only when the button is held.
### Display & Graphics Subsystem
- **I2C Bus Hardware**: Utilizes the ESP32's hardware I2C controller at 400kHz (Fast Mode) for minimal latency when pushing full-frame buffers to the SSD1306 display.
- **Memory Management (`PROGMEM`)**: Large 128x64 bitmap arrays (1024 bytes per frame) are stored in Flash memory using the `PROGMEM` attribute.
- **Macro-Based Asset Management**: The firmware uses a `FACE_LIST` macro in [face-bitmaps.h](face-bitmaps.h) to automatically register and handle new faces, reducing the boilerplate required when adding animations.
- **Rendering Pipeline**: The `updateAnimatedFace()` function manages frame rates and sequence looping outside of the main movement logic to ensure smooth visual feedback even during complex movements.
- **Dynamic WiFi Info Overlay**: The `updateWifiInfoScroll()` function composites scrolling connection information over the face bitmap during the first 30 seconds of operation (before first input), drawing the face as background with a black bar and white text overlay for readability.
- **Idle Animation System**: Implements realistic idle behavior with randomized blinking (including double-blinks) and boomerang face animations, triggered automatically when no input is detected.
## Prerequisites & Development Environment
- **Board Support**: ESP32 by Espressif Systems (v2.0.0+ recommended). Lolin S2 Mini and ESP32-WROOM32 DevKitC are best supported.
- **Libraries**:
- `ESP32Servo` **v3.0.9**: Low-level PWM timer management. (Pinned due to known multi-servo command leak in newer versions: [madhephaestus/ESP32Servo#103](https://github.com/madhephaestus/ESP32Servo/issues/103))
- `Adafruit_SSD1306` & `Adafruit_GFX`: Buffer-based OLED rendering.
- `ESPmDNS`: mDNS service discovery (included with ESP32 board support).
- `DNSServer`: Captive portal DNS redirection (included with ESP32 board support).
- `WebServer`: HTTP server implementation (included with ESP32 board support).
- **Tooling**: Arduino IDE 2.0+ recommended.
## Network Configuration & Connectivity
The firmware now supports **dual-mode WiFi operation**, allowing the robot to simultaneously act as an Access Point (for direct connections) and connect to your existing WiFi network (for integration with other devices and remote control).
### Access Point Mode (Default)
By default, the robot creates its own WiFi network:
- **SSID**: `Sesame-Controller-BETA`
- **Password**: `12345678` (Some devices may require you to change this if the default isn't connecting)
- **IP Address**: `192.168.4.1`
Connect to this network and navigate to any website to access the captive portal control interface. If you're having trouble connecting, try changing the password to something custom in the code's `#define AP_PASS` section.
### Network Mode (Optional)
To connect the robot to your home or office WiFi network:
1. **Enable Network Mode** in [sesame-firmware-main.ino](sesame-firmware-main.ino):
```cpp
#define NETWORK_SSID "YourNetworkName" // Your WiFi network name
#define NETWORK_PASS "YourPassword" // Your WiFi password
#define ENABLE_NETWORK_MODE true // Set to true to enable
```
2. **Flash the updated firmware** to your robot.
3. **Access via multiple methods**:
- **mDNS Hostname**: `http://sesame-robot.local` (works on most devices)
- **Network IP**: Check Serial Monitor at 115200 baud for the assigned IP address
- **Still accessible via AP**: The robot maintains its Access Point even when connected to your network
### mDNS Discovery
The firmware includes an mDNS responder that broadcasts the hostname `sesame-robot.local` on your local network. This allows you to access the robot without knowing its IP address:
```bash
# Access from browser
http://sesame-robot.local
# Ping test
ping sesame-robot.local
```
**Note**: mDNS works natively on:
- macOS and iOS devices
- Linux with Avahi installed
- Windows 10+ (may require Bonjour service)
### Security Considerations
The firmware is designed for local network use and does not include authentication by default. When enabling network mode:
- **Local Network Only**: The robot responds to requests from any device on your network. Do not expose it to the internet without proper security measures.
- **No HTTPS**: Communication is unencrypted HTTP. Avoid transmitting sensitive data through the robot's API.
- **Access Point Password**: The default AP password is `12345678`. For production use, change `AP_PASS` in the firmware to a stronger password.
- **Trusted Networks**: Only connect your robot to trusted WiFi networks.
For enhanced security:
1. Change the default AP password to a strong, unique password
2. Use network segmentation (IoT VLAN) to isolate the robot from critical devices
3. Implement firewall rules to restrict access to specific IP addresses
4. Consider adding authentication headers in a custom firmware fork for production deployments
### WiFi Information Display
The robot's OLED screen now features an intelligent WiFi info system:
- **First 30 seconds**: If no input is received, WiFi connection information scrolls across the top of the display
- **After first input**: WiFi info disappears to show only faces
- **Dual-mode info**: When connected to both AP and network, displays both connection details
The scrolling text includes:
- Access Point SSID and IP
- Network name and IP (if connected)
- mDNS hostname
- Captive portal instructions
## API Reference
The firmware exposes both legacy web endpoints and a modern JSON-based API for programmatic control.
### Legacy Web Endpoints
These endpoints use URL parameters and are primarily used by the web interface:
#### Movement Control
```http
GET /cmd?go=forward
GET /cmd?go=backward
GET /cmd?go=left
GET /cmd?go=right
GET /cmd?stop
```
#### Pose Control
```http
GET /cmd?pose=wave
GET /cmd?pose=dance
GET /cmd?pose=rest
GET /cmd?pose=stand
# ... (see movement-sequences.h for all available poses)
```
#### Individual Motor Control
```http
GET /cmd?motor=1&value=90
# motor: 1-8 (motor number)
# value: 0-180 (angle in degrees)
```
#### Settings Management
```http
GET /getSettings
# Returns: {"frameDelay":100,"walkCycles":10,"motorCurrentDelay":20,"faceFps":8}
GET /setSettings?frameDelay=120&walkCycles=15&motorCurrentDelay=25&faceFps=10
```
### JSON API (Recommended for Network Clients)
The JSON API is designed for programmatic control from external devices, Python scripts, and IoT integrations.
#### Get Robot Status
```http
GET /api/status
```
**Response:**
```json
{
"currentCommand": "forward",
"currentFace": "walk",
"networkConnected": true,
"apIP": "192.168.4.1",
"networkIP": "192.168.1.100"
}
```
#### Send Commands
```http
POST /api/command
Content-Type: application/json
{
"command": "forward",
"face": "walk"
}
```
**Response:**
```json
{
"status": "ok",
"message": "Command executed"
}
```
#### Face-Only Updates
Send a face change without triggering movement:
```http
POST /api/command
Content-Type: application/json
{
"face": "happy"
}
```
**Response:**
```json
{
"status": "ok",
"message": "Face updated"
}
```
#### Stop Command
```http
POST /api/command
Content-Type: application/json
{
"command": "stop"
}
```
### Available Commands
**Movement Commands:**
- `forward`, `backward`, `left`, `right` - Continuous movement (loops until stopped)
- `stop` - Immediately stop current movement
**Pose Commands (one-shot animations):**
- `rest`, `stand`, `wave`, `dance`, `swim`, `point`
- `pushup`, `bow`, `cute`, `freaky`, `worm`, `shake`
- `shrug`, `dead`, `crab`
**Available Faces:**
- Movement faces: `walk`, `rest`, `stand`, `dance`, `wave`, etc.
- Conversational faces: `happy`, `sad`, `angry`, `surprised`, `sleepy`, `love`, `excited`, `confused`, `thinking`
- Talk variants: `talk_happy`, `talk_sad`, `talk_angry`, etc.
- Special: `idle`, `idle_blink`, `default`
### Python API Example
```python
import requests
import time
# Robot IP or hostname
robot_url = "http://sesame-robot.local"
# Get status
response = requests.get(f"{robot_url}/api/status")
status = response.json()
print(f"Current face: {status['currentFace']}")
# Make robot wave and show happy face
requests.post(f"{robot_url}/api/command", json={
"command": "wave",
"face": "happy"
})
time.sleep(3)
# Just change face without moving
requests.post(f"{robot_url}/api/command", json={
"face": "excited"
})
# Stop movement
requests.post(f"{robot_url}/api/command", json={
"command": "stop"
})
```
### cURL Examples
```bash
# Get robot status
curl http://sesame-robot.local/api/status
# Make robot dance
curl -X POST http://sesame-robot.local/api/command \
-H "Content-Type: application/json" \
-d '{"command":"dance","face":"dance"}'
# Change face only
curl -X POST http://sesame-robot.local/api/command \
-H "Content-Type: application/json" \
-d '{"face":"happy"}'
# Stop movement
curl -X POST http://sesame-robot.local/api/command \
-H "Content-Type: application/json" \
-d '{"command":"stop"}'
```
### Node.js/JavaScript Example
```javascript
const robotURL = 'http://sesame-robot.local';
// Get status
fetch(`${robotURL}/api/status`)
.then(res => res.json())
.then(data => console.log('Robot status:', data));
// Send command
fetch(`${robotURL}/api/command`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
command: 'wave',
face: 'happy'
})
})
.then(res => res.json())
.then(data => console.log('Response:', data));
// Face-only update
fetch(`${robotURL}/api/command`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({face: 'surprised'})
})
.then(res => res.json())
.then(data => console.log('Response:', data));
```
## Advanced Integration Examples
### Voice Assistant Integration
The combination of conversational faces, the JSON API, and network mode makes the Sesame Robot perfect for voice assistant projects. Here's an example using Python with speech recognition:
```python
import requests
import speech_recognition as sr
from textblob import TextBlob
import time
robot_url = "http://sesame-robot.local"
def analyze_sentiment(text):
"""Analyze sentiment and return appropriate face"""
blob = TextBlob(text)
polarity = blob.sentiment.polarity
if polarity > 0.5:
return "excited"
elif polarity > 0.2:
return "happy"
elif polarity < -0.5:
return "angry"
elif polarity < -0.2:
return "sad"
else:
return "thinking"
def set_robot_face(face, talking=False):
"""Update robot's face expression"""
if talking:
face = f"talk_{face}"
requests.post(f"{robot_url}/api/command", json={"face": face})
# Initialize speech recognition
recognizer = sr.Recognizer()
while True:
with sr.Microphone() as source:
print("Listening...")
set_robot_face("idle")
try:
audio = recognizer.listen(source, timeout=5)
set_robot_face("thinking")
text = recognizer.recognize_google(audio)
print(f"You said: {text}")
# Determine emotion and show talking face
emotion = analyze_sentiment(text)
set_robot_face(emotion, talking=True)
# Process command...
time.sleep(2)
# Return to neutral
set_robot_face(emotion, talking=False)
except sr.WaitTimeoutError:
set_robot_face("sleepy")
except sr.UnknownValueError:
set_robot_face("confused")
```
### Home Automation Integration
Integrate with Home Assistant or other home automation platforms:
```python
# Home Assistant automation example
import requests
def robot_notification(message_type):
"""Display robot emotion based on notification type"""
robot_url = "http://sesame-robot.local"
emotion_map = {
"doorbell": "surprised",
"alarm": "angry",
"reminder": "thinking",
"success": "happy",
"error": "sad"
}
face = emotion_map.get(message_type, "default")
requests.post(f"{robot_url}/api/command", json={"face": face})
# Optional: add movement
if message_type == "doorbell":
requests.post(f"{robot_url}/api/command", json={
"command": "wave",
"face": "happy"
})
```
### WebSocket Streaming (Advanced)
For real-time control with minimal latency, you can poll the status endpoint or implement a simple state machine:
```javascript
// React/Vue.js real-time robot control
class RobotController {
constructor(robotURL) {
this.url = robotURL;
this.currentFace = 'default';
}
async updateEmotion(emotion, isSpeaking) {
const face = isSpeaking ? `talk_${emotion}` : emotion;
if (face !== this.currentFace) {
await fetch(`${this.url}/api/command`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({face})
});
this.currentFace = face;
}
}
async getStatus() {
const res = await fetch(`${this.url}/api/status`);
return await res.json();
}
async performAction(action, emotion = null) {
const payload = {command: action};
if (emotion) payload.face = emotion;
await fetch(`${this.url}/api/command`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
}
}
// Usage
const robot = new RobotController('http://sesame-robot.local');
// Set emotion without moving
await robot.updateEmotion('happy', false);
// Set talking face
await robot.updateEmotion('excited', true);
// Perform action with face
await robot.performAction('dance', 'excited');
// Check status
const status = await robot.getStatus();
console.log(`Robot is ${status.currentFace} and doing ${status.currentCommand}`);
```
## Idle Animation System
The firmware includes an intelligent idle animation system that activates automatically:
### Behavior
- **Activation**: When the robot has received no commands for a period, it enters idle mode
- **Idle Face**: Displays a gentle "breathing" animation using the `idle` face in `FACE_ANIM_BOOMERANG` mode
- **Blinking**: Randomly triggers blink animations (3-7 second intervals)
- **Double Blinks**: 30% chance of a double blink for realistic behavior
- **Exit**: Any movement command or input immediately exits idle mode
### Technical Implementation
The idle system uses:
- `enterIdle()` - Activates idle face with boomerang animation
- `exitIdle()` - Returns to normal operation
- `updateIdleBlink()` - Manages random blink timing and double-blink logic
- `scheduleNextIdleBlink()` - Randomizes blink intervals for natural appearance
To customize idle behavior, modify the timing values in [sesame-firmware-main.ino](sesame-firmware-main.ino):
```cpp
scheduleNextIdleBlink(3000, 7000); // Min and max ms between blinks
```
## Asset Pipeline & Face Customization
The firmware includes an intelligent idle animation system that activates automatically:
### Behavior
- **Activation**: When the robot has received no commands for a period, it enters idle mode
- **Idle Face**: Displays a gentle "breathing" animation using the `idle` face in `FACE_ANIM_BOOMERANG` mode
- **Blinking**: Randomly triggers blink animations (3-7 second intervals)
- **Double Blinks**: 30% chance of a double blink for realistic behavior
- **Exit**: Any movement command or input immediately exits idle mode
### Technical Implementation
The idle system uses:
- `enterIdle()` - Activates idle face with boomerang animation
- `exitIdle()` - Returns to normal operation
- `updateIdleBlink()` - Manages random blink timing and double-blink logic
- `scheduleNextIdleBlink()` - Randomizes blink intervals for natural appearance
To customize idle behavior, modify the timing values in [sesame-firmware-main.ino](sesame-firmware-main.ino):
```cpp
scheduleNextIdleBlink(3000, 7000); // Min and max ms between blinks
```
## Hardware Abstraction Layer (HAL)
The firmware abstracts pin definitions via the `servoPins` array. The default configuration is optimized for the **Sesame Distro Board V1, V2, or V3** and **Lolin S2 Mini**, but is easily portable to any ESP32 with WiFi capability (e.g., S3, C3, or DevKit V1).
### Pin Configuration Tables
#### Lolin S2 Mini (ESP32-S2)
| Motor/Component | Array Index | GPIO Pin | Notes |
| ----------------- | ----------- | ------------ | ---------------------------- |
| Motor 0 | 0 | 1 | R1 |
| Motor 1 | 1 | 2 | R2 |
| Motor 2 | 2 | 4 | L1 |
| Motor 3 | 3 | 6 | L2 |
| Motor 4 | 4 | 8 | R4 |
| Motor 5 | 5 | 10 | R3 |
| Motor 6 | 6 | 13 | L3 |
| Motor 7 | 7 | 14 | L4 |
| **I2C SDA** | - | **33** | SSD1306 Data (Hardware I2C) |
| **I2C SCL** | - | **35** | SSD1306 Clock (Hardware I2C) |
#### Sesame Distro Board V1 (ESP32-WROOM32)
| Motor/Component | Array Index | GPIO Pin | Notes |
| ----------------- | ----------- | ------------ | ---------------------------- |
| Motor 0 | 0 | 15 | R1 |
| Motor 1 | 1 | 2 | R2 |
| Motor 2 | 2 | 23 | L1 |
| Motor 3 | 3 | 19 | L2 |
| Motor 4 | 4 | 4 | R4 |
| Motor 5 | 5 | 16 | R3 |
| Motor 6 | 6 | 17 | L3 |
| Motor 7 | 7 | 18 | L4 |
| **I2C SDA** | - | **21** | SSD1306 Data (Hardware I2C) |
| **I2C SCL** | - | **22** | SSD1306 Clock (Hardware I2C) |
#### Sesame Distro Board V3 (ESP32-S3)
| Motor/Component | Array Index | GPIO Pin | Notes |
| ----------------- | ----------- | ----------- | ---------------------------- |
| Motor 0 | 0 | 4 | R1 |
| Motor 1 | 1 | 5 | R2 |
| Motor 2 | 2 | 6 | L1 |
| Motor 3 | 3 | 7 | L2 |
| Motor 4 | 4 | 10 | R4 |
| Motor 5 | 5 | 11 | R3 |
| Motor 6 | 6 | 12 | L3 |
| Motor 7 | 7 | 13 | L4 |
| **I2C SDA** | - | **8** | SSD1306 Data (Hardware I2C) |
| **I2C SCL** | - | **9** | SSD1306 Clock (Hardware I2C) |
#### Sesame Distro Board V2 (ESP32-S3)
| Motor/Component | Array Index | GPIO Pin | Notes |
| ----------------- | ----------- | ----------- | ---------------------------- |
| Motor 0 | 0 | 4 | R1 |
| Motor 1 | 1 | 5 | R2 |
| Motor 2 | 2 | 6 | L1 |
| Motor 3 | 3 | 7 | L2 |
| Motor 4 | 4 | 15 | R4 |
| Motor 5 | 5 | 16 | R3 |
| Motor 6 | 6 | 17 | L3 |
| Motor 7 | 7 | 18 | L4 |
| **I2C SDA** | - | **8** | SSD1306 Data (Hardware I2C) |
| **I2C SCL** | - | **9** | SSD1306 Clock (Hardware I2C) |
### Porting to Other ESP32 Variants
To port this to a different ESP32 variant, modify the `servoPins` and `I2C_` defines in the header of [sesame-firmware-main.ino](sesame-firmware-main.ino). Ensure the chosen pins are PWM-capable and not "input-only".
## Asset Pipeline & Face Customization
To maintain a clean main source file and optimize performance, face bitmaps are decoupled from the primary logic. Faces are managed in [face-bitmaps.h](face-bitmaps.h) using a "Single Source of Truth" macro system.
### Face Library
The firmware includes an extensive library of faces organized into three categories:
#### Movement Faces
Synchronized with physical poses and animations:
- `walk`, `rest`, `stand`, `swim`, `dance`, `wave`, `point`
- `pushup`, `bow`, `cute`, `freaky`, `worm`, `shake`, `shrug`
- `dead`, `crab`, `idle`, `idle_blink`
#### Conversational Faces
Designed for expressive communication and voice assistant integration:
- **Base emotions**: `happy`, `sad`, `angry`, `surprised`, `sleepy`, `love`, `excited`, `confused`, `thinking`
- **Talk variants**: `talk_happy`, `talk_sad`, `talk_angry`, `talk_surprised`, `talk_sleepy`, `talk_love`, `talk_excited`, `talk_confused`, `talk_thinking`
The "talk_" variants feature open mouths for lip-sync and animated speech. These faces are perfect for:
- Voice assistant projects (Alexa, Google Assistant, custom TTS)
- Chatbot interfaces controlled via the JSON API
- Interactive storytelling and educational applications
- Remote-controlled performances
#### Special Faces
- `default` - The startup/fallback face
- `idle` - Gentle breathing animation for idle state
- `idle_blink` - Blink animation triggered randomly during idle
### Adding Custom Faces
### Workflow:
1. **Image Creation**: Find faces using Kaomoji or [Emojicombos](https://emojicombos.com/kaomoji). Create a `128x64` image in a tool like [jsPaint](https://jspaint.app/).
2. **Bitmap Conversion**: Use [image2cpp](https://javl.github.io/image2cpp/) with `Horizontal` scaling, `128x64` resolution, and `Arduino Code` output.
3. **Registration**:
- Add your face name to the `FACE_LIST` macro in [face-bitmaps.h](face-bitmaps.h).
- Paste the generated C array into [face-bitmaps.h](face-bitmaps.h) right after the last bitmap in the list.
- (Optional) For animations, add numbered suffixes (e.g., `_1`, `_2`) and register the FPS in the `faceFpsEntries` in [sesame-firmware-main.ino](sesame-firmware-main.ino).
### Animating Faces
For an animation to be recognized by the `MAKE_FACE_FRAMES` macro, your array names in [face-bitmaps.h](face-bitmaps.h) must follow a strict naming convention:
- **Root Frame**: `epd_bitmap_myface` (This is required and acts as frame 0).
- **Subsequent Frames**: `epd_bitmap_myface_1`, `epd_bitmap_myface_2`, etc.
- **Limit**: The default system supports up to 6 frames per face (Root + 5 numbered frames).
- **Animation Modes**: Animations can be configured to play as a `LOOP` (restarts at frame 0), `ONCE` (stops on the final frame), or `BOOMERANG` (plays forward then reverses). These modes are typically defined in [movement-sequences.h](movement-sequences.h) when triggerring a pose.
### Macro System
The `FACE_LIST` macro uses X-Macros to automatically generate variable declarations and registration objects:
```cpp
#define FACE_LIST \
X(walk) \
X(rest) \
X(my_new_face) // Just add this line!
```
This eliminates the need to manually update multiple switch statements or arrays when adding new assets.
## Execution & Deployment
1. **Toolchain**: Configure your IDE for `ESP32 Dev Module` or `Lolin S2 Mini`.
2. **Calibration**: Use the Serial Monitor (115200) to send manual step commands (e.g., `rn wf`).
3. **Power Management**: If the robot brownouts during movement, increase `motorCurrentDelay` in the web settings to further stagger servo bursts.
+851
View File
@@ -0,0 +1,851 @@
#pragma once
#include <Arduino.h>
// ======================================================================
// --- WEB INTERFACE HTML ---
// ======================================================================
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
<title>Sesame Access Point Controller</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8">
<style>
:root {
--content-color: #ff8c42;
--content-color-dark: #e67a30;
--content-color-darker: #cc6b29;
--content-color-glow: rgba(255, 140, 66, 0.3);
}
* {
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none;
}
body {
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
text-align: center;
background: linear-gradient(135deg, #0a0a0a, #1a1a2e);
color: #e0e0e0;
touch-action: manipulation;
margin: 0;
padding: 10px;
overflow-x: hidden;
box-sizing: border-box;
}
h2 {
margin: 10px 0 20px 0;
color: #fff;
font-size: 32px;
font-weight: 600;
text-shadow: 0 2px 4px rgba(0,0,0,0.5);
}
/* Command Queue Status */
.command-queue {
font-size: 12px;
color: #888;
margin-bottom: 20px;
}
.command-queue.full {
color: #ff6b6b;
font-weight: bold;
}
/* Section Containers */
.sections-container {
display: flex;
flex-direction: column;
gap: 15px;
max-width: 1400px;
margin: 0 auto;
}
.section {
background: rgba(30, 30, 30, 0.8);
border: 1px solid #333;
border-radius: 16px;
padding: 15px;
margin: 0 auto;
width: calc(100% - 20px);
max-width: 450px;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
box-sizing: border-box;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: var(--content-color);
margin: 0 0 15px 0;
text-transform: uppercase;
letter-spacing: 1px;
}
/* Button Base Styles */
button {
background: linear-gradient(145deg, #3a3a3a, #2a2a2a);
border: none;
color: #e0e0e0;
padding: 15px;
font-size: 18px;
border-radius: 12px;
cursor: pointer;
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
transition: all 0.1s;
font-weight: 500;
}
button:active {
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
transform: translateY(2px);
}
/* D-Pad Controls */
.dpad-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
width: 100%;
}
.dpad {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(2, 1fr);
gap: 12px;
width: 100%;
max-width: 294px;
aspect-ratio: 3 / 2;
}
.dpad button {
font-size: 35px;
border: 2px solid #555;
color: #fff;
width: 100%;
height: 100%;
min-height: 70px;
}
.spacer {
visibility: hidden;
}
/* Pose Grid */
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
.btn-pose {
background: linear-gradient(145deg, var(--content-color), var(--content-color-dark));
padding: 12px 8px;
font-size: 15px;
}
.btn-pose:active {
background: linear-gradient(145deg, var(--content-color-dark), var(--content-color-darker));
}
/* Special Buttons */
.btn-stop-all {
background: linear-gradient(145deg, #e63946, #c92a35);
width: 100%;
font-size: 20px;
padding: 18px;
box-shadow: 0 6px 12px rgba(230, 57, 70, 0.4);
border: 2px solid #ff6b6b;
color: #fff;
text-transform: uppercase;
letter-spacing: 2px;
}
.btn-stop-all:active {
background: linear-gradient(145deg, #c92a35, #a8222c);
transform: translateY(3px);
}
.btn-settings {
background: linear-gradient(145deg, #555, #444);
padding: 12px 25px;
font-size: 16px;
}
/* Motor Controls */
.lock-indicator {
font-size: 11px;
color: #ff6b6b;
text-align: center;
margin-top: 5px;
display: none;
}
.lock-indicator.active {
display: block;
}
.motor-controls {
margin-top: 10px;
}
.motor-slider {
margin: 15px 0;
}
.motor-slider label {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #aaa;
margin-bottom: 5px;
}
.motor-slider input[type="range"] {
width: 100%;
height: 6px;
background: #333;
border-radius: 5px;
outline: none;
-webkit-appearance: none;
}
.motor-slider input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 18px;
height: 18px;
background: var(--content-color);
border-radius: 50%;
cursor: pointer;
box-shadow: 0 2px 6px var(--content-color-glow);
}
.motor-slider input[type="range"]::-moz-range-thumb {
width: 18px;
height: 18px;
background: var(--content-color);
border-radius: 50%;
cursor: pointer;
border: none;
box-shadow: 0 2px 6px var(--content-color-glow);
}
.motor-slider input[type="range"]:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.motor-slider input[type="range"]:disabled::-webkit-slider-thumb,
.motor-slider input[type="range"]:disabled::-moz-range-thumb {
background: #666;
cursor: not-allowed;
}
/* Gamepad Status */
.gamepad-status {
font-size: 13px;
padding: 8px 14px;
border-radius: 10px;
border: 2px solid #666;
color: #ccc;
background: rgba(26, 26, 26, 0.8);
display: inline-block;
}
.gamepad-status.connected {
border-color: #2ecc71;
color: #2ecc71;
background: rgba(46, 204, 113, 0.1);
}
/* Settings Panel */
.settings-panel {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.9);
z-index: 100;
backdrop-filter: blur(8px);
overflow-y: auto;
}
.settings-content {
background: linear-gradient(145deg, #1e1e1e, #2a2a2a);
border: 1px solid #444;
max-width: 400px;
margin: 30px auto;
padding: 25px;
border-radius: 20px;
text-align: left;
box-shadow: 0 10px 40px rgba(0,0,0,0.6);
}
.settings-content h3 {
color: var(--content-color);
margin-top: 0;
text-align: center;
font-size: 24px;
}
.settings-section {
margin: 20px 0;
padding: 15px;
background: rgba(0,0,0,0.3);
border-radius: 10px;
}
.settings-section h4 {
color: var(--content-color);
margin: 0 0 10px 0;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 1px;
}
.settings-content label {
display: block;
margin-top: 12px;
font-weight: 500;
color: #ccc;
font-size: 13px;
}
.settings-content input,
.settings-content select {
width: 100%;
padding: 10px;
margin-top: 5px;
background: #333;
color: #fff;
border: 1px solid #555;
border-radius: 8px;
box-sizing: border-box;
font-size: 14px;
}
.btn-save {
background: linear-gradient(145deg, #2ecc71, #27ae60);
box-shadow: 0 4px 8px rgba(46, 204, 113, 0.3);
width: 100%;
margin-top: 25px;
color: #fff;
}
.btn-close {
background: linear-gradient(145deg, #e74c3c, #c0392b);
box-shadow: 0 4px 8px rgba(231, 76, 60, 0.3);
width: 100%;
margin-top: 12px;
color: #fff;
}
/* Desktop Layout */
@media (min-width: 1024px) {
body {
padding: 15px;
}
.section {
padding: 20px;
width: 100%;
}
h2 {
margin-bottom: 30px;
}
.command-queue {
margin-bottom: 30px;
}
.sections-container {
flex-direction: row;
justify-content: center;
align-items: flex-start;
gap: 50px;
padding: 0 20px;
}
.section-column {
flex: 0 1 450px;
display: flex;
flex-direction: column;
gap: 20px;
}
.section {
width: 100%;
max-width: 450px;
margin: 0;
}
}
</style>
</head>
<body>
<h2>Sesame Controller</h2>
<div class="command-queue" id="queueStatus">Command Queue: 0/3</div>
<div class="sections-container">
<div class="section-column">
<!-- Movement Control Section -->
<div class="section">
<div class="section-title">Movement Control</div>
<div class="dpad-container">
<div class="dpad">
<div class="spacer"></div>
<button onmousedown="move('forward')" onmouseup="stop()" ontouchstart="move('forward')" ontouchend="stop()">&#9650;</button>
<div class="spacer"></div>
<button onmousedown="move('left')" onmouseup="stop()" ontouchstart="move('left')" ontouchend="stop()">&#9664;</button>
<button onmousedown="move('backward')" onmouseup="stop()" ontouchstart="move('backward')" ontouchend="stop()">&#9660;</button>
<button onmousedown="move('right')" onmouseup="stop()" ontouchstart="move('right')" ontouchend="stop()">&#9654;</button>
</div>
<button class="btn-stop-all" onclick="stop()">STOP ALL</button>
</div>
</div>
<!-- Poses & Animations Section -->
<div class="section">
<div class="section-title">Poses & Animations</div>
<div class="grid">
<button class="btn-pose" onclick="pose('rest')">Rest</button>
<button class="btn-pose" onclick="pose('stand')">Stand</button>
<button class="btn-pose" onclick="pose('wave')">Wave</button>
<button class="btn-pose" onclick="pose('dance')">Dance</button>
<button class="btn-pose" onclick="pose('swim')">Swim</button>
<button class="btn-pose" onclick="pose('point')">Point</button>
<button class="btn-pose" onclick="pose('pushup')">Pushup</button>
<button class="btn-pose" onclick="pose('bow')">Bow</button>
<button class="btn-pose" onclick="pose('cute')">Cute</button>
<button class="btn-pose" onclick="pose('freaky')">Freaky</button>
<button class="btn-pose" onclick="pose('worm')">Worm</button>
<button class="btn-pose" onclick="pose('shake')">Shake</button>
<button class="btn-pose" onclick="pose('shrug')">Shrug</button>
<button class="btn-pose" onclick="pose('dead')">Dead</button>
<button class="btn-pose" onclick="pose('crab')">Crab</button>
</div>
</div>
</div>
<div class="section-column">
<!-- Settings & Status Section -->
<div class="section">
<div class="section-title">System</div>
<button class="btn-settings" onclick="openSettings()">Settings</button>
<div style="margin-top: 15px;">
<div id="gamepadStatus" class="gamepad-status">Gamepad disconnected</div>
</div>
</div>
</div>
</div>
<div id="settingsPanel" class="settings-panel">
<div class="settings-content">
<h3>Settings</h3>
<div class="settings-section">
<h4>Animation Parameters</h4>
<label>Frame Delay (ms):</label>
<input type="number" id="frameDelay" min="1" max="1000" step="1">
<label>Walk Cycles:</label>
<input type="number" id="walkCycles" min="1" max="50" step="1">
</div>
<div class="settings-section">
<h4>Motor Settings</h4>
<label>Motor Current Delay (ms):</label>
<input type="number" id="motorCurrentDelay" min="0" max="500" step="1">
<label>Motor Speed:</label>
<select id="motorSpeed">
<option value="slow">Slow</option>
<option value="medium" selected>Medium</option>
<option value="fast">Fast</option>
</select>
</div>
<div class="settings-section">
<h4>Theme</h4>
<label>Accent Color:</label>
<select id="themeColor">
<option value="#ff8c42">Orange (Default)</option>
<option value="#66d9ef">Cyan</option>
<option value="#a8dadc">Light Blue</option>
<option value="#2ecc71">Green</option>
<option value="#e74c3c">Red</option>
<option value="#9b59b6">Purple</option>
<option value="#f39c12">Yellow</option>
<option value="#e91e63">Pink</option>
<option value="custom">Custom</option>
</select>
<input type="color" id="customColor" value="#ff8c42" style="margin-top: 10px; display: none;">
</div>
<button class="btn-settings" style="width: 100%; margin-top: 20px;" onclick="openMotorControl()">Manual Motor Control</button>
<button class="btn-save" onclick="saveSettings()">Save Settings</button>
<button class="btn-close" onclick="closeSettings()">Close</button>
</div>
</div>
<div id="motorControlPanel" class="settings-panel">
<div class="settings-content">
<h3>Manual Motor Control</h3>
<div class="lock-indicator" id="lockIndicator">Locked during animations</div>
<div class="settings-section">
<div class="motor-controls">
<div class="motor-slider">
<label><span>S0 R1</span><span id="m1val">90&deg;</span></label>
<input type="range" id="motor1" min="0" max="180" value="90" oninput="updateMotor(1, this.value)">
</div>
<div class="motor-slider">
<label><span>S1 R2</span><span id="m2val">90&deg;</span></label>
<input type="range" id="motor2" min="0" max="180" value="90" oninput="updateMotor(2, this.value)">
</div>
<div class="motor-slider">
<label><span>S2 L1</span><span id="m3val">90&deg;</span></label>
<input type="range" id="motor3" min="0" max="180" value="90" oninput="updateMotor(3, this.value)">
</div>
<div class="motor-slider">
<label><span>S3 L2</span><span id="m4val">90&deg;</span></label>
<input type="range" id="motor4" min="0" max="180" value="90" oninput="updateMotor(4, this.value)">
</div>
<div class="motor-slider">
<label><span>S4 R4</span><span id="m5val">90&deg;</span></label>
<input type="range" id="motor5" min="0" max="180" value="90" oninput="updateMotor(5, this.value)">
</div>
<div class="motor-slider">
<label><span>S5 R3</span><span id="m6val">90&deg;</span></label>
<input type="range" id="motor6" min="0" max="180" value="90" oninput="updateMotor(6, this.value)">
</div>
<div class="motor-slider">
<label><span>S6 L3</span><span id="m7val">90&deg;</span></label>
<input type="range" id="motor7" min="0" max="180" value="90" oninput="updateMotor(7, this.value)">
</div>
<div class="motor-slider">
<label><span>S7 L4</span><span id="m8val">90&deg;</span></label>
<input type="range" id="motor8" min="0" max="180" value="90" oninput="updateMotor(8, this.value)">
</div>
</div>
</div>
<button class="btn-close" onclick="closeMotorControl()">Close</button>
</div>
</div>
<script>
// Command queue management - max 3 commands
let commandQueue = 0;
const MAX_COMMANDS = 3;
let motorsLocked = false;
// Load theme on page load
document.addEventListener('DOMContentLoaded', () => {
loadTheme();
});
function loadTheme() {
const savedColor = localStorage.getItem('themeColor');
if (savedColor) {
applyTheme(savedColor);
}
}
function applyTheme(color) {
const root = document.documentElement;
root.style.setProperty('--content-color', color);
// Calculate darker shades
const rgb = hexToRgb(color);
if (rgb) {
const dark = `rgb(${Math.max(0, rgb.r - 20)}, ${Math.max(0, rgb.g - 20)}, ${Math.max(0, rgb.b - 20)})`;
const darker = `rgb(${Math.max(0, rgb.r - 40)}, ${Math.max(0, rgb.g - 40)}, ${Math.max(0, rgb.b - 40)})`;
const glow = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, 0.3)`;
root.style.setProperty('--content-color-dark', dark);
root.style.setProperty('--content-color-darker', darker);
root.style.setProperty('--content-color-glow', glow);
}
}
function hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
function updateQueueStatus() {
const queueEl = document.getElementById('queueStatus');
queueEl.textContent = `Command Queue: ${commandQueue}/${MAX_COMMANDS}`;
if (commandQueue >= MAX_COMMANDS) {
queueEl.classList.add('full');
} else {
queueEl.classList.remove('full');
}
}
function canSendCommand() {
return commandQueue < MAX_COMMANDS;
}
function incrementQueue() {
commandQueue++;
updateQueueStatus();
setTimeout(() => {
if (commandQueue > 0) {
commandQueue--;
}
updateQueueStatus();
}, 1000);
}
function lockMotors(duration = 3000) {
motorsLocked = true;
document.getElementById('lockIndicator').classList.add('active');
for (let i = 1; i <= 8; i++) {
const slider = document.getElementById('motor' + i);
if (slider) slider.disabled = true;
}
setTimeout(() => {
motorsLocked = false;
document.getElementById('lockIndicator').classList.remove('active');
for (let i = 1; i <= 8; i++) {
const slider = document.getElementById('motor' + i);
if (slider) slider.disabled = false;
}
}, duration);
}
function move(dir) {
if (!canSendCommand()) return;
incrementQueue();
fetch('/cmd?go=' + dir).catch(console.log);
}
function stop() {
commandQueue = 0;
updateQueueStatus();
fetch('/cmd?stop=1').catch(console.log);
}
function pose(name) {
if (!canSendCommand()) return;
incrementQueue();
lockMotors(3000);
fetch('/cmd?pose=' + name).catch(console.log);
}
function updateMotor(motorNum, value) {
if (motorsLocked) return;
document.getElementById('m' + motorNum + 'val').textContent = value + '\u00B0';
if (!canSendCommand()) return;
incrementQueue();
fetch('/cmd?motor=' + motorNum + '&value=' + value).catch(console.log);
}
function openSettings() {
fetch('/getSettings').then(r => r.json()).then(data => {
document.getElementById('frameDelay').value = data.frameDelay || 100;
document.getElementById('walkCycles').value = data.walkCycles || 10;
document.getElementById('motorCurrentDelay').value = data.motorCurrentDelay || 20;
document.getElementById('motorSpeed').value = data.motorSpeed || 'medium';
// Load theme settings
const savedColor = localStorage.getItem('themeColor') || '#ff8c42';
const colorSelect = document.getElementById('themeColor');
const customColorInput = document.getElementById('customColor');
// Check if saved color matches a preset
let matchFound = false;
for (let option of colorSelect.options) {
if (option.value === savedColor) {
colorSelect.value = savedColor;
matchFound = true;
break;
}
}
if (!matchFound) {
colorSelect.value = 'custom';
customColorInput.value = savedColor;
customColorInput.style.display = 'block';
}
document.getElementById('settingsPanel').style.display = 'block';
}).catch(() => {
// Fallback if settings endpoint doesn't exist yet
document.getElementById('frameDelay').value = 100;
document.getElementById('walkCycles').value = 10;
document.getElementById('motorCurrentDelay').value = 20;
const savedColor = localStorage.getItem('themeColor') || '#ff8c42';
document.getElementById('themeColor').value = savedColor;
document.getElementById('settingsPanel').style.display = 'block';
});
// Add event listener for theme color change
document.getElementById('themeColor').addEventListener('change', function() {
const customColorInput = document.getElementById('customColor');
if (this.value === 'custom') {
customColorInput.style.display = 'block';
} else {
customColorInput.style.display = 'none';
applyTheme(this.value);
}
});
document.getElementById('customColor').addEventListener('input', function() {
applyTheme(this.value);
});
}
function closeSettings() {
document.getElementById('settingsPanel').style.display = 'none';
}
function openMotorControl() {
document.getElementById('motorControlPanel').style.display = 'block';
}
function closeMotorControl() {
document.getElementById('motorControlPanel').style.display = 'none';
}
function saveSettings() {
const fd = document.getElementById('frameDelay').value;
const wc = document.getElementById('walkCycles').value;
const mcd = document.getElementById('motorCurrentDelay').value;
const ms = document.getElementById('motorSpeed').value;
// Save theme color
const colorSelect = document.getElementById('themeColor');
const customColorInput = document.getElementById('customColor');
const themeColor = colorSelect.value === 'custom' ? customColorInput.value : colorSelect.value;
localStorage.setItem('themeColor', themeColor);
applyTheme(themeColor);
fetch(`/setSettings?frameDelay=${fd}&walkCycles=${wc}&motorCurrentDelay=${mcd}&motorSpeed=${ms}`)
.then(() => closeSettings())
.catch(() => closeSettings());
}
let activeGamepadIndex = null;
let gamepadPollId = null;
let lastButtonStates = [];
let lastAxisDir = { x: 0, y: 0 };
const axisThreshold = 0.5;
const pollIntervalMs = 80;
const buttonBindings = {
0: () => pose('stand'), // A / Cross
1: () => pose('wave'), // B / Circle
2: () => pose('dance'), // X / Square
3: () => pose('swim'), // Y / Triangle
4: () => pose('point'), // LB / L1
5: () => pose('pushup'), // RB / R1
6: () => pose('bow'), // LT / L2
7: () => pose('shake'), // RT / R2
8: () => stop(), // Back / Share
9: () => pose('rest'), // Start / Options
10: () => pose('cute'), // L3
11: () => pose('freaky'), // R3
12: () => move('forward'),// D-pad up
13: () => move('backward'),// D-pad down
14: () => move('left'), // D-pad left
15: () => move('right'), // D-pad right
16: () => stop(), // Home / PS
17: () => pose('worm') // Touchpad / extra
};
const buttonReleaseStop = new Set([12, 13, 14, 15]);
function updateGamepadStatus(connected) {
const status = document.getElementById('gamepadStatus');
if (!status) return;
if (connected) {
status.textContent = 'Gamepad connected';
status.classList.add('connected');
} else {
status.textContent = 'Gamepad disconnected';
status.classList.remove('connected');
}
}
function handleButtonChange(index, pressed) {
if (pressed) {
const action = buttonBindings[index];
if (action) action();
} else if (buttonReleaseStop.has(index)) {
stop();
}
}
function getAxisDirection(x, y) {
if (Math.abs(x) < axisThreshold && Math.abs(y) < axisThreshold) return { x: 0, y: 0 };
if (Math.abs(x) > Math.abs(y)) {
return { x: x > 0 ? 1 : -1, y: 0 };
}
return { x: 0, y: y > 0 ? 1 : -1 };
}
function applyAxisDirection(dir) {
if (dir.x === 1) move('right');
else if (dir.x === -1) move('left');
else if (dir.y === 1) move('backward');
else if (dir.y === -1) move('forward');
else stop();
}
function pollGamepad() {
const pads = navigator.getGamepads ? navigator.getGamepads() : [];
const pad = pads && activeGamepadIndex !== null ? pads[activeGamepadIndex] : null;
if (!pad) {
updateGamepadStatus(false);
return;
}
updateGamepadStatus(true);
if (!lastButtonStates.length) {
lastButtonStates = pad.buttons.map(b => !!b.pressed);
}
pad.buttons.forEach((btn, i) => {
const pressed = !!btn.pressed;
if (pressed !== lastButtonStates[i]) {
handleButtonChange(i, pressed);
lastButtonStates[i] = pressed;
}
});
const x = pad.axes[0] || 0;
const y = pad.axes[1] || 0;
const dir = getAxisDirection(x, y);
if (dir.x !== lastAxisDir.x || dir.y !== lastAxisDir.y) {
applyAxisDirection(dir);
lastAxisDir = dir;
}
}
window.addEventListener('gamepadconnected', (e) => {
activeGamepadIndex = e.gamepad.index;
lastButtonStates = [];
lastAxisDir = { x: 0, y: 0 };
updateGamepadStatus(true);
if (!gamepadPollId) {
gamepadPollId = setInterval(pollGamepad, pollIntervalMs);
}
});
window.addEventListener('gamepaddisconnected', (e) => {
if (activeGamepadIndex === e.gamepad.index) {
activeGamepadIndex = null;
lastButtonStates = [];
lastAxisDir = { x: 0, y: 0 };
updateGamepadStatus(false);
}
});
if (navigator.getGamepads) {
setInterval(() => {
if (activeGamepadIndex !== null) return;
const pads = navigator.getGamepads();
if (!pads) return;
for (let i = 0; i < pads.length; i++) {
if (pads[i]) {
activeGamepadIndex = pads[i].index;
updateGamepadStatus(true);
if (!gamepadPollId) {
gamepadPollId = setInterval(pollGamepad, pollIntervalMs);
}
break;
}
}
}, 1000);
}
</script>
</body>
</html>
)rawliteral";
@@ -0,0 +1,142 @@
#include <Arduino.h>
#include <ESP32Servo.h>
// ======================================================================
// --- CONFIGURATION ---
// ======================================================================
Servo servos[8];
// Motor Pin Mapping
// Index: 0 1 2 3 4 5 6 7
const int servoPins[8] = {1, 2, 4, 6, 8, 10, 13, 14};
// Distro board V3 pins: (uncomment if needed)
// const int servoPins[8] = {4, 5, 6, 7, 10, 11, 12, 13};
// Distro board V2 pins: (uncomment if needed)
// const int servoPins[8] = {4, 5, 6, 7, 15, 16, 17, 18};
// Distro board V1 pins: (uncomment if needed)
// const int servoPins[8] = {15, 2, 23, 19, 4, 16, 17, 18};
// Pulse width settings
const int MIN_PULSE = 732;
const int MAX_PULSE = 2929;
// ======================================================================
// --- SETUP ---
// ======================================================================
void setup() {
Serial.begin(115200);
while (!Serial);
Serial.println("-----------------------------------");
Serial.println(" Sesame Motor Tester Interface ");
Serial.println("-----------------------------------");
Serial.println("Commands:");
Serial.println("1. id,angle -> e.g. '0,90'");
Serial.println("2. all,angle -> e.g. 'all,90'");
Serial.println("3. stop -> Detaches/Powers down motors");
Serial.println("-----------------------------------");
Serial.println("Status: Motors are currently OFF (Limp).");
// Allocate timers for ESP32Servo
ESP32PWM::allocateTimer(0);
ESP32PWM::allocateTimer(1);
ESP32PWM::allocateTimer(2);
ESP32PWM::allocateTimer(3);
// We do NOT attach motors here. They stay limp until a command is sent.
}
// ======================================================================
// --- MAIN LOOP ---
// ======================================================================
void loop() {
if (Serial.available() > 0) {
String input = Serial.readStringUntil('\n');
input.trim(); // Remove whitespace/newlines
if (input.length() == 0) return;
// --- Check for STOP command ---
if (input.equalsIgnoreCase("stop")) {
stopMotors();
return;
}
// --- Parse "key,value" format ---
int commaIndex = input.indexOf(',');
if (commaIndex != -1) {
String cmd = input.substring(0, commaIndex);
String valStr = input.substring(commaIndex + 1);
int angle = valStr.toInt();
// Check Angle Limits
if (angle < 0) angle = 0;
if (angle > 180) angle = 180;
// Check if command is "all" or a motor ID
if (cmd.equalsIgnoreCase("all")) {
moveAll(angle);
} else {
// Assume it's a number
int motorId = cmd.toInt();
// Basic check to ensure "0" wasn't actually text garbage
if (motorId == 0 && cmd.charAt(0) != '0') {
Serial.println("Error: Invalid Motor ID");
} else {
moveMotor(motorId, angle);
}
}
} else {
Serial.println("Error: Invalid format. Use 'id,angle', 'all,angle', or 'stop'.");
}
}
}
// ======================================================================
// --- HELPER FUNCTIONS ---
// ======================================================================
void moveMotor(int id, int angle) {
if (id < 0 || id > 7) {
Serial.println("Error: Motor ID must be 0-7");
return;
}
// If servo isn't attached yet (powered off), attach it now
if (!servos[id].attached()) {
servos[id].setPeriodHertz(50);
servos[id].attach(servoPins[id], MIN_PULSE, MAX_PULSE);
}
servos[id].write(angle);
Serial.print("OK: Motor ");
Serial.print(id);
Serial.print(" -> ");
Serial.println(angle);
}
void moveAll(int angle) {
Serial.print("Moving ALL to ");
Serial.println(angle);
for (int i = 0; i < 8; i++) {
moveMotor(i, angle);
}
}
void stopMotors() {
Serial.println("Stopping (Detaching) all motors...");
for (int i = 0; i < 8; i++) {
if (servos[i].attached()) {
servos[i].detach();
}
}
Serial.println("Motors are now OFF.");
}
File diff suppressed because it is too large Load Diff
+429
View File
@@ -0,0 +1,429 @@
#pragma once
#include <Arduino.h>
enum ServoName : uint8_t {
R1 = 0,
R2 = 1,
L1 = 2,
L2 = 3,
R4 = 4,
R3 = 5,
L3 = 6,
L4 = 7
};
const String ServoNames[]={"R1","R2","L1","L2","R4","R3","L3","L4"};
inline int servoNameToIndex(const String& servo) {
if (servo == "L1") return L1;
if (servo == "L2") return L2;
if (servo == "L3") return L3;
if (servo == "L4") return L4;
if (servo == "R1") return R1;
if (servo == "R2") return R2;
if (servo == "R3") return R3;
if (servo == "R4") return R4;
return -1;
}
enum FaceAnimMode : uint8_t {
FACE_ANIM_LOOP = 0,
FACE_ANIM_ONCE = 1,
FACE_ANIM_BOOMERANG = 2
};
// External globals and helpers used by movement/pose sequences
extern int frameDelay;
extern int walkCycles;
extern String currentCommand;
extern void setServoAngle(uint8_t channel, int angle);
extern void setFace(const String& faceName);
extern void setFaceMode(FaceAnimMode mode);
extern void setFaceWithMode(const String& faceName, FaceAnimMode mode);
extern void delayWithFace(unsigned long ms);
extern void enterIdle();
extern bool pressingCheck(String cmd, int ms);
// Pose/animation prototypes
void runRestPose();
void runStandPose(int face = 1);
void runWavePose();
void runDancePose();
void runSwimPose();
void runPointPose();
void runPushupPose();
void runBowPose();
void runCutePose();
void runFreakyPose();
void runWormPose();
void runShakePose();
void runShrugPose();
void runDeadPose();
void runCrabPose();
void runWalkPose();
void runWalkBackward();
void runTurnLeft();
void runTurnRight();
// ====== POSES ======
inline void runRestPose() {
Serial.println(F("REST"));
setFaceWithMode("rest", FACE_ANIM_BOOMERANG);
for (int i = 0; i < 8; i++) setServoAngle(i, 90);
}
inline void runStandPose(int face) {
Serial.println(F("STAND"));
if (face == 1) setFaceWithMode("stand", FACE_ANIM_ONCE);
setServoAngle(R1, 135);
setServoAngle(R2, 45);
setServoAngle(L1, 45);
setServoAngle(L2, 135);
setServoAngle(R4, 0);
setServoAngle(R3, 180);
setServoAngle(L3, 0);
setServoAngle(L4, 180);
if (face == 1) enterIdle();
}
inline void runWavePose() {
Serial.println(F("WAVE"));
setFaceWithMode("wave", FACE_ANIM_ONCE);
runStandPose(0);
delayWithFace(200);
setServoAngle(R4, 80); setServoAngle(L3, 180);
setServoAngle(L2, 90); setServoAngle(R1, 100);
delayWithFace(200);
setServoAngle(L3, 180);
delayWithFace(300);
for (int i = 0; i < 4; i++) {
setServoAngle(L3, 180); delayWithFace(300);
setServoAngle(L3, 100); delayWithFace(300);
}
runStandPose(1);
if (currentCommand == "wave") currentCommand = "";
}
inline void runDancePose() {
Serial.println(F("DANCE"));
setFaceWithMode("dance", FACE_ANIM_LOOP);
setServoAngle(R1, 90); setServoAngle(R2, 90);
setServoAngle(L1, 90); setServoAngle(L2, 90);
setServoAngle(R4, 160); setServoAngle(R3, 160);
setServoAngle(L3, 10); setServoAngle(L4, 10);
delayWithFace(300);
for (int i = 0; i < 5; i++) {
setServoAngle(R4, 115); setServoAngle(R3, 115);
setServoAngle(L3, 10); setServoAngle(L4, 10);
delayWithFace(300);
setServoAngle(R4, 160); setServoAngle(R3, 160);
setServoAngle(L3, 65); setServoAngle(L4, 65);
delayWithFace(300);
}
runStandPose(1);
if (currentCommand == "dance") currentCommand = "";
}
inline void runSwimPose() {
Serial.println(F("SWIM"));
setFaceWithMode("swim", FACE_ANIM_ONCE);
for (int i = 0; i < 8; i++) setServoAngle(i, 90);
for (int i = 0; i < 4; i++) {
setServoAngle(R1, 135); setServoAngle(R2, 45);
setServoAngle(L1, 45); setServoAngle(L2, 135);
delayWithFace(400);
setServoAngle(R1, 90); setServoAngle(R2, 90);
setServoAngle(L1, 90); setServoAngle(L2, 90);
delayWithFace(400);
}
runStandPose(1);
if (currentCommand == "swim") currentCommand = "";
}
inline void runPointPose() {
Serial.println(F("POINT"));
setFaceWithMode("point", FACE_ANIM_BOOMERANG);
setServoAngle(L2, 90); setServoAngle(R1, 135);
setServoAngle(R2, 100); setServoAngle(L4, 180);
setServoAngle(L1, 25); setServoAngle(L3, 145);
setServoAngle(R4, 80); setServoAngle(R3, 170);
delayWithFace(2000);
runStandPose(1);
if (currentCommand == "point") currentCommand = "";
}
inline void runPushupPose() {
Serial.println(F("PUSHUP"));
setFaceWithMode("pushup", FACE_ANIM_ONCE);
runStandPose(0);
delayWithFace(200);
setServoAngle(L1, 0);
setServoAngle(R1, 180);
setServoAngle(L3, 90);
setServoAngle(R3, 90);
delayWithFace(500);
for (int i = 0; i < 4; i++) {
setServoAngle(L3, 0);
setServoAngle(R3, 180);
delayWithFace(600);
setServoAngle(L3, 90);
setServoAngle(R3, 90);
delayWithFace(500);
}
runStandPose(1);
if (currentCommand == "pushup") currentCommand = "";
}
inline void runBowPose() {
Serial.println(F("BOW"));
setFaceWithMode("bow", FACE_ANIM_ONCE);
runStandPose(0);
delayWithFace(200);
setServoAngle(L1, 0);
setServoAngle(R1, 180);
setServoAngle(L3, 0);
setServoAngle(R3, 180);
setServoAngle(L2, 180);
setServoAngle(R2, 0);
setServoAngle(R4, 0);
setServoAngle(L4, 180);
delayWithFace(600);
setServoAngle(L3, 90);
setServoAngle(R3, 90);
delayWithFace(3000);
runStandPose(1);
if (currentCommand == "bow") currentCommand = "";
}
inline void runCutePose() {
Serial.println(F("CUTE"));
setFaceWithMode("cute", FACE_ANIM_ONCE);
runStandPose(0);
delayWithFace(200);
setServoAngle(L2, 160);
setServoAngle(R2, 20);
setServoAngle(R4, 180);
setServoAngle(L4, 0);
setServoAngle(L1, 0);
setServoAngle(R1, 180);
setServoAngle(L3, 180);
setServoAngle(R3, 0);
delayWithFace(200);
for (int i = 0; i < 5; i++) {
setServoAngle(R4, 180);
setServoAngle(L4, 45);
delayWithFace(300);
setServoAngle(R4, 135);
setServoAngle(L4, 0);
delayWithFace(300);
}
runStandPose(1);
if (currentCommand == "cute") currentCommand = "";
}
inline void runFreakyPose() {
Serial.println(F("FREAKY"));
setFaceWithMode("freaky", FACE_ANIM_ONCE);
runStandPose(0);
delayWithFace(200);
setServoAngle(L1, 0);
setServoAngle(R1, 180);
setServoAngle(L2, 180);
setServoAngle(R2, 0);
setServoAngle(R4, 90);
setServoAngle(R3, 0);
delayWithFace(200);
for (int i = 0; i < 3; i++) {
setServoAngle(R3, 25);
delayWithFace(400);
setServoAngle(R3, 0);
delayWithFace(400);
}
runStandPose(1);
if (currentCommand == "freaky") currentCommand = "";
}
inline void runWormPose() {
Serial.println(F("WORM"));
setFaceWithMode("worm", FACE_ANIM_ONCE);
runStandPose(0);
delayWithFace(200);
setServoAngle(R1, 180); setServoAngle(R2, 0); setServoAngle(L1, 0); setServoAngle(L2, 180);
setServoAngle(R4, 90); setServoAngle(R3, 90); setServoAngle(L3, 90); setServoAngle(L4, 90);
delayWithFace(200);
for(int i=0; i<5; i++) {
setServoAngle(R3, 45); setServoAngle(L3, 135); setServoAngle(R4, 45); setServoAngle(L4, 135);
delayWithFace(300);
setServoAngle(R3, 135); setServoAngle(L3, 45); setServoAngle(R4, 135); setServoAngle(L4, 45);
delayWithFace(300);
}
runStandPose(1);
if (currentCommand == "worm") currentCommand = "";
}
inline void runShakePose() {
Serial.println(F("SHAKE"));
setFaceWithMode("shake", FACE_ANIM_ONCE);
runStandPose(0);
delayWithFace(200);
setServoAngle(R1, 135); setServoAngle(L1, 45); setServoAngle(L3, 90); setServoAngle(R3, 90);
setServoAngle(L2, 90); setServoAngle(R2, 90);
delayWithFace(200);
for(int i=0; i<5; i++) {
setServoAngle(R4, 45); setServoAngle(L4, 135);
delayWithFace(300);
setServoAngle(R4, 0); setServoAngle(L4, 180);
delayWithFace(300);
}
runStandPose(1);
if (currentCommand == "shake") currentCommand = "";
}
inline void runShrugPose() {
Serial.println(F("SHRUG"));
runStandPose(0);
setFaceWithMode("dead", FACE_ANIM_ONCE);
delayWithFace(200);
setServoAngle(R3, 90); setServoAngle(R4, 90); setServoAngle(L3, 90); setServoAngle(L4, 90);
delayWithFace(1000);
setFaceWithMode("shrug", FACE_ANIM_ONCE);
setServoAngle(R3, 0); setServoAngle(R4, 180); setServoAngle(L3, 180); setServoAngle(L4, 0);
delayWithFace(1500);
runStandPose(1);
if (currentCommand == "shrug") currentCommand = "";
}
inline void runDeadPose() {
Serial.println(F("DEAD"));
runStandPose(0);
setFaceWithMode("dead", FACE_ANIM_BOOMERANG);
delayWithFace(200);
setServoAngle(R3, 90); setServoAngle(R4, 90); setServoAngle(L3, 90); setServoAngle(L4, 90);
if (currentCommand == "dead") currentCommand = "";
}
inline void runCrabPose() {
Serial.println(F("CRAB"));
setFaceWithMode("crab", FACE_ANIM_ONCE);
runStandPose(0);
delayWithFace(200);
setServoAngle(R1, 90); setServoAngle(R2, 90); setServoAngle(L1, 90); setServoAngle(L2, 90);
setServoAngle(R4, 0); setServoAngle(R3, 180); setServoAngle(L3, 45); setServoAngle(L4, 135);
for(int i=0; i<5; i++) {
setServoAngle(R4, 45); setServoAngle(R3, 135); setServoAngle(L3, 0); setServoAngle(L4, 180);
delayWithFace(300);
setServoAngle(R4, 0); setServoAngle(R3, 180); setServoAngle(L3, 45); setServoAngle(L4, 135);
delayWithFace(300);
}
runStandPose(1);
if (currentCommand == "crab") currentCommand = "";
}
// --- MOVEMENT ANIMATIONS ---
inline void runWalkPose() {
Serial.println(F("WALK FWD"));
setFaceWithMode("walk", FACE_ANIM_ONCE);
// Initial Step
setServoAngle(R3, 135); setServoAngle(L3, 45);
setServoAngle(R2, 100); setServoAngle(L1, 25);
if (!pressingCheck("forward", frameDelay)) return;
for (int i = 0; i < walkCycles; i++) {
setServoAngle(R3, 135); setServoAngle(L3, 0);
if (!pressingCheck("forward", frameDelay)) return;
setServoAngle(L4, 135); setServoAngle(L2, 90);
setServoAngle(R4, 0); setServoAngle(R1, 180);
if (!pressingCheck("forward", frameDelay)) return;
setServoAngle(R2, 45); setServoAngle(L1, 90);
if (!pressingCheck("forward", frameDelay)) return;
setServoAngle(R4, 45); setServoAngle(L4, 180);
if (!pressingCheck("forward", frameDelay)) return;
setServoAngle(R3, 180); setServoAngle(L3, 45);
setServoAngle(R2, 90); setServoAngle(L1, 0);
if (!pressingCheck("forward", frameDelay)) return;
setServoAngle(L2, 135); setServoAngle(R1, 90);
if (!pressingCheck("forward", frameDelay)) return;
}
runStandPose(1);
}
// Logic reversed from Walk
inline void runWalkBackward() {
Serial.println(F("WALK BACK"));
setFaceWithMode("walk", FACE_ANIM_ONCE);
if (!pressingCheck("backward", frameDelay)) return;
for (int i = 0; i < walkCycles; i++) {
setServoAngle(R3, 135); setServoAngle(L3, 0);
if (!pressingCheck("backward", frameDelay)) return;
setServoAngle(L4, 135); setServoAngle(L2, 135);
setServoAngle(R4, 0); setServoAngle(R1, 90);
if (!pressingCheck("backward", frameDelay)) return;
setServoAngle(R2, 90); setServoAngle(L1, 0);
if (!pressingCheck("backward", frameDelay)) return;
setServoAngle(R4, 45); setServoAngle(L4, 180);
if (!pressingCheck("backward", frameDelay)) return;
setServoAngle(R3, 180); setServoAngle(L3, 45);
setServoAngle(R2, 45); setServoAngle(L1, 90);
if (!pressingCheck("backward", frameDelay)) return;
setServoAngle(L2, 90); setServoAngle(R1, 180);
if (!pressingCheck("backward", frameDelay)) return;
}
runStandPose(1);
}
// Simple turn logic
inline void runTurnLeft() {
Serial.println(F("TURN LEFT"));
setFaceWithMode("walk", FACE_ANIM_ONCE);
for (int i = 0; i < walkCycles; i++) {
//legset 1 (R1 L2)
setServoAngle(R3, 135); setServoAngle(L4, 135);
if (!pressingCheck("left", frameDelay)) return;
setServoAngle(R1, 180); setServoAngle(L2, 180);
if (!pressingCheck("left", frameDelay)) return;
setServoAngle(R3, 180); setServoAngle(L4, 180);
if (!pressingCheck("left", frameDelay)) return;
setServoAngle(R1, 135); setServoAngle(L2, 135);
if (!pressingCheck("left", frameDelay)) return;
//legset 2 (R2 L1)
setServoAngle(R4, 45); setServoAngle(L3, 45);
if (!pressingCheck("left", frameDelay)) return;
setServoAngle(R2, 90); setServoAngle(L1, 90);
if (!pressingCheck("left", frameDelay)) return;
setServoAngle(R4, 0); setServoAngle(L3, 0);
if (!pressingCheck("left", frameDelay)) return;
setServoAngle(R2, 45); setServoAngle(L1, 45);
if (!pressingCheck("left", frameDelay)) return;
}
runStandPose(1);
}
inline void runTurnRight() {
Serial.println(F("TURN RIGHT"));
setFaceWithMode("walk", FACE_ANIM_ONCE);
for (int i = 0; i < walkCycles; i++) {
//legset 2 (R2 L1)
setServoAngle(R4, 45); setServoAngle(L3, 45);
if (!pressingCheck("right", frameDelay)) return;
setServoAngle(R2, 0); setServoAngle(L1, 0);
if (!pressingCheck("right", frameDelay)) return;
setServoAngle(R4, 0); setServoAngle(L3, 0);
if (!pressingCheck("right", frameDelay)) return;
setServoAngle(R2, 45); setServoAngle(L1, 45);
if (!pressingCheck("right", frameDelay)) return;
//legset 1 (R1 L2)
setServoAngle(R3, 135); setServoAngle(L4, 135);
if (!pressingCheck("right", frameDelay)) return;
setServoAngle(R1, 90); setServoAngle(L2, 90);
if (!pressingCheck("right", frameDelay)) return;
setServoAngle(R3, 180); setServoAngle(L4, 180);
if (!pressingCheck("right", frameDelay)) return;
setServoAngle(R1, 135); setServoAngle(L2, 135);
if (!pressingCheck("right", frameDelay)) return;
}
runStandPose(1);
}
+857
View File
@@ -0,0 +1,857 @@
#include <WiFi.h>
#include <WebServer.h>
#include <DNSServer.h>
#include <ESPmDNS.h>
#include <Wire.h>
#include <ESP32Servo.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include "face-bitmaps.h"
#include "movement-sequences.h"
#include "captive-portal.h"
// --- Access Point Configuration ---
// This is the network the Robot will create
#define AP_SSID "Sesame-Controller"
#define AP_PASS "12345678" // Must be at least 8 characters
// --- Station Mode Configuration (Optional) ---
// Set these to connect to your home/office WiFi network
// Leave NETWORK_SSID empty to disable station mode
#define NETWORK_SSID "" // Your WiFi network name
#define NETWORK_PASS "" // Your WiFi password
#define ENABLE_NETWORK_MODE false // Set to true to enable network connection attempts
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define OLED_I2C_ADDR 0x3C
// I2C Pins for Distro Board V2 / V3
//#define I2C_SDA 8
//#define I2C_SCL 9
// I2C Pins for Distro Board V1
//#define I2C_SDA 21
//#define I2C_SCL 22
// I2C Pins for S2 Mini Board
#define I2C_SDA 33
#define I2C_SCL 35
// DNS Server for Captive Portal
DNSServer dnsServer;
const byte DNS_PORT = 53;
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
WebServer server(80);
// Global state for animations
String currentCommand = "";
String currentFaceName = "default";
const unsigned char* const* currentFaceFrames = nullptr;
uint8_t currentFaceFrameCount = 0;
uint8_t currentFaceFrameIndex = 0;
unsigned long lastFaceFrameMs = 0;
int faceFps = 8;
FaceAnimMode currentFaceMode = FACE_ANIM_LOOP;
int8_t faceFrameDirection = 1;
bool faceAnimFinished = false;
int currentFaceFps = 0;
bool idleActive = false;
bool idleBlinkActive = false;
unsigned long nextIdleBlinkMs = 0;
uint8_t idleBlinkRepeatsLeft = 0;
// WiFi Info Scrolling
unsigned long lastInputTime = 0;
bool firstInputReceived = false;
bool showingWifiInfo = false;
int wifiScrollPos = 0;
unsigned long lastWifiScrollMs = 0;
String wifiInfoText = "";
// Network Mode
bool networkConnected = false;
IPAddress networkIP;
String deviceHostname = "sesame-robot";
// Servo Pins for Distro Board
// ======================================================================
// Pin numbers are coorisponding to the ESP32 GPIO pins and may differ based on which board you use.
// If you are using a different board, please adjust the servoPins array accordingly.
// ======================================================================
Servo servos[8];
// Sesame Distro Board V3 Pinout [NEW]
//const int servoPins[8] = {4, 5, 6, 7, 10, 11, 12, 13};
// Sesame Distro Board V2 Pinout (Legacy)
//const int servoPins[8] = {4, 5, 6, 7, 15, 16, 17, 18};
// Sesame Distro Board V1 Pinout (Legacy)
//const int servoPins[8] = {15, 2, 23, 19, 4, 16, 17, 18};
// Lolin S2 Mini Pinout
const int servoPins[8] = {1, 2, 4, 6, 8, 10, 13, 14};
// Subtrim values for each servo (offset in degrees)
int8_t servoSubtrim[8] = {0, 0, 0, 0, 0, 0, 0, 0};
// Animation constants
int frameDelay = 100;
int walkCycles = 10;
int motorCurrentDelay = 20; // ms delay between motor movements to prevent over-current
struct FaceEntry {
const char* name;
const unsigned char* const* frames;
uint8_t maxFrames;
};
static const uint8_t MAX_FACE_FRAMES = 6;
#define MAKE_FACE_FRAMES(name) \
const unsigned char* const face_##name##_frames[] = { \
epd_bitmap_##name, epd_bitmap_##name##_1, epd_bitmap_##name##_2, \
epd_bitmap_##name##_3, epd_bitmap_##name##_4, epd_bitmap_##name##_5 \
};
#define X(name) MAKE_FACE_FRAMES(name)
FACE_LIST
#undef X
#undef MAKE_FACE_FRAMES
const FaceEntry faceEntries[] = {
#define X(name) { #name, face_##name##_frames, MAX_FACE_FRAMES },
FACE_LIST
#undef X
{ "default", face_defualt_frames, MAX_FACE_FRAMES }
};
struct FaceFpsEntry {
const char* name;
uint8_t fps;
};
const FaceFpsEntry faceFpsEntries[] = {
{ "walk", 1 },
{ "rest", 1 },
{ "swim", 1 },
{ "dance", 1 },
{ "wave", 1 },
{ "point", 5 },
{ "stand", 1 },
{ "cute", 1 },
{ "pushup", 1 },
{ "freaky", 1 },
{ "bow", 1 },
{ "worm", 1 },
{ "shake", 1 },
{ "shrug", 1 },
{ "dead", 2 },
{ "crab", 1 },
{ "idle", 1 },
{ "idle_blink", 7 },
{ "default", 1 },
// Conversational faces (manually controlled by Python - no auto-animation)
{ "happy", 1 },
{ "talk_happy", 1 },
{ "sad", 1 },
{ "talk_sad", 1 },
{ "angry", 1 },
{ "talk_angry", 1 },
{ "surprised", 1 },
{ "talk_surprised", 1 },
{ "sleepy", 1 },
{ "talk_sleepy", 1 },
{ "love", 1 },
{ "talk_love", 1 },
{ "excited", 1 },
{ "talk_excited", 1 },
{ "confused", 1 },
{ "talk_confused", 1 },
{ "thinking", 1 },
{ "talk_thinking", 1 },
};
// Prototypes
void setServoAngle(uint8_t channel, int angle);
void updateFaceBitmap(const unsigned char* bitmap);
void setFace(const String& faceName);
void setFaceMode(FaceAnimMode mode);
void setFaceWithMode(const String& faceName, FaceAnimMode mode);
void updateAnimatedFace();
void delayWithFace(unsigned long ms);
void enterIdle();
void exitIdle();
void updateIdleBlink();
int getFaceFpsForName(const String& faceName);
bool pressingCheck(String cmd, int ms);
void handleGetSettings();
void handleSetSettings();
void handleGetStatus();
void handleApiCommand();
void updateWifiInfoScroll();
void recordInput();
void handleRoot() {
server.send(200, "text/html", index_html);
}
void handleCommandWeb() {
// We send 200 OK immediately so the web browser doesn't hang waiting for animation to finish
if (server.hasArg("pose")) {
currentCommand = server.arg("pose");
recordInput();
exitIdle();
server.send(200, "text/plain", "OK");
}
else if (server.hasArg("go")) {
currentCommand = server.arg("go");
recordInput();
exitIdle();
server.send(200, "text/plain", "OK");
}
else if (server.hasArg("stop")) {
currentCommand = "";
recordInput();
server.send(200, "text/plain", "OK");
}
else if (server.hasArg("motor") && server.hasArg("value")) {
int motorNum = server.arg("motor").toInt();
int servoIdx = servoNameToIndex(server.arg("motor"));
int angle = server.arg("value").toInt();
if (motorNum >= 1 && motorNum <= 8 && angle >= 0 && angle <= 180) {
setServoAngle(motorNum - 1, angle); // Convert 1-based to 0-based index
recordInput();
server.send(200, "text/plain", "OK");
} else if (servoIdx != -1 && angle >= 0 && angle <= 180) {
setServoAngle(servoIdx, angle);
recordInput();
server.send(200, "text/plain", "OK");
} else {
server.send(400, "text/plain", "Invalid motor or angle");
}
}
else {
server.send(400, "text/plain", "Bad Args");
}
}
void handleGetSettings() {
String json = "{";
json += "\"frameDelay\":" + String(frameDelay) + ",";
json += "\"walkCycles\":" + String(walkCycles) + ",";
json += "\"motorCurrentDelay\":" + String(motorCurrentDelay) + ",";
json += "\"faceFps\":" + String(faceFps);
json += "}";
server.send(200, "application/json", json);
}
void handleSetSettings() {
if (server.hasArg("frameDelay")) frameDelay = server.arg("frameDelay").toInt();
if (server.hasArg("walkCycles")) walkCycles = server.arg("walkCycles").toInt();
if (server.hasArg("motorCurrentDelay")) motorCurrentDelay = server.arg("motorCurrentDelay").toInt();
if (server.hasArg("faceFps")) faceFps = (int)max(1L, server.arg("faceFps").toInt());
server.send(200, "text/plain", "OK");
}
// API endpoint for network clients to get robot status
void handleGetStatus() {
String json = "{";
json += "\"currentCommand\":\"" + currentCommand + "\",";
json += "\"currentFace\":\"" + currentFaceName + "\",";
json += "\"networkConnected\":" + String(networkConnected ? "true" : "false") + ",";
json += "\"apIP\":\"" + WiFi.softAPIP().toString() + "\"";
if (networkConnected) {
json += ",\"networkIP\":\"" + networkIP.toString() + "\"";
}
json += "}";
server.send(200, "application/json", json);
}
// API endpoint for network clients to send commands (JSON-based)
void handleApiCommand() {
if (server.method() != HTTP_POST) {
server.send(405, "application/json", "{\"error\":\"Method not allowed\"}");
return;
}
String body = server.arg("plain");
Serial.println("API Command received:");
Serial.println(body);
// Check for face-only command (no movement)
int faceOnlyStart = body.indexOf("\"face\":\"");
if (faceOnlyStart == -1) {
faceOnlyStart = body.indexOf("\"face\": \"");
}
// If we have a face but no command field, it's face-only
bool faceOnly = (faceOnlyStart > 0 && body.indexOf("\"command\":") == -1 && body.indexOf("\"command\": ") == -1);
String command = "";
String face = "";
// Parse face
if (faceOnlyStart > 0) {
faceOnlyStart = body.indexOf("\"", faceOnlyStart + 6) + 1;
int faceEnd = body.indexOf("\"", faceOnlyStart);
if (faceEnd > faceOnlyStart) {
face = body.substring(faceOnlyStart, faceEnd);
Serial.print("Parsed face: ");
Serial.println(face);
}
}
// Parse command (if not face-only)
if (!faceOnly) {
int cmdStart = body.indexOf("\"command\":\"");
if (cmdStart == -1) {
cmdStart = body.indexOf("\"command\": \"");
}
if (cmdStart == -1) {
Serial.println("Error: command field not found");
server.send(400, "application/json", "{\"error\":\"Missing command field\"}");
return;
}
cmdStart = body.indexOf("\"", cmdStart + 10) + 1;
int cmdEnd = body.indexOf("\"", cmdStart);
if (cmdEnd <= cmdStart) {
Serial.println("Error: invalid command format");
server.send(400, "application/json", "{\"error\":\"Invalid command format\"}");
return;
}
command = body.substring(cmdStart, cmdEnd);
Serial.print("Parsed command: ");
Serial.println(command);
}
// Set face if provided
if (face.length() > 0) {
setFace(face);
}
// If face-only, just acknowledge
if (faceOnly) {
recordInput();
server.send(200, "application/json", "{\"status\":\"ok\",\"message\":\"Face updated\"}");
return;
}
// Execute command
if (command == "stop") {
currentCommand = "";
recordInput();
server.send(200, "application/json", "{\"status\":\"ok\",\"message\":\"Command stopped\"}");
} else {
currentCommand = command;
recordInput();
exitIdle();
server.send(200, "application/json", "{\"status\":\"ok\",\"message\":\"Command executed\"}");
}
}
void setup() {
Serial.begin(115200);
randomSeed(micros());
// I2C Init for ESP32
Wire.begin(I2C_SDA, I2C_SCL);
// OLED Init
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_I2C_ADDR)) {
Serial.println(F("SSD1306 allocation failed."));
while (1);
}
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0,0);
display.println(F("Setting up WiFi..."));
display.display();
// --- WIFI CONFIGURATION ---
// Try to connect to network first if configured
if (ENABLE_NETWORK_MODE && String(NETWORK_SSID).length() > 0) {
Serial.println("Attempting to connect to network: " + String(NETWORK_SSID));
WiFi.mode(WIFI_AP_STA); // Enable both AP and Station modes
WiFi.setHostname(deviceHostname.c_str());
WiFi.begin(NETWORK_SSID, NETWORK_PASS);
// Wait up to 10 seconds for connection
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
networkConnected = true;
networkIP = WiFi.localIP();
Serial.println();
Serial.print("Connected to network! IP: ");
Serial.println(networkIP);
} else {
Serial.println();
Serial.println("Failed to connect to network. Running in AP-only mode.");
WiFi.mode(WIFI_AP); // Fall back to AP-only
}
} else {
WiFi.mode(WIFI_AP);
Serial.println("Network mode disabled. Running in AP-only mode.");
}
// --- ACCESS POINT CONFIGURATION ---
WiFi.softAP(AP_SSID, AP_PASS);
IPAddress myIP = WiFi.softAPIP();
Serial.print("AP Created. IP: ");
Serial.println(myIP);
// Build WiFi info text for scrolling
if (networkConnected) {
wifiInfoText = "AP: " + String(AP_SSID) + " (" + myIP.toString() + ") | Network: " + String(NETWORK_SSID) + " (" + networkIP.toString() + ") or " + deviceHostname + ".local | ";
} else {
wifiInfoText = "Connect to WiFi: " + String(AP_SSID) + " | Pass: " + String(AP_PASS) + " | IP: " + myIP.toString() + " | Captive Portal will auto-open! | ";
}
// Initialize input tracking
lastInputTime = millis();
firstInputReceived = false;
showingWifiInfo = false;
// Start mDNS responder for local network discovery
if (MDNS.begin(deviceHostname.c_str())) {
Serial.println("mDNS responder started");
Serial.print("Access controller at: http://");
Serial.print(deviceHostname);
Serial.println(".local");
MDNS.addService("http", "tcp", 80);
} else {
Serial.println("Error setting up mDNS responder!");
}
// Start DNS Server for Captive Portal
// This redirects ALL domain requests to the ESP32's IP
dnsServer.start(DNS_PORT, "*", myIP);
// Web Server Routes
server.on("/", handleRoot);
server.on("/cmd", handleCommandWeb);
server.on("/getSettings", handleGetSettings);
server.on("/setSettings", handleSetSettings);
// API endpoints for network communication
server.on("/api/status", handleGetStatus);
server.on("/api/command", handleApiCommand);
// Catch-all route for captive portal
// This ensures any URL redirects to the controller page
server.onNotFound(handleRoot);
server.begin();
// PWM Init
ESP32PWM::allocateTimer(0);
ESP32PWM::allocateTimer(1);
ESP32PWM::allocateTimer(2);
ESP32PWM::allocateTimer(3);
for (int i = 0; i < 8; i++) {
servos[i].setPeriodHertz(50);
// Map 0-180 to approx 732-2929us
servos[i].attach(servoPins[i], 732, 2929);
}
delay(10);
// Show rest face on startup without moving motors
setFace("rest");
Serial.println(F("HTTP server & Captive Portal started."));
}
void loop() {
// Process DNS requests for captive portal
dnsServer.processNextRequest();
server.handleClient();
updateAnimatedFace();
updateIdleBlink();
updateWifiInfoScroll();
if (currentCommand != "") {
String cmd = currentCommand;
if (cmd == "forward") runWalkPose();
else if (cmd == "backward") runWalkBackward();
else if (cmd == "left") runTurnLeft();
else if (cmd == "right") runTurnRight();
else if (cmd == "rest") { runRestPose(); if (currentCommand == "rest") currentCommand = ""; }
else if (cmd == "stand") { runStandPose(1); if (currentCommand == "stand") currentCommand = ""; }
else if (cmd == "wave") runWavePose();
else if (cmd == "dance") runDancePose();
else if (cmd == "swim") runSwimPose();
else if (cmd == "point") runPointPose();
else if (cmd == "pushup") runPushupPose();
else if (cmd == "bow") runBowPose();
else if (cmd == "cute") runCutePose();
else if (cmd == "freaky") runFreakyPose();
else if (cmd == "worm") runWormPose();
else if (cmd == "shake") runShakePose();
else if (cmd == "shrug") runShrugPose();
else if (cmd == "dead") runDeadPose();
else if (cmd == "crab") runCrabPose();
}
// Serial CLI for debugging (can be used to diagnose servo position issues and wiring)
if (Serial.available()) {
static char command_buffer[32];
static byte buffer_pos = 0;
char c = Serial.read();
if (c == '\n' || c == '\r') {
if (buffer_pos > 0) {
command_buffer[buffer_pos] = '\0';
int motorNum, angle;
recordInput();
if(strcmp(command_buffer, "run walk") == 0 || strcmp(command_buffer, "rn wf") == 0) { currentCommand = "forward"; runWalkPose(); currentCommand = ""; }
else if(strcmp(command_buffer, "rn wb") == 0) { currentCommand = "backward"; runWalkBackward(); currentCommand = ""; }
else if(strcmp(command_buffer, "rn tl") == 0) { currentCommand = "left"; runTurnLeft(); currentCommand = ""; }
else if(strcmp(command_buffer, "rn tr") == 0) { currentCommand = "right"; runTurnRight(); currentCommand = ""; }
else if(strcmp(command_buffer, "run rest") == 0 || strcmp(command_buffer, "rn rs") == 0) runRestPose();
else if(strcmp(command_buffer, "run stand") == 0 || strcmp(command_buffer, "rn st") == 0) runStandPose(1);
else if(strcmp(command_buffer, "rn wv") == 0) { currentCommand = "wave"; runWavePose(); }
else if(strcmp(command_buffer, "rn dn") == 0) { currentCommand = "dance"; runDancePose(); }
else if(strcmp(command_buffer, "rn sw") == 0) { currentCommand = "swim"; runSwimPose(); }
else if(strcmp(command_buffer, "rn pt") == 0) { currentCommand = "point"; runPointPose(); }
else if(strcmp(command_buffer, "rn pu") == 0) { currentCommand = "pushup"; runPushupPose(); }
else if(strcmp(command_buffer, "rn bw") == 0) { currentCommand = "bow"; runBowPose(); }
else if(strcmp(command_buffer, "rn ct") == 0) { currentCommand = "cute"; runCutePose(); }
else if(strcmp(command_buffer, "rn fk") == 0) { currentCommand = "freaky"; runFreakyPose(); }
else if(strcmp(command_buffer, "rn wm") == 0) { currentCommand = "worm"; runWormPose(); }
else if(strcmp(command_buffer, "rn sk") == 0) { currentCommand = "shake"; runShakePose(); }
else if(strcmp(command_buffer, "rn sg") == 0) { currentCommand = "shrug"; runShrugPose(); }
else if(strcmp(command_buffer, "rn dd") == 0) { currentCommand = "dead"; runDeadPose(); }
else if(strcmp(command_buffer, "rn cb") == 0) { currentCommand = "crab"; runCrabPose(); }
else if (strcmp(command_buffer, "subtrim") == 0 || strcmp(command_buffer, "st") == 0) {
Serial.println("Subtrim values:");
for (int i = 0; i < 8; i++) {
Serial.print("Motor "); Serial.print(i); Serial.print(": ");
if (servoSubtrim[i] >= 0) Serial.print("+");
Serial.println(servoSubtrim[i]);
}
}
else if (strcmp(command_buffer, "subtrim save") == 0 || strcmp(command_buffer, "st save") == 0) {
Serial.println("Copy and paste this into your code:");
Serial.print("int8_t servoSubtrim[8] = {");
for (int i = 0; i < 8; i++) {
Serial.print(servoSubtrim[i]);
if (i < 7) Serial.print(", ");
}
Serial.println("};");
}
else if (strncmp(command_buffer, "subtrim reset", 13) == 0 || strncmp(command_buffer, "st reset", 8) == 0) {
for (int i = 0; i < 8; i++) servoSubtrim[i] = 0;
Serial.println("All subtrim values reset to 0");
}
else if (strncmp(command_buffer, "subtrim ", 8) == 0 || strncmp(command_buffer, "st ", 3) == 0) {
const char* params = (command_buffer[1] == 't') ? command_buffer + 3 : command_buffer + 8;
int trimMotor, trimValue;
if (sscanf(params, "%d %d", &trimMotor, &trimValue) == 2) {
if (trimMotor >= 0 && trimMotor < 8) {
if (trimValue >= -90 && trimValue <= 90) {
servoSubtrim[trimMotor] = trimValue;
Serial.print("Motor "); Serial.print(trimMotor); Serial.print(" subtrim set to ");
if (trimValue >= 0) Serial.print("+");
Serial.println(trimValue);
} else {
Serial.println("Subtrim value must be between -90 and +90");
}
} else {
Serial.println("Invalid motor number (0-7)");
}
}
}
else if (strncmp(command_buffer, "all ", 4) == 0) {
if (sscanf(command_buffer + 4, "%d", &angle) == 1) {
for (int i = 0; i < 8; i++) setServoAngle(i, angle);
Serial.print("All servos set to "); Serial.println(angle);
}
}
else if (sscanf(command_buffer, "%d %d", &motorNum, &angle) == 2) {
if (motorNum >= 0 && motorNum < 8) {
setServoAngle(motorNum, angle);
Serial.print("Servo "); Serial.print(motorNum); Serial.print(" set to "); Serial.println(angle);
} else {
Serial.println("Invalid motor number (0-7)");
}
}
buffer_pos = 0;
}
} else if (buffer_pos < sizeof(command_buffer) - 1) {
command_buffer[buffer_pos++] = c;
}
}
}
// Function to update the robot's face
void updateFaceBitmap(const unsigned char* bitmap) {
display.clearDisplay();
display.drawBitmap(0, 0, bitmap, 128, 64, SSD1306_WHITE);
display.display();
}
uint8_t countFrames(const unsigned char* const* frames, uint8_t maxFrames) {
if (frames == nullptr || frames[0] == nullptr) return 0;
uint8_t count = 0;
for (uint8_t i = 0; i < maxFrames; i++) {
if (frames[i] == nullptr) break;
count++;
}
return count;
}
void setFace(const String& faceName) {
if (faceName == currentFaceName && currentFaceFrames != nullptr) return;
currentFaceName = faceName;
currentFaceFrameIndex = 0;
lastFaceFrameMs = 0;
faceFrameDirection = 1;
faceAnimFinished = false;
currentFaceFps = getFaceFpsForName(faceName);
currentFaceFrames = face_defualt_frames;
currentFaceFrameCount = countFrames(face_defualt_frames, MAX_FACE_FRAMES);
for (size_t i = 0; i < (sizeof(faceEntries) / sizeof(faceEntries[0])); i++) {
if (faceName.equalsIgnoreCase(faceEntries[i].name)) {
currentFaceFrames = faceEntries[i].frames;
currentFaceFrameCount = countFrames(faceEntries[i].frames, faceEntries[i].maxFrames);
break;
}
}
if (currentFaceFrameCount == 0) {
currentFaceFrames = face_defualt_frames;
currentFaceFrameCount = countFrames(face_defualt_frames, MAX_FACE_FRAMES);
currentFaceName = "default";
currentFaceFps = getFaceFpsForName(currentFaceName);
}
if (currentFaceFrameCount > 0 && currentFaceFrames[0] != nullptr) {
updateFaceBitmap(currentFaceFrames[0]);
}
}
void setFaceMode(FaceAnimMode mode) {
currentFaceMode = mode;
faceFrameDirection = 1;
faceAnimFinished = false;
}
void setFaceWithMode(const String& faceName, FaceAnimMode mode) {
setFaceMode(mode);
setFace(faceName);
}
int getFaceFpsForName(const String& faceName) {
for (size_t i = 0; i < (sizeof(faceFpsEntries) / sizeof(faceFpsEntries[0])); i++) {
if (faceName.equalsIgnoreCase(faceFpsEntries[i].name)) {
return faceFpsEntries[i].fps;
}
}
return faceFps;
}
void updateAnimatedFace() {
if (currentFaceFrames == nullptr || currentFaceFrameCount <= 1) return;
if (currentFaceMode == FACE_ANIM_ONCE && faceAnimFinished) return;
unsigned long now = millis();
int fps = max(1, (currentFaceFps > 0 ? currentFaceFps : faceFps));
unsigned long interval = 1000UL / fps;
if (now - lastFaceFrameMs >= interval) {
lastFaceFrameMs = now;
if (currentFaceMode == FACE_ANIM_LOOP) {
currentFaceFrameIndex = (currentFaceFrameIndex + 1) % currentFaceFrameCount;
} else if (currentFaceMode == FACE_ANIM_ONCE) {
if (currentFaceFrameIndex + 1 >= currentFaceFrameCount) {
currentFaceFrameIndex = currentFaceFrameCount - 1;
faceAnimFinished = true;
} else {
currentFaceFrameIndex++;
}
} else {
if (faceFrameDirection > 0) {
if (currentFaceFrameIndex + 1 >= currentFaceFrameCount) {
faceFrameDirection = -1;
if (currentFaceFrameIndex > 0) currentFaceFrameIndex--;
} else {
currentFaceFrameIndex++;
}
} else {
if (currentFaceFrameIndex == 0) {
faceFrameDirection = 1;
if (currentFaceFrameCount > 1) currentFaceFrameIndex++;
} else {
currentFaceFrameIndex--;
}
}
}
updateFaceBitmap(currentFaceFrames[currentFaceFrameIndex]);
}
}
void delayWithFace(unsigned long ms) {
unsigned long start = millis();
while (millis() - start < ms) {
updateAnimatedFace();
server.handleClient();
dnsServer.processNextRequest();
delay(5);
}
}
void scheduleNextIdleBlink(unsigned long minMs, unsigned long maxMs) {
unsigned long now = millis();
unsigned long interval = (unsigned long)random(minMs, maxMs);
nextIdleBlinkMs = now + interval;
}
void enterIdle() {
idleActive = true;
idleBlinkActive = false;
idleBlinkRepeatsLeft = 0;
setFaceWithMode("idle", FACE_ANIM_BOOMERANG);
scheduleNextIdleBlink(3000, 7000);
}
void exitIdle() {
idleActive = false;
idleBlinkActive = false;
}
void updateIdleBlink() {
if (!idleActive) return;
if (!idleBlinkActive) {
if (millis() >= nextIdleBlinkMs) {
idleBlinkActive = true;
if (idleBlinkRepeatsLeft == 0 && random(0, 100) < 30) {
idleBlinkRepeatsLeft = 1; // double blink
}
setFaceWithMode("idle_blink", FACE_ANIM_ONCE);
}
return;
}
if (currentFaceMode == FACE_ANIM_ONCE && faceAnimFinished) {
idleBlinkActive = false;
setFaceWithMode("idle", FACE_ANIM_BOOMERANG);
if (idleBlinkRepeatsLeft > 0) {
idleBlinkRepeatsLeft--;
scheduleNextIdleBlink(120, 220);
} else {
scheduleNextIdleBlink(3000, 7000);
}
}
}
// ====== HELPERS ======
void setServoAngle(uint8_t channel, int angle) {
if (channel < 8) {
int adjustedAngle = constrain(angle + servoSubtrim[channel], 0, 180);
servos[channel].write(adjustedAngle);
delayWithFace(motorCurrentDelay);
}
}
bool pressingCheck(String cmd, int ms) {
unsigned long start = millis();
while (millis() - start < ms) {
server.handleClient();
dnsServer.processNextRequest();
updateAnimatedFace();
if (currentCommand != cmd) {
runStandPose(1);
return false;
}
yield();
}
return true;
}
void recordInput() {
lastInputTime = millis();
if (!firstInputReceived) {
firstInputReceived = true;
showingWifiInfo = false;
}
}
void updateWifiInfoScroll() {
// Don't show WiFi info if first input has been received
if (firstInputReceived) {
if (showingWifiInfo) {
showingWifiInfo = false;
// Restore the current face
if (currentFaceFrames != nullptr && currentFaceFrameCount > 0) {
updateFaceBitmap(currentFaceFrames[currentFaceFrameIndex]);
}
}
return;
}
unsigned long now = millis();
// Check if 30 seconds have passed without input
if (!showingWifiInfo && (now - lastInputTime >= 30000)) {
showingWifiInfo = true;
wifiScrollPos = 0;
lastWifiScrollMs = now;
}
if (!showingWifiInfo) return;
// Update scroll every 150ms
if (now - lastWifiScrollMs >= 150) {
lastWifiScrollMs = now;
// Clear and redraw with current face in background
display.clearDisplay();
// Draw the face bitmap in the background
if (currentFaceFrames != nullptr && currentFaceFrameCount > 0) {
display.drawBitmap(0, 0, currentFaceFrames[currentFaceFrameIndex], 128, 64, SSD1306_WHITE);
}
// Draw black bar for text background on top row
display.fillRect(0, 0, 128, 10, SSD1306_BLACK);
// Draw scrolling text
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setTextWrap(false);
display.setCursor(-wifiScrollPos, 1);
display.print(wifiInfoText);
display.setTextWrap(true);
display.display();
// Advance scroll position
wifiScrollPos += 2;
if (wifiScrollPos >= (int)(wifiInfoText.length() * 6)) {
wifiScrollPos = 0;
}
}
}
+36
View File
@@ -0,0 +1,36 @@
# Hardware
All mechanical and electrical source files for the Sesame Robot live here. The hardware stack supports multiple build paths:
- **Hand-wired / Lolin S2 Mini build** for simple protoboard construction (recommended for DIY builds).
- **Custom Sesame Distro Board V3 build** (Current) for a professional SMD design with USB-C PD 12V and specific connector for Bambu Lab battery (included in new Sesame Build Kits, pre-flashed).
- **Custom Sesame Distro Board V3/V2 build** (Legacy) professional SMD design but plagued with brownouts on battery. Recommended for USB only or bypassing the buck converter.
- **Custom Sesame Distro Board V1 build** (legacy) for ESP32-DevKitC-32E stackup (phased out but still supported).
Use the sections below to jump to the files that match the version you are assembling.
## Directory Guide
| Folder | What you will find |
| --- | --- |
| [bom](bom/README.md) | Full bill of materials covering both wiring approaches, plus power budget notes and links back to the build tutorial. Start here to gather every component before printing or soldering. |
| [cad](cad/README.md) | Parametric STEP and Fusion 360 source models for every printed part. Great for remixing joint geometry or adapting the shell—just note the caution about features that may not translate across CAD packages. |
| [pcb](pcb/README.md) | Sesame Distro Board V3 (current), V2 (legacy), and V1 (legacy) schematics, layout files, Gerber files, BOM, and Pick-and-Place files. Includes ordering instructions for PCBway fabrication and assembly services. |
| [printing](printing/README.md) | Practical PLA print settings, support callouts, and image references for joint orientation and the top covers manual supports. |
## Getting Started
1. **Decide on a wiring strategy.** Review the comparison section in [docs/wiring-guide/README.md](../docs/wiring-guide/README.md) to choose between the protoboard and custom PCB routes. The Lolin S2 Mini build is recommended for DIY builders. If you have a Sesame Build Kit, your V3 (or early V2) Distro Board is already pre-flashed and ready to use.
2. **Print the shell and joints.** Follow the presets in [printing/README.md](printing/README.md) to prep the 3D files from `hardware/printing/stl`.
3. **Source electronics.** Use the line-by-line list in [bom/README.md](bom/README.md) and verify your power supply meets the 5V/3A requirement.
4. **Assemble electronics.** Reference either the hand-wiring steps or the appropriate Distro Board guide plus the wiring diagrams in `docs/wiring-guide/`.
> [!TIP]
> Keep photos of your wiring progress. They are invaluable for troubleshooting later and help when sharing build notes with the community.
## Supporting Documents
- [docs/build-guide/README.md](../docs/build-guide/README.md) for end-to-end assembly sequencing.
- [docs/wiring-guide/README.md](../docs/wiring-guide/README.md) for wiring diagrams, safety notes, and packing tips.
Improvements, remixes, and test reports are welcome, file issues or PRs with any updates you discover while building. <3
+93
View File
@@ -0,0 +1,93 @@
# Bill of Materials
Every part required to assemble Sesame is cataloged here. Pick the wiring strategy that fits your parts bin, then follow the build flow in [docs/build-guide/README.md](../../docs/build-guide/README.md).
> [!NOTE]
> Amazon links below point to representative search results so you can choose local suppliers or equivalent listings. Pricing and availability change frequently. If you don't mind waiting shipping times you can also order direct from manufacturer for much lower rates.
## Core Electronics (Both Builds)
| Item | Qty | Notes | Source |
| --- | --- | --- | --- |
| MG90S all-metal micro servos (180 Deg) | 8 (buy 10 for spares) | Primary hip/leg actuators; includes servo horns but keep extras | [Amazon](https://www.amazon.com/s?k=mg90s+metal+gear+servo+pack+of+8) |
| 0.96" SSD1306 I2C OLED | 1 | 128x64 display that slides into the top cover slot | [Amazon](https://www.amazon.com/s?k=0.96%22+I2C+OLED+SSD1306) |
| USB-C data/power cable | 1 | Needs to carry 5V/3A for flashing and tethered mode | [Amazon](https://www.amazon.com/s?k=usb+c+cable+60w) |
| Rocker power switch (KCD1, panel mount) | 1 | Snaps into the top cover cutout | [Amazon](https://www.amazon.com/s?k=KCD1+mini+rocker+switch+2+pin) |
| 22AWG silicone wire kit | 1 | Power/ground bus lines | [Amazon](https://www.amazon.com/s?k=22awg+silicone+wire+kit) |
| 30AWG silicone wire kit | 1 | Signal leads and dense harnessing | [Amazon](https://www.amazon.com/s?k=30awg+silicone+wire) |
| Heat-shrink assortment | 1 | Insulate OLED, switch, and battery joints | [Amazon](https://www.amazon.com/s?k=heat+shrink+tubing+kit) |
| Small zip ties | 1 pack | Bundling wires inside the frame | [Amazon](https://www.amazon.com/s?k=mini+zip+ties) |
## Wiring Option A S2 Mini / Hand-Wired Harness
| Item | Qty | Notes | Source |
| --- | --- | --- | --- |
| Lolin/WeMos ESP32-S2 Mini | 1 | Native USB-C, fits on perfboard for the hand-wired build | [Amazon](https://www.amazon.com/s?k=esp32+s2+mini) |
| Small protoboard (approx. 5×7 cm) | 1 | Hosts the header matrix and rails | [Amazon](https://www.amazon.com/s?k=prototype+perfboard) |
| 3-pin male headers | 8 | Build the servo breakout; match spacing to MG90 plugs | [Amazon](https://www.amazon.com/s?k=pin+header+strip) |
| Buck converter (512 V in to stable 5V/3A out) | 1 | Powers motors + MCU when using batteries | [Amazon](https://www.amazon.com/s?k=3a+dc+dc+buck+converter+module) |
## Wiring Option B Sesame Distro Board V3/V2 (Included in Build Kits)
> [!NOTE]
> If you purchased a Sesame Build Kit, your V2 Distro Board is already assembled, pre-flashed, and included. You don't need to order these parts separately.
| Item | Qty | Notes | Source |
| --- | --- | --- | --- |
| Sesame Distro Board V3/V2 PCB | 1 | Fully SMD design. Order with PCBway assembly service or attempt advanced hand soldering. See [ordering guide](/hardware/pcb/README.md) | [GitHub](/hardware/pcb/README.md) |
## Wiring Option C Sesame Distro Board V1 / ESP32-DevKitC-32E (Legacy)
> [!CAUTION]
> V1 is now phased out but still supported. Only choose this if you already have a V1 board.
| Item | Qty | Notes | Source |
| --- | --- | --- | --- |
| ESP32-DevKitC-32E (ESP32-WROOM-32) | 1 | Base board the Distro Board V1 stacks on. This one is very tricky because its a very specific board. You can use the 32E with the floating pcb antenna OR you can use the 32U but you have to route an antenna inside. | [Amazon](https://www.amazon.com/s?k=ESP32+DevKitC+32) |
| Sesame Distro Board V1 PCB | 1 | Order `Gerber_Sesame-Distro-Board_PCB_Sesame-Distro-Board_V1.zip` via PCBway | [GitHub](/hardware/pcb/README.md) |
| 5V buck converter (same spec as above) | 1 | Mounts on the distro board pads | [Amazon](https://www.amazon.com/s?k=3a+dc+dc+buck+converter+module) |
| 1000 µF electrolytic capacitor | 1 | Smooths output voltage on buck converter; 10V+ rating recommended | [Amazon](https://www.amazon.com/s?k=1000uf+electrolytic+capacitor) |
| 4-pin JST-XH or PH header | 1 | Optional external connector footprint on PCB | [Amazon](https://www.amazon.com/s?k=jst+xh+4+pin+kit) |
| 2-pin screw terminal (2.54 mm pitch) | 1 | Optional battery input on PCB | [Amazon](https://www.amazon.com/s?k=2+pin+screw+terminal+block+2.54mm+pitch) |
| M2.5 × 5 mm male-female standoffs | 4 | Elevate the PCB over the DevKit mounting holes | [Amazon](https://www.amazon.com/s?k=m2.5+male+female+standoff+5mm) |
## Power Sources & Connectors
| Item | Qty | Notes | Source |
| --- | --- | --- | --- |
| Bambu Lab 14500 7.4V 800mAh Li-ion Battery | 1 | Recommended wireless pack; cheap, effective, designed to fit inside the new internal frame. | [Bambu Lab](https://us.store.bambulab.com/products/14500-7-4v-800mah-li-ion-battery-1pcs) |
| Bambu Lab 7.4V Lithium battery charger | 1 | Matching charger for the 14500 battery with XH2.54 connector. | [Bambu Lab](https://us.store.bambulab.com/products/7-4v-lithium-battery-charger-with-xh2-54-connector-1pcs?id=593290727051776002) |
| XH2.54 female pigtail | 1 | Interface battery to switch/PCB without cutting stock leads (V3 requires soldering). | [Amazon](https://www.amazon.com/s?k=xh2.54+pigtail+cable) |
## Fasteners & Mechanical Hardware
| Item | Qty | Usage | Amazon |
| --- | --- | --- | --- |
| M2 × 5 mm self-threading screws | ~40 | All plastic joints, OLED retention, motor mounts, and covers (Can just get a variety pack) | [Amazon](https://www.amazon.com/s?k=m2+self+tapping+screws+kit) |
| M2.5 × 5mm machine screws | 10 | Servo horn attachment to servo shafts only. Included servo horn screws are usually too short. | [Amazon](https://www.amazon.com/s?k=m2+machine+screw+kit) |
## 3D Printed Parts
Print the 11-part part set outlined in [printing/README.md](../printing/README.md). STL and CAD sources live under `hardware/printing/`.
## Consumables & Tools Checklist
| Item | Notes | Source |
| --- | --- | --- |
| Leaded solder (0.60.8 mm) | Easier flow for dense perfboard work | [Amazon](https://www.amazon.com/s?k=63%2F37+solder+0.8mm) |
| Flux pen | Protects pads on the perfboard and PCB | [Amazon](https://www.amazon.com/s?k=flux+pen) |
| Solder wick / pump | For rework on the OLED pins | [Amazon](https://www.amazon.com/s?k=solder+wick) |
| Small flush cutters | Trim servo leads, perfboard traces, or supports | [Amazon](https://www.amazon.com/s?k=flush+cutters) |
| Precision screwdriver set | Needed for self-tapping M2 hardware | [Amazon](https://www.amazon.com/s?k=precision+screwdriver+set) |
## Power & Safety Notes
- Sesame needs at least 5 V at 3 A available at the rails.
- **Lolin S2 Mini:** Can be powered via USB-C PD (5V/3A capable) for tethered operation, or via battery + buck converter.
- **Distro Board V3/V2:** Supports both USB-C PD (5V/3A) for tethered operation AND battery + buck converter. Included in all Sesame Build Kits.
- **Distro Board V1 (Legacy):** Cannot run on tethered USB-C power due to design limitations. Must use battery + buck converter for operation.
- When battery powering either build, route the pack through the rocker switch and buck converter before it touches the rails, mirroring the schematic in [docs/wiring-guide/README.md](../../docs/wiring-guide/README.md).
- **Never cut the factory battery connector off the pack.** Instead, create adapter pigtails using XT30 or JST RCY leads so the pack remains chargeable.
- A Bambu Lab 14500 7.4V 800mAh Li-ion battery fits the new stock battery cavity (V3 requires printing the new internal frame).
- **Always apply heat shrink tubing to connectors**. Be extremely careful when cutting and soldering battery connectors. If you are still using the legacy 10440 solution make sure the cells are removed during soldering.
+21
View File
@@ -0,0 +1,21 @@
# CAD Files
3D CAD models for the Sesame Robot Project. Files are formatted in STEP format and F360 format.
Looking for STLs for 3D printing? See [printing](../printing/README.md).
>[!CAUTION]
> Parametric drawings and tools made in the original design files may not properly translate to your system. You may need to re-design certain components if you are making significant size changes.
## Editing the CAD with Free Tools
You do not need a paid CAD suite to remix Sesames parts. A few practical workflows:
1. **Fusion 360 personal license** Autodesk offers a free hobbyist tier that can open the native `.f3d` files. Enable the *Design History* timeline to see how assemblies were built before changing dimensions.
2. **FreeCAD for STEP tweaks** All parts are exported as `.step`, so you can import them into FreeCAD, adjust sketches, and re-export STL/STEP files without losing references. Use the *Part Design* workbench for parametric edits.
3. **Onshape education account** Onshapes free plan lets you upload the STEP files and edit directly in-browser. Great for Chromebook users or when collaborating in real time.
4. **Meshmixer/Blender for quick mods** If you just need to shave openings or add text, import the STL into Meshmixer or Blender, make the change, and re-export. This is faster for cosmetic tweaks, but stick with the parametric source for anything dimension-critical.
> [!TIP]
> Keep the original STEP/ Fusion file untouched and branch your edits into a copy. That way you can always rebase onto the upstream design if new hardware revisions ship.
Binary file not shown.
File diff suppressed because it is too large Load Diff
+153
View File
@@ -0,0 +1,153 @@
# PCB Schematics
Electronic schematics and PCB designs for the Sesame Robot Project. There are two versions of the Sesame Distro Board available: V3 (current), V2 (legacy), and V1 (legacy).
> [!NOTE]
> **For Sesame Build Kits:** All Sesame Build Kits include a pre-flashed Sesame Distro Board V3 (or earlier V2), so you don't need to order or assemble a board separately.
> [!TIP]
> **Building from Scratch?** If you're building a Sesame Robot from scratch, we recommend using the **S2 Mini with hand wiring approach** for the easiest assembly experience. The V2/V3 distro boards use advanced SMD components that require specialized soldering skills or PCB assembly services.
---
## Sesame Distro Board V3 (Current)
The **Sesame Distro Board V3** is the latest version and features:
- **ESP32-S3 Processor**
- **USB-C PD 12V** (requires high quality PD charger cable)
- **Bambu Lab 14500 Battery Connector** for cheap and effective battery powering (priority feature)
- One-click order link via PCBway: [Order On PCBway!](https://www.pcbway.com/project/shareproject/Sesame_Distro_Board_V3_377de6fe.html)
- Limited stock of pre-assembled units: [Full Contact Engineering](https://fullcontactengineering.com/products/sesame-distro-board-v3-pcb)
*Note: Due to time constraints, the USB-C PD 12V negotiation chip is slightly untested, so a high quality PD cable is necessary.*
### V3 Board Details and Schematics:
<img src="distro-v3/assets/close-v3.png" alt="Sesame Distro Board V3 Close-up" width="70%">
<img src="distro-v3/assets/Schematic_Sesame-Distro-Board-V3_2026-05-30.png" alt="Schematic_Sesame-Distro-Board-V3" width="70%">
<img src="distro-v3/assets/layout-v3.png" alt="Sesame Distro Board V3 Layout" width="70%">
### V3 Files Available
All files are located in the [`distro-v3/`](distro-v3/) directory:
- **Schematic source:** `SCH_Sesame-Distro-Board-V3.json` - EasyEDA source design file for the V3 schematic
- **Gerber file:** `Gerber_Sesame-Distro-Board-V3_PCB.zip` - For PCB fabrication
- **BOM file:** `BOM_Sesame-Distro-Board-V3.csv` - Bill of materials for SMD components
- **Pick-and-Place file:** `PickAndPlace_PCB_Sesame-Distro-Board-V3.csv` - Component placement data for assembly
## Sesame Distro Board V2 (Legacy)
The **Sesame Distro Board V2** is an older version. *Major instabilities on battery power (brownouts due to buck converter handling) restrict this to USB only, or you must bypass the chip by soldering a separate buck converter directly to the power line. I may have extra V3 boards to send to those who ordered V2.*
### V2 Assembly Options (Legacy)
The V2 board consists entirely of SMD (surface-mount) components, which are **advanced to hand solder**. We recommend:
1. **PCB Assembly Service (Recommended):** Use [PCBway&#39;s PCB assembly service](https://www.pcbway.com/pcb-assembly.html) to have the board professionally assembled. Upload the Gerber, BOM, and Pick-and-Place files to their assembly service.
2. **Hand Soldering (Advanced Only):** Only attempt hand soldering if you're experienced with SMD components and have the proper tools (hot air station, fine-tip soldering iron, flux, etc.).
### V2 Files Available
All files are located in the [`distro-v2/`](distro-v2/) directory:
- **Schematic source:** `SCH_Sesame-Distro-Board-V2_2026-03-06.json` - EasyEDA source design file for the V2 schematic
- **Gerber file:** `Gerber_Sesame-Distro-V2_PCB.zip` - For PCB fabrication
- **BOM file:** `BOM_Sesame-Distro-V2.csv` - Bill of materials for SMD components
- **Pick-and-Place file:** `PickAndPlace_PCB_Sesame-Distro-V2.csv` - Component placement data for assembly
### PCBway / JLCPCB Sourcing Notes
When uploading the BOM to PCBway or JLCPCB for assembly, note the following three components that require special handling:
| Designator | Issue | Corrected Part | LCSC # |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | -------------------------------------------------------- |
| `U5`, `U7` | **Solder Pads** — these are copper pads only, no physical component is needed. Remove these lines from the BOM when ordering assembly. | N/A | N/A |
| `5-12V` | 2-pin 3.5 mm screw terminal. Original part `1984617` (Phoenix Contact) cannot be sourced. | `XY302V-3.5-2P` | [C784940](https://www.lcsc.com/product-detail/C784940.html) |
| `JST-XH` | 4-pin JST XH connector. Original `JST-XH-4-PIN` cannot be sourced. | `B4B-XH-A(LF)(SN)` | [C144395](https://www.lcsc.com/product-detail/C144395.html) |
The BOM CSV has already been updated with the corrected parts. The solder-pad rows are marked **"No Part Required"** in the Name field so they are easy to identify and exclude.
---
## Sesame Distro Board V1 (Legacy)
> [!CAUTION]
> The Sesame Distro Board V1 is now **phased out** but still supported. V1 has some limitations: it won't run on tethered power (e.g., USB-C) and is harder to assemble than other options. V1 is still supported with wiring guides and firmware and works on battery power. If you have a V1 board, you can still use it successfully.
The Distro Board V1 pairs with the ESP32-DevKitC-32E.
> [!IMPORTANT]
> **ESP32 Pin Header Requirement:** The distro board V1 stacks on top of the ESP32-DevKitC-32E, so you need an ESP32 board **without pre-soldered pin headers**. If your board came with headers already soldered on the top, you will need to desolder all of the headers and flip them to the bottom side of the ESP32 board so the distro board can mount on top.
### V1 Board Schematic:
<img src="distro-v1/Schematic_Sesame-Distro-Board.png" alt="Schematic_Sesame-Distro-Board" width="70%">
### V1 Files Available
All files are located in the [`distro-v1/`](distro-v1/) directory.
---
## PCBway Sponsorship
**PCBway Sponsorship**
This project was sponsored by [PCBway](https://www.pcbway.com/), who manufactured the custom distro boards. PCBway offers high-quality PCB fabrication services with fast turnaround times and excellent customer support.
If you're building your own Sesame Robot, PCBway is a great option for getting professional-quality PCB fabrication and assembly services at reasonable prices:
- **PCB Fabrication:** Upload Gerber files to get boards manufactured
- **PCB Assembly:** Upload Gerber, BOM, and Pick-and-Place files for fully assembled boards (recommended for V2)
<img src="pcbs.png" alt="pcbs-from-pcbway" width="70%">
---
## How to Order
### Ordering V3 Boards (Current)
**Option 1: Pre-assembled Units (Recommended)**
Depending on stock availability, you can buy fully populated, pre-flashed boards directly from [Full Contact Engineering](https://fullcontactengineering.com/products/sesame-distro-board-v3-pcb).
**Option 2: PCBway Assembly Service**
1. Go to [PCBway&#39;s Shared Project Page](https://www.pcbway.com/project/shareproject/Sesame_Distro_Board_V3_377de6fe.html)
2. Add to cart to order the boards fully assembled!
**Option 3: Manual PCBway Assembly Service Upload**
1. Go to [PCBway&#39;s PCB Assembly service](https://www.pcbway.com/pcb-assembly.html)
2. Upload the Gerber file: `distro-v3/Gerber_Sesame-Distro-Board-V3_PCB.zip`
3. Upload the BOM file: `distro-v3/BOM_Sesame-Distro-Board-V3.csv`
4. Upload the Pick-and-Place file: `distro-v3/PickAndPlace_PCB_Sesame-Distro-Board-V3.csv`
5. Confirm board specifications and component availability
### Ordering V2 Boards (Deprecated)
**Option 1: PCB Assembly Service (Recommended)**
1. Go to [PCBway&#39;s PCB Assembly service](https://www.pcbway.com/pcb-assembly.html)
2. Upload the Gerber file: `distro-v2/Gerber_Sesame-Distro-V2_PCB.zip`
3. Upload the BOM file: `distro-v2/BOM_Sesame-Distro-V2.csv`
4. Upload the Pick-and-Place file: `distro-v2/PickAndPlace_PCB_Sesame-Distro-V2.csv`
5. Confirm board specifications and component availability
6. Place your order to receive fully assembled boards
**Option 2: Fabrication Only (For Advanced Users)**
1. Download the Gerber file from [`distro-v2/Gerber_Sesame-Distro-V2_PCB.zip`](distro-v2/Gerber_Sesame-Distro-V2_PCB.zip)
2. Upload to [PCBway](https://www.pcbway.com/) or another PCB manufacturer
3. Confirm board specifications in the preview
4. Order the bare PCBs and hand-solder SMD components yourself (advanced)
### Ordering V1 Boards (Legacy)
1. Download the Gerber file from [`distro-v1/Gerber_Sesame-Distro-Board_PCB_Sesame-Distro-Board_V1.zip`](distro-v1/Gerber_Sesame-Distro-Board_PCB_Sesame-Distro-Board_V1.zip)
2. Upload to [PCBway](https://www.pcbway.com/) or another PCB manufacturer
3. Confirm board specifications in the preview
4. Place your order to receive bare V1 boards
Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.
1 ID Name Designator Footprint Quantity Manufacturer Part Manufacturer Supplier Supplier Part Price
2 1 22nF C3,C9,C11 C0603 3 CL10B223KB8NNNC SAMSUNG(三星) LCSC C21122 0.004
3 2 100nF C4,C8,C10 C0603 3 CC0603KRX7R9BB104 YAGEO(国巨) LCSC C14663 0.002
4 3 10nF C5 C0603 1 C0603B103M500NT TORCH(火炬) LCSC C17702744 0.005
5 4 3.3uH L1 IND-SMD_L7.3-W6.6 1 MHCI06030-3R3M-S8 Chilisin(奇力新) LCSC C285713 0.147
6 5 KT-0603R LED1 LED0603-RD 1 KT-0603R KENTO LCSC C2286 0.007
7 6 5.1kΩ R1,R5 R0603 2 0603WAF5101T5E UNI-ROYAL(厚声) LCSC C23186 0.001
8 7 56kΩ R2 R0603 1 0603WAF5602T5E UNI-ROYAL(厚声) LCSC C23206 0.001
9 8 10kΩ R3,R4,R8,R9 R0603 4 RCT0310KJLF HKR(香港电阻) LCSC C177337
10 9 3.5MM-2PIN 5-12V SCREWTERM-3.5MM-2PIN 1 XY302V-3.5-2P XY LCSC C784940
11 10 B4B-XH-A(LF)(SN) JST-XH JST-XH-4PIN-2.5 1 B4B-XH-A(LF)(SN) JST LCSC C144395
12 11 HX 3X4X2-2P-1.6N TACTILE SWITCH RESET,BOOT KEY-SMD_L4.0-W3.0-LS4.9-1 2 HX 3x4x2-2P-1.6N TACTILE SWITCH hanxia(韩下) LCSC C49234124 0.02
13 12 SolderPad (No Part Required) U5,U7 SOLDERPAD LG 2
14 13 1uF C1 C0805 1 AC0805KKX7R9BB105 YAGEO(国巨) LCSC C726584 0.035
15 14 470uF C7 CAP-SMD_BD10.0-L10.3-W10.3-LS11.3-FD 1 XT470UF25V90RV0111 KNSCHA(科尼盛) LCSC C3445244 0.128
16 15 SS34_C8678 D1,D2 SMA_L4.3-W2.6-LS5.2-RD 2 SS34 MDD(辰达半导体) LCSC C8678 0.03
17 16 3PIN MALE HEADER P1,P2,P3,P4,P5,P6,P7,P8 CONNECTOR_PINHEADER_2.54MM:PINHEADER_1X3 8 Male headerHDR1X3-2.54 BOOMELE LCSC C49257 0.088
18 17 ESP32-S3-WROOM-1(N16R8) U1 WIRELM-SMD_ESP32-S3-WROOM-1 1 ESP32-S3-WROOM-1-N16R8 ESPRESSIF(乐鑫) LCSC C2913202 5.613
19 18 AMS1117-3.3 U2 SOT-223-3_L6.5-W3.4-P2.30-LS7.0-BR 1 AMS1117-3.3 AMS LCSC C6186 0.197
20 19 MSTPS563200DDCR U3BUCK SOT-23-6_L2.9-W1.6-P0.95-LS2.8-BR 1 MSTPS563200DDCR MSKSEMI(美森科) LCSC C49208508 0.177
21 20 10uF U4 CAP-SMD_BD4.0-L4.3-W4.3-LS5.0-FD 1 RST10UF25V014 KNSCHA(科尼盛) LCSC C4747969 0.025
22 21 EXPANSION HEADER (No Part Required) U6 HEADER MALE 2.54-1*10 1
23 22 TYPE-C-31-M-12 USBC1 USB-C_SMD-TYPE-C-31-M-12_1 1 TYPE-C-31-M-12 韩国韩荣 LCSC C165948 0.183
Binary file not shown.
1 Designator Footprint Mid X Mid Y Ref X Ref Y Pad X Pad Y Layer Rotation Comment
2 5-12V SCREWTERM-3.5MM-2PIN 55.47mm 3.05mm 53.72mm 3.05mm 53.72mm 3.05mm T 0 3.5MM-2PIN
3 BOOT KEY-SMD_L4.0-W3.0-LS4.9-1 22.86mm 21.97mm 22.86mm 21.97mm 20.7mm 21.97mm T 0 HX 3X4X2-2P-1.6N TACTILE SWITCH
4 C1 C0805 25.02mm 2.92mm 25.02mm 2.92mm 26.02mm 2.92mm T 180 1uF
5 C3 C0603 47.88mm 17.65mm 47.88mm 17.65mm 47.18mm 17.65mm T 0 22nF
6 C4 C0603 47.88mm 12.57mm 47.88mm 12.57mm 47.18mm 12.57mm T 0 100nF
7 C5 C0603 50.55mm 3.3mm 50.55mm 3.3mm 50.55mm 2.6mm T 90 10nF
8 C7 CAP-SMD_BD10.0-L10.3-W10.3-LS11.3-FD 35.56mm 7.49mm 35.56mm 7.49mm 31.06mm 7.49mm T 0 470uF
9 C8 C0603 34.67mm 19.43mm 34.67mm 19.43mm 35.37mm 19.43mm T 180 100nF
10 C9 C0603 50.42mm 17.4mm 50.42mm 17.4mm 50.42mm 16.7mm T 90 22nF
11 C10 C0603 42.04mm 10.41mm 42.04mm 10.41mm 42.04mm 9.71mm T 90 100nF
12 C11 C0603 47.88mm 14.22mm 47.88mm 14.22mm 47.18mm 14.22mm T 0 22nF
13 D1 SMA_L4.3-W2.6-LS5.2-RD 44.45mm 15.11mm 44.45mm 15.11mm 44.45mm 12.91mm T 90 SS34_C8678
14 D2 SMA_L4.3-W2.6-LS5.2-RD 20.19mm 0.25mm 20.19mm 0.25mm 17.99mm 0.25mm T 0 SS34_C8678
15 JST-XH JST-XH-4PIN-2.5 24.26mm 12.07mm 24.26mm 12.07mm 24.26mm 15.82mm T 90 JST-XH-4-PIN
16 L1 IND-SMD_L7.3-W6.6 42.93mm 21.97mm 42.93mm 21.97mm 46.2mm 21.97mm T 0 3.3uH
17 LED1 LED0603-RD 52.96mm 18.03mm 52.96mm 18.03mm 53.71mm 18.03mm T 180 KT-0603R
18 P1 CONNECTOR_PINHEADER_2.54MM:PINHEADER_1X3 -5.08mm 2.41mm -5.08mm 4.95mm -5.08mm 4.95mm T 0 3PIN
19 P2 CONNECTOR_PINHEADER_2.54MM:PINHEADER_1X3 -2.29mm 2.41mm -2.29mm 4.95mm -2.29mm 4.95mm T 0 3PIN
20 P3 CONNECTOR_PINHEADER_2.54MM:PINHEADER_1X3 0.51mm 2.41mm 0.51mm 4.95mm 0.51mm 4.95mm T 0 3PIN
21 P4 CONNECTOR_PINHEADER_2.54MM:PINHEADER_1X3 3.3mm 2.41mm 3.3mm 4.95mm 3.3mm 4.95mm T 0 3PIN
22 P5 CONNECTOR_PINHEADER_2.54MM:PINHEADER_1X3 6.1mm 2.41mm 6.1mm 4.95mm 6.1mm 4.95mm T 0 3PIN
23 P6 CONNECTOR_PINHEADER_2.54MM:PINHEADER_1X3 8.89mm 2.41mm 8.89mm 4.95mm 8.89mm 4.95mm T 0 3PIN
24 P7 CONNECTOR_PINHEADER_2.54MM:PINHEADER_1X3 11.68mm 2.41mm 11.68mm 4.95mm 11.68mm 4.95mm T 0 3PIN
25 P8 CONNECTOR_PINHEADER_2.54MM:PINHEADER_1X3 14.48mm 2.41mm 14.48mm 4.95mm 14.48mm 4.95mm T 0 3PIN
26 R1 R0603 50.55mm 6.22mm 50.55mm 6.22mm 50.55mm 5.47mm T 90 5.1kΩ
27 R2 R0603 32.51mm 15.75mm 32.51mm 15.75mm 31.76mm 15.75mm T 0 56kΩ
28 R3 R0603 34.67mm 17.78mm 34.67mm 17.78mm 35.42mm 17.78mm T 180 10kΩ
29 R4 R0603 21.21mm 24.64mm 21.21mm 24.64mm 20.46mm 24.64mm T 0 10kΩ
30 R5 R0603 47.88mm 15.88mm 47.88mm 15.88mm 47.13mm 15.88mm T 0 5.1kΩ
31 R8 R0603 28.7mm 2.79mm 28.7mm 2.79mm 27.95mm 2.79mm T 0 10kΩ
32 R9 R0603 29.21mm 15.75mm 29.21mm 15.75mm 28.46mm 15.75mm T 0 10kΩ
33 RESET KEY-SMD_L4.0-W3.0-LS4.9-1 19.81mm 3.94mm 19.81mm 3.94mm 17.65mm 3.94mm T 0 HX 3X4X2-2P-1.6N TACTILE SWITCH
34 U1 WIRELM-SMD_ESP32-S3-WROOM-1 9.91mm 16.13mm 9.91mm 16.13mm 1.01mm 7.38mm T 90 ESP32-S3-WROOM-1(N16R8)
35 U2 SOT-223-3_L6.5-W3.4-P2.30-LS7.0-BR 46.1mm 6.35mm 46.1mm 6.35mm 48.4mm 9.32mm T 90 AMS1117-3.3
36 U3BUCK SOT-23-6_L2.9-W1.6-P0.95-LS2.8-BR 30.86mm 18.54mm 30.86mm 18.54mm 32.21mm 17.59mm T 0 MSTPS563200DDCR
37 U4 CAP-SMD_BD4.0-L4.3-W4.3-LS5.0-FD 39.24mm 15.24mm 39.24mm 15.24mm 40.9mm 15.24mm T 180 10uF
38 U5 SOLDERPAD LG 28.83mm 24.26mm 28.83mm 24.26mm 28.83mm 24.26mm T 180 SolderPad
39 U6 HEADER MALE 2.54-1*10 37.47mm -0.51mm 26.04mm -0.51mm 26.04mm -0.51mm T 0 EXPANSION HEADER
40 U7 SOLDERPAD LG 35.31mm 24.26mm 35.31mm 24.26mm 35.31mm 24.26mm T 180 SolderPad
41 USBC1 USB-C_SMD-TYPE-C-31-M-12_1 54.23mm 12.07mm 54.23mm 12.07mm 52.52mm 16.39mm T 90 TYPE-C-31-M-12
File diff suppressed because one or more lines are too long
Binary file not shown.
1 ID Name Designator Footprint Quantity Manufacturer Part Manufacturer Supplier Supplier Part Price
2 1 1uF C10 C0402 1 CC0402MRY5V6BB105 YAGEO(国巨) LCSC C527031 0.006
3 2 XY-XH2.54-2A11-R CN1 CONN-TH_2P-P2.50_C9900022029 1 XY-XH2.54-2A11-R XYECONN(辛译) LCSC C22391422 0.015
4 3 1kΩ R4,R3 R0402 2 0402WGF1001TCE UNI-ROYAL(厚声) LCSC C11702 0.001
5 4 10kΩ R8,R5,R7,R12,R15 R0402 5 0402WGF1002TCE UNI-ROYAL(厚声) LCSC C25744 0.001
6 5 HX 3X4X2-2P-1.6N TACTILE SWITCH RST,BOOT KEY-SMD_L4.0-W3.0-LS4.9-1 2 HX 3x4x2-2P-1.6N TACTILE SWITCH hanxia(韩下) LCSC C49234124 0.019
7 6 HOLE 1.5 H3,H4 HOLEHOLE 2 No Part Required
8 7 LTC4416EMS#PBF U7 MSOP-10_L3.0-W3.0-P0.50-LS5.0-BL 1 LTC4416EMS#PBF ADI(亚德诺) LCSC C694754 8.147
9 8 820uF C1 CAP-TH_BD8.0-P3.50-D0.6-FD 1 E2821M010D120RL 瑞隆 LCSC C2898774 0.123
10 9 100nF C2,C7,C18 C0402 3 VJ0402Y104KXJPW1BC VISHAY(威世) LCSC C3891429 0.073
11 10 10nF C4 C0402 1 CL05B103KB5NNNC SAMSUNG(三星) LCSC C15195 0.002
12 11 1uF C5 C0402 1 CL05A105KA5NQNC SAMSUNG(三星) LCSC C52923 0.005
13 12 100nF C11,C_MUX1,C_MUX2,C_MUX3 C0402 4 MC0402B104K500N5RH Sunway(信维通信) LCSC C3152552
14 13 10uF C12,C13,C14NB C0402 3 CL05A106MQ5NUNC SAMSUNG(三星) LCSC C15525 0.017
15 14 47pF C19 C0402 1 0402CG470J500NT FH(风华) LCSC C1567 0.001
16 15 4.7kΩ R1,R13 R0402 2 0402WGF4701TCE UNI-ROYAL(厚声) LCSC C25900 0.001
17 16 100kΩ R9 R0402 1 0402WGF1003TCE UNI-ROYAL(厚声) LCSC C25741 0.001
18 17 24kΩ R11 R0603 1 0603WAF2402T5E UNI-ROYAL(厚声) LCSC C23352 0.002
19 18 33kΩ R14 R0402 1 0402WGF3302TCE UNI-ROYAL(厚声) LCSC C25779 0.001
20 19 10nF C3 C0805 1 CL21B103KBANNNC SAMSUNG(三星) LCSC C1710 0.008
21 20 3.3nF C6 C0603 1 CL10B332KB8NNNC SAMSUNG(三星) LCSC C1613 0.006
22 21 22uF C8,C9,C17 C0603 3 CL10A226MQ8NRNC SAMSUNG(三星) LCSC C59461 0.013
23 22 SS54_C123946 D1 SMA_L4.4-W2.8-LS5.4-RD 1 SS54 晶导微电子 LCSC C123946 0.052
24 23 JST-XH-4-PIN JST1 JST-XH-4PIN-2.5 1 C144395 0.054
25 24 4.7uH L1 IND-SMD_L7.0-W6.6_APH0630 1 APH0630T4R7M APV(爱普微) LCSC C5349705 0.072
26 25 KT-0603R LED1 LED0603-RD 1 KT-0603R KENTO LCSC C2286 0.007
27 26 CSD25402Q3A Q_BAT,Q_USB VSONP-8_L3.1-W3.1-P0.65-LS3.5-BL 2 CSD25402Q3A TI(德州仪器) LCSC C111356 0.638
28 27 62kΩ R2 R0603 1 RT0603BRD0762KL YAGEO(国巨) LCSC C136961 0.026
29 28 45.3kΩ R6 R0603 1 RMC 0603 45K3 F N Tyohm(幸亚电阻) LCSC C325695 0.004
30 29 HX S0 HDR-TH_3P-P2.54-V-M-1 1 HX PH254-01-03-Z-L11.5 pcb pin header hanxia(韩下) LCSC C52016391 0.023
31 30 HX PH254-01-03-Z-L11.5 PCB PIN HEADER S1,S2,S3,S4,S5,S6,S7 HDR-TH_3P-P2.54-V-M-1 7 HX PH254-01-03-Z-L11.5 pcb pin header hanxia(韩下) LCSC C52016391 0.023
32 31 ESP32-S3-WROOM-1(N16R8) U1 WIRELM-SMD_ESP32-S3-WROOM-1 1 ESP32-S3-WROOM-1-N16R8 ESPRESSIF(乐鑫) LCSC C2913202 5.389
33 32 TPS54531DDA U2 SOIC-8_L4.9-W3.9-P1.27-LS6.0-BL-EP2.9 1 TPS54531DDA TI(德州仪器) LCSC C544938 0.631
34 33 SY8120B1ABC U3 SOT-23-6_L2.9-W1.6-P0.95-LS2.8-BL 1 SY8120B1ABC Silergy(矽力杰) LCSC C88474 0.161
35 34 4.7uH U4 IND-SMD_L4.4-W4.2 1 APH0420T4R7M APV(爱普微) LCSC C5349668 0.081
36 35 EXPANSION HEADER U6 HEADER MALE 2.54-1*10 1 No Part Required
37 36 CH224K U8 ESSOP-10_L4.9-W3.9-P1.0-LS6.0-TL-EP 1 CH224K WCH(南京沁恒) LCSC C970725 0.616
38 37 TYPE-C-31-M-12 USBC1 USB-C_SMD-TYPE-C-31-M-12_1 1 TYPE-C-31-M-12 韩国韩荣 LCSC C165948 0.188
File diff suppressed because one or more lines are too long
Binary file not shown.
1 Designator Footprint Mid X Mid Y Ref X Ref Y Pad X Pad Y Layer Rotation Comment
2 BOOT KEY-SMD_L4.0-W3.0-LS4.9-1 29.85mm 16.38mm 29.85mm 16.38mm 27.69mm 16.38mm T 0 HX 3X4X2-2P-1.6N TACTILE SWITCH
3 C1 CAP-TH_BD8.0-P3.50-D0.6-FD 43.69mm 6.35mm 43.69mm 6.35mm 41.94mm 6.35mm T 0 820uF
4 C2 C0402 35.56mm 8.51mm 35.56mm 8.51mm 35.56mm 8.09mm T 90 100nF
5 C3 C0805 34.8mm 18.03mm 34.8mm 18.03mm 33.8mm 18.03mm T 0 10nF
6 C4 C0402 47.24mm 11.43mm 47.24mm 11.43mm 47.24mm 11.01mm T 90 10nF
7 C5 C0402 24.89mm -1.02mm 24.89mm -1.02mm 25.31mm -1.02mm T 180 1uF
8 C6 C0603 42.29mm 13.21mm 42.29mm 13.21mm 41.59mm 13.21mm T 0 3.3nF
9 C7 C0402 52.71mm 3.68mm 52.71mm 3.68mm 53.13mm 3.68mm T 180 100nF
10 C8 C0603 49.91mm 3.68mm 49.91mm 3.68mm 49.21mm 3.68mm T 0 22uF
11 C9 C0603 44.7mm 11.43mm 44.7mm 11.43mm 45.4mm 11.43mm T 180 22uF
12 C10 C0402 52.71mm 2.41mm 52.71mm 2.41mm 52.28mm 2.41mm T 0 1uF
13 C11 C0402 47.5mm 2.67mm 47.5mm 2.67mm 47.5mm 3.09mm T 270 100nF
14 C12 C0402 46.1mm 13.21mm 46.1mm 13.21mm 45.68mm 13.21mm T 0 10uF
15 C13 C0402 37.59mm 17.91mm 37.59mm 17.91mm 37.59mm 18.33mm T 270 10uF
16 C14NB C0402 26.8mm -3.18mm 26.8mm -3.18mm 26.8mm -2.75mm T 270 10uF
17 C17 C0603 53.85mm 5.21mm 53.85mm 5.21mm 54.55mm 5.21mm T 180 22uF
18 C18 C0402 52.83mm 7.75mm 52.83mm 7.75mm 52.83mm 7.33mm T 90 100nF
19 C19 C0402 33.66mm 7.87mm 33.66mm 7.87mm 34.08mm 7.87mm T 180 47pF
20 CN1 CONN-TH_2P-P2.50_C9900022029 60.45mm -5.08mm 60.45mm -5.08mm 60.45mm -6.33mm T 90 XY-XH2.54-2A11-R
21 C_MUX1 C0402 37.08mm 10.16mm 37.08mm 10.16mm 36.66mm 10.16mm T 0 100nF
22 C_MUX2 C0402 38.74mm 17.91mm 38.74mm 17.91mm 38.74mm 18.33mm T 270 100nF
23 C_MUX3 C0402 54.99mm 2.41mm 54.99mm 2.41mm 54.99mm 1.99mm T 90 100nF
24 D1 SMA_L4.4-W2.8-LS5.4-RD 37.85mm 5.84mm 37.85mm 5.84mm 37.85mm 3.28mm T 90 SS54_C123946
25 H3 HOLEHOLE 41.91mm 17.27mm 41.91mm 17.27mm 41.91mm 17.27mm T 0 HOLE 1.5
26 H4 HOLEHOLE 45.47mm 17.27mm 45.47mm 17.27mm 45.47mm 17.27mm T 0 HOLE 1.5
27 JST1 JST-XH-4PIN-2.5 41.78mm -2.54mm 41.78mm -2.54mm 45.53mm -2.54mm T 0 JST-XH-4-PIN
28 L1 IND-SMD_L7.0-W6.6_APH0630 30.86mm 3.56mm 30.86mm 3.56mm 33.76mm 3.56mm T 180 4.7uH
29 LED1 LED0603-RD 59.44mm 11.68mm 59.44mm 11.68mm 60.19mm 11.68mm T 180 KT-0603R
30 Q_BAT VSONP-8_L3.1-W3.1-P0.65-LS3.5-BL 38.74mm 14.48mm 38.74mm 14.48mm 37.75mm 12.98mm T 0 CSD25402Q3A
31 Q_USB VSONP-8_L3.1-W3.1-P0.65-LS3.5-BL 34.93mm 14.48mm 34.93mm 14.48mm 33.94mm 12.98mm T 0 CSD25402Q3A
32 R1 R0402 28.32mm -3.3mm 28.32mm -3.3mm 28.32mm -3.73mm T 90 4.7kΩ
33 R2 R0603 33.53mm 10.29mm 33.53mm 10.29mm 33.53mm 9.53mm T 90 62kΩ
34 R3 R0402 56.77mm 11.68mm 56.77mm 11.68mm 57.2mm 11.68mm T 180 1kΩ
35 R4 R0402 37.08mm 11.43mm 37.08mm 11.43mm 36.65mm 11.43mm T 0 1kΩ
36 R5 R0402 26.92mm 18.42mm 26.92mm 18.42mm 27.36mm 18.42mm T 180 10kΩ
37 R6 R0603 27.56mm -1.02mm 27.56mm -1.02mm 28.31mm -1.02mm T 180 45.3kΩ
38 R7 R0402 54.48mm 7.75mm 54.48mm 7.75mm 54.48mm 7.31mm T 90 10kΩ
39 R8 R0402 52.71mm 1.14mm 52.71mm 1.14mm 52.27mm 1.14mm T 0 10kΩ
40 R9 R0402 35.56mm 6.35mm 35.56mm 6.35mm 35.56mm 5.92mm T 90 100kΩ
41 R11 R0603 49.91mm 1.65mm 49.91mm 1.65mm 50.66mm 1.65mm T 180 24kΩ
42 R12 R0402 35.18mm 10.8mm 35.18mm 10.8mm 35.18mm 10.36mm T 90 10kΩ
43 R13 R0402 39.24mm 10.92mm 39.24mm 10.92mm 39.24mm 11.35mm T 270 4.7kΩ
44 R14 R0402 41.4mm 11.3mm 41.4mm 11.3mm 41.83mm 11.3mm T 180 33kΩ
45 R15 R0402 24.89mm -3.05mm 24.89mm -3.05mm 25.32mm -3.05mm T 180 10kΩ
46 RST KEY-SMD_L4.0-W3.0-LS4.9-1 26.8mm -7.75mm 26.8mm -7.75mm 24.64mm -7.75mm T 0 HX 3X4X2-2P-1.6N TACTILE SWITCH
47 S0 HDR-TH_3P-P2.54-V-M-1 2.79mm -4.32mm 2.79mm -4.32mm 2.79mm -6.86mm T 90 HX
48 S1 HDR-TH_3P-P2.54-V-M-1 5.59mm -4.32mm 5.59mm -4.32mm 5.59mm -6.86mm T 90 HX PH254-01-03-Z-L11.5 PCB PIN HEADER
49 S2 HDR-TH_3P-P2.54-V-M-1 8.38mm -4.32mm 8.38mm -4.32mm 8.38mm -6.86mm T 90 HX PH254-01-03-Z-L11.5 PCB PIN HEADER
50 S3 HDR-TH_3P-P2.54-V-M-1 11.18mm -4.32mm 11.18mm -4.32mm 11.18mm -6.86mm T 90 HX PH254-01-03-Z-L11.5 PCB PIN HEADER
51 S4 HDR-TH_3P-P2.54-V-M-1 13.97mm -4.32mm 13.97mm -4.32mm 13.97mm -6.86mm T 90 HX PH254-01-03-Z-L11.5 PCB PIN HEADER
52 S5 HDR-TH_3P-P2.54-V-M-1 16.76mm -4.32mm 16.76mm -4.32mm 16.76mm -6.86mm T 90 HX PH254-01-03-Z-L11.5 PCB PIN HEADER
53 S6 HDR-TH_3P-P2.54-V-M-1 19.56mm -4.32mm 19.56mm -4.32mm 19.56mm -6.86mm T 90 HX PH254-01-03-Z-L11.5 PCB PIN HEADER
54 S7 HDR-TH_3P-P2.54-V-M-1 22.35mm -4.32mm 22.35mm -4.32mm 22.35mm -6.86mm T 90 HX PH254-01-03-Z-L11.5 PCB PIN HEADER
55 U1 WIRELM-SMD_ESP32-S3-WROOM-1 16.64mm 9.27mm 16.64mm 9.27mm 7.74mm 0.52mm T 90 ESP32-S3-WROOM-1(N16R8)
56 U2 SOIC-8_L4.9-W3.9-P1.27-LS6.0-BL-EP2.9 29.72mm 10.92mm 29.72mm 10.92mm 27.81mm 8.2mm T 0 TPS54531DDA
57 U3 SOT-23-6_L2.9-W1.6-P0.95-LS2.8-BL 50.04mm 6.73mm 50.04mm 6.73mm 49.09mm 5.58mm T 0 SY8120B1ABC
58 U4 IND-SMD_L4.4-W4.2 32.26mm -2.67mm 32.26mm -2.67mm 34.29mm -2.67mm T 0 4.7uH
59 U6 HEADER MALE 2.54-1*10 43.69mm -8.13mm 32.26mm -8.13mm 32.26mm -8.13mm T 0 EXPANSION HEADER
60 U7 MSOP-10_L3.0-W3.0-P0.50-LS5.0-BL 51.44mm 10.67mm 51.44mm 10.67mm 49.33mm 11.67mm T 270 LTC4416EMS#PBF
61 U8 ESSOP-10_L4.9-W3.9-P1.0-LS6.0-TL-EP 52.45mm -2.29mm 52.45mm -2.29mm 55.45mm -4.29mm T 90 CH224K
62 USBC1 USB-C_SMD-TYPE-C-31-M-12_1 59.31mm 5.08mm 59.31mm 5.08mm 57.6mm 9.41mm T 90 TYPE-C-31-M-12
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 588 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

+50
View File
@@ -0,0 +1,50 @@
# 3D Printing Guide
Sesame is designed to be printed in **PLA**. Most parts print without supports, but the top cover needs supports in targeted areas. Below is a quick reference for each component:
Note: There are 3 different top cover styles currently available. **Enclosed v91 is recommended** as it's the most modern design with magnetic hat mounts, covered display wires, and multicolor detail support. See [Top Covers](stl/top-covers/) for details on all styles.
> [!NOTE]
> **Battery Upgrade:** The internal frame design has been slightly altered to fit the new recommended battery (Bambu Lab 14500 Li-ion). If you have an older Sesame and want to upgrade to this battery, simply print the new internal frame and install it.
## Recommended Settings
* **Material:** PLA / PLA+
* **Infill:** 8-10%
* **Wall Loops:** 2
* **Infill Pattern:** Honeycomb
## 3D Printed Components Support Guide
| Component | Supports Required | STL Link |
| -------------- | ----------------- | --------------------------------------------------- |
| Joint R1 | No | [R1-v117.stl](stl/R1-v117.stl) |
| Joint R2 | No | [R2-v117.stl](stl/R2-v117.stl) |
| Joint R3 | No | [R3-v117.stl](stl/R3-v117.stl) |
| Joint R4 | No | [R4-v117.stl](stl/R4-v117.stl) |
| Joint L1 | No | [L1-v117.stl](stl/L1-v117.stl) |
| Joint L2 | No | [L2-v117.stl](stl/L2-v117.stl) |
| Joint L3 | No | [L3-v117.stl](stl/L3-v117.stl) |
| Joint L4 | No | [L4-v117.stl](stl/L4-v117.stl) |
| Internal Frame | No | [Internal-Frame-v121.stl](stl/Internal-Frame-v121.stl) |
| Bottom Cover | No | [Bottom-Cover-v121.stl](stl/Bottom-Cover-v121.stl) |
| Top Cover | Yes | [Top Covers](stl/top-covers/) |
### Top Cover Settings (Original Style)
Brim: Outer brim only
Support type: Normal (Manual)
Manual support locations:
<img src="assets/topcover-supports2.png" alt="topcover-supports2" width="70%">
This is what the part should look like sliced:
<img src="assets/sliced-topcover.png" alt="sliced-topcover" width="70%">
### Recommended Print Orientation for Joints:
Using the auto orientate tool will place these the correct way.
<img src="assets/joints-orientation.png" alt="sliced-topcover" width="70%">
Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Some files were not shown because too many files have changed in this diff Show More