chore: import upstream snapshot with attribution
@@ -0,0 +1 @@
|
||||
.vscode/launch.json
|
||||
@@ -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.
|
||||
@@ -0,0 +1,134 @@
|
||||
# The Sesame Robot Project
|
||||
___
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
<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"*
|
||||
@@ -0,0 +1,7 @@
|
||||
# WeHub 来源说明
|
||||
|
||||
- 原始项目:`dorianborian/sesame-robot`
|
||||
- 原始仓库:https://github.com/dorianborian/sesame-robot
|
||||
- 导入方式:上游默认分支的最新快照
|
||||
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
|
||||
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
|
||||
@@ -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
|
||||
@@ -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 | 1–2 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 | 1–2 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.
|
||||
|
||||
**You’ll 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 R1–R4/L1–L4) plus 6–8 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 (S0–S7) 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.1 V 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 1–7. 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 aren’t 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 Sesame’s Wi-Fi AP, load the control page, and trigger a pose to verify everything moves as expected.
|
||||
|
||||
That’s it. You’ve 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!
|
||||
|
After Width: | Height: | Size: 272 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 354 KiB |
|
After Width: | Height: | Size: 423 KiB |
|
After Width: | Height: | Size: 240 KiB |
|
After Width: | Height: | Size: 602 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 377 KiB |
|
After Width: | Height: | Size: 224 KiB |
|
After Width: | Height: | Size: 317 KiB |
|
After Width: | Height: | Size: 283 KiB |
|
After Width: | Height: | Size: 203 KiB |
|
After Width: | Height: | Size: 266 KiB |
|
After Width: | Height: | Size: 456 KiB |
|
After Width: | Height: | Size: 647 KiB |
|
After Width: | Height: | Size: 337 KiB |
|
After Width: | Height: | Size: 490 KiB |
|
After Width: | Height: | Size: 484 KiB |
|
After Width: | Height: | Size: 295 KiB |
|
After Width: | Height: | Size: 406 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 584 KiB |
|
After Width: | Height: | Size: 385 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 266 KiB |
|
After Width: | Height: | Size: 403 KiB |
|
After Width: | Height: | Size: 2.8 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 478 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
@@ -0,0 +1,3 @@
|
||||
# Images
|
||||
|
||||
Photos, diagrams, and illustrations for documentation.
|
||||
|
After Width: | Height: | Size: 4.1 MiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
@@ -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
|
||||
|
After Width: | Height: | Size: 415 KiB |
|
After Width: | Height: | Size: 587 KiB |
|
After Width: | Height: | Size: 825 KiB |
|
After Width: | Height: | Size: 875 KiB |
|
After Width: | Height: | Size: 286 KiB |
|
After Width: | Height: | Size: 911 KiB |
@@ -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 & 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.
|
||||
@@ -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()">▲</button>
|
||||
<div class="spacer"></div>
|
||||
|
||||
<button onmousedown="move('left')" onmouseup="stop()" ontouchstart="move('left')" ontouchend="stop()">◀</button>
|
||||
<button onmousedown="move('backward')" onmouseup="stop()" ontouchstart="move('backward')" ontouchend="stop()">▼</button>
|
||||
<button onmousedown="move('right')" onmouseup="stop()" ontouchstart="move('right')" ontouchend="stop()">▶</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°</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°</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°</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°</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°</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°</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°</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°</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.");
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 cover’s 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
|
||||
@@ -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 (5–12 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.6–0.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.
|
||||
@@ -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 Sesame’s 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** – Onshape’s 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.
|
||||
|
||||
@@ -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'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'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'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'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
|
||||
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 316 KiB |
|
|
|
|
|
After Width: | Height: | Size: 588 KiB |
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 209 KiB |
|
After Width: | Height: | Size: 224 KiB |
|
After Width: | Height: | Size: 4.4 MiB |
@@ -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%">
|
||||
|
After Width: | Height: | Size: 199 KiB |