Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 42 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,67 +1,65 @@
# Hexapod Robot Simulator

[![build status](https://github.com/mithi/hexapod/workflows/build/badge.svg)](https://hexapod.netlify.app)
[![Code Climate](https://codeclimate.com/github/mithi/hexapod/badges/gpa.svg)](https://codeclimate.com/github/mithi/hexapod)
[![buy me coffee](https://img.shields.io/badge/Buy%20me%20-coffee!-orange.svg?logo=buy-me-a-coffee&color=795548)](https://ko-fi.com/minimithi)
A browser-based kinematics and gait simulator adapted from Mithi's Bare-Minimum Hexapod Robot Simulator for the scorpion-style hexapod prototype described below. The calculations run in the browser; this project does not send commands to the physical servos.

# Mithi's Bare-Minimum Hexapod Robot Simulator 2
## Prototype configuration

<p align="center">
<img src="https://mithi.github.io/robotics-blog/show-off.gif" alt="drawing" width="400" />
</p>
All model dimensions are in **millimetres**. The six coxa axes are arranged in three left/right pairs:

You can use this web app to solve inverse kinematics, simulate various gaits, and more. In real time, you can also view all the angles the robot's eighteen joints make at any particular pose. All the computations are solely done in your browser, nothing's fetching data from somewhere else, so it should be fast. Another (somewhat) cool thing is that this app does NOT depend on any external mathematics library; it only uses Javascript's built-in Math object.
| Measurement | Value |
| --- | ---: |
| Left-to-right coxa-axis spacing, at each station | 120 mm |
| Front-to-middle and middle-to-rear coxa-axis spacing | 115 mm each |
| Coxia link: body axis to femur axis | 64.5 mm |
| Femur link: femur axis to tibia axis | 64.5 mm |
| Tibia link: tibia axis to foot tip | 130.59 mm |

👉 [hexapod.netlify.app](https://hexapod.netlify.app)
The current body parameters are `front: 60`, `middle: 60`, and `side: 115` in `src/templates/hexapodParams.js`. In this model, `front` and `middle` are offsets from the body centre (half the 120 mm spacing); `side` is the offset of the front and rear stations from the middle station. All six hip mounts sit along the two side rails, not at the chassis corners. The simple outline currently extends 30 mm beyond the front and rear mounts; replace that visual estimate when the chassis edge dimensions are available.

Consider buying me [a few cups of coffee ☕ ☕ ☕](https://ko-fi.com/minimithi) to motivate me to build other robotics-related visualizers. (Quadrotors?!)
The prototype uses 18 RDS3218 servos (three per leg, specified as 20 kg and 270°). The simulator currently has **no tail model**, as requested.

## Features

<img src="https://mithi.github.io/robotics-blog/show-off-v2-1.gif" alt="drawing" width="325" align="right" />
- Interactive 3D hexapod view and editable dimensions.
- Forward kinematics, inverse kinematics, leg-pose controls, and support/stability checks.
- Tripod and ripple gait options, forward/backward playback, and rotation mode.
- Gait frames are generated from Cartesian foot paths and solved through IK. If a requested stride or lift is unreachable, the solver reduces the gait amplitude until it finds a reachable sequence.
- The initial gait settings use the prototype's tripod reference values: 15° hip swing and 20° lift swing.

🎉 Control
The body and legs are rendered as a simplified kinematic model, not as the physical CAD solids.

- [x] The robot's dimensions
- [x] The robot's 3d orientation, translation, stance, and more
## Run locally

🎉 Solve
Requirements: Node.js and Yarn.

- [x] Inverse Kinematics
- [x] Forward Kinematics
```bash
yarn install
yarn start
```

🎉 Simulate
The development server normally opens at <http://localhost:3000>.

- [x] Ripple and tripod gait variations
- [x] Walking forward and backwards
- [x] Rotating clockwise and counterclockwise
Other useful commands:

[🤖](https://hexapod.netlify.app/) [🐳](https://mithi.github.io/deep-blueberry/) [☕](https://ko-fi.com/minimithi)
```bash
yarn test --watchAll=false --runInBand
yarn build
```

## Related Things
## Hardware and angle conventions

If you'd like to build your own user interface with Node, you can download the algorithm alone as a package: [Hexapod Kinematics Library](https://github.com/mithi/hexapod-kinematics-library). There is also [a "fork" modified where you can use the app to control a physical hexapod robot](https://github.com/mithi/hexapod-irl) as you can see in the gif below.
This repository is a **kinematic simulator**, not a dynamics simulator or servo controller. It does not model servo torque, electrical limits, collisions, mass/inertia, or ground friction, and it does not output PWM signals.

|![](https://user-images.githubusercontent.com/1670421/103467849-46981980-4d8e-11eb-911e-7cb63282c0c2.gif)|![](https://user-images.githubusercontent.com/1670421/103467765-451a2180-4d8d-11eb-8f94-1a23201595b9.gif)|
|--------|-------|
| Walking Gaits | Kinematics |
The hardware `stand`, `sit`, and `belly_touch` poses in the reference Python controller use servo command angles. They are not automatically the same as the simulator's `alpha`, `beta`, and `gamma` angles. A hardware mapping needs each joint's zero offset, direction (including any mirrored joints), and installed usable travel. A servo's advertised 270° travel should not be treated as a safe ±270° joint range; calibrate mechanical limits before using any simulated angles to drive hardware.

## Main Contributors [![PRs welcome!](https://img.shields.io/badge/PRs-welcome-orange.svg?style=flat)](./CONTRIBUTING.md)
## Main code locations

Any contribution to improve the source code is always appreciated. [See contributing Guide](./CONTRIBUTING.md). I will put your name below if I've merged your PR multiple times or if you've substantially contributed to this project in other ways.

- [@mithi](https://github.com/mithi)
- [@icyJoseph](https://github.com/icyJoseph)
- [@mikong](https://github.com/mikong)

## I love badges! (Don't we all?)

[![last commit](https://img.shields.io/github/last-commit/mithi/hexapod)](https://github.com/mithi/hexapod/commits/master)
[![commits per month](https://img.shields.io/github/commit-activity/m/mithi/hexapod?color=yellow&style=flat)](https://github.com/mithi/hexapod/graphs/commit-activity)
![top language](https://img.shields.io/github/languages/top/mithi/hexapod)
![code files size](https://img.shields.io/github/languages/code-size/mithi/hexapod?color=yellow)
![repo size](https://img.shields.io/github/repo-size/mithi/hexapod?color=violet)
[![codecov](https://codecov.io/gh/mithi/hexapod/branch/master/graph/badge.svg)](https://codecov.io/gh/mithi/hexapod)
[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat)](https://github.com/prettier/prettier)
[![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat)](https://opensource.org/licenses/Apache-2.0)
- `src/templates/hexapodParams.js` — prototype dimensions, default pose, and gait parameters.
- `src/hexapod/VirtualHexapod.js`, `src/hexapod/Linkage.js` — robot geometry and forward kinematics.
- `src/hexapod/solvers/ik/` — inverse-kinematics solvers.
- `src/hexapod/solvers/walkSequenceSolver.js` — Cartesian gait paths and per-frame IK.
- `src/components/pages/WalkingGaitsPage.js` — gait controls and animation.

## Attribution

This project is based on Mithi's Bare-Minimum Hexapod Robot Simulator 2 and its contributors. The upstream project is licensed under the Apache License 2.0; see [LICENSE](./LICENSE).
24 changes: 6 additions & 18 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,17 @@
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#32ff7e" />
<meta name="description" content="Mithi's Hexapod Robot Simulator" />
<meta name="theme-color" content="#0b1422" />
<meta
name="description"
content="Hexapod Robot Simulator for robot-specific dimensions, inverse kinematics, and IK-based walking gaits."
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/img/icon-192x192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>Mithi's Hexapod Robot Simulator</title>

<link rel="preconnect" crossorigin href="https://fonts.gstatic.com">
<link rel="preconnect" crossorigin href="https://www.googletagmanager.com">
<link rel="dns-prefetch" href="https://www.google-analytics.com">
<script defer src="https://www.googletagmanager.com/gtag/js?id=UA-170794768-1"></script>
<link rel="stylesheet" href="%PUBLIC_URL%/font.min.css" media="print" onload="this.media='all'; this.onload=null;">
<title>Hexapod Robot Simulator</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>

<script>
window.dataLayer = window.dataLayer || []
function gtag() { dataLayer.push(arguments) }
gtag('js', new Date())
gtag('config', 'UA-170794768-1')
</script>
<style>@font-face{font-family:'Permanent Marker';font-style:normal;font-weight:400;font-display:swap;src:local('Permanent Marker Regular'),local('PermanentMarker-Regular'),url(https://fonts.gstatic.com/s/permanentmarker/v9/Fh4uPib9Iyv2ucM6pGQMWimMp004La2Cf5b6jlg.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}</style>

</body>
</html>
11 changes: 6 additions & 5 deletions public/manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"short_name": "Hexapod",
"name": "Mithi's Hexapod Robot Simulator",
"short_name": "Hexapod Sim",
"name": "Hexapod Robot Simulator",
"icons": [
{
"src": "favicon.ico",
Expand All @@ -17,9 +17,10 @@
"src": "img/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}],
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#32ff7e",
"background_color": "#17212B"
"theme_color": "#35c9bb",
"background_color": "#0b1422"
}
18 changes: 6 additions & 12 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,6 @@ import { Nav, NavDetailed, DimensionsWidget } from "./components"
import { updateHexapod, Page } from "./AppHelpers"
import HexapodPlot from "./components/HexapodPlot"

window.dataLayer = window.dataLayer || []
function gtag() {
window.dataLayer.push(arguments)
}

class App extends React.Component {
state = {
inHexapodPage: false,
Expand All @@ -23,10 +18,7 @@ class App extends React.Component {
* * * * * * * * * * * * * */

onPageLoad = pageName => {
document.title = pageName + " - Mithi's Bare Minimum Hexapod Robot Simulator"
gtag("config", "UA-170794768-1", {
page_path: window.location.pathname + window.location.search,
})
document.title = pageName + " - Hexapod Robot Simulator"

if (pageName === SECTION_NAMES.landingPage) {
this.setState({ inHexapodPage: false })
Expand Down Expand Up @@ -71,7 +63,10 @@ class App extends React.Component {
render = () => (
<Router>
<Nav />
<div id="main">
<div
id="main"
className={this.state.inHexapodPage ? "sim-layout" : "landing-layout"}
>
<div id="sidebar">
<div hidden={!this.state.inHexapodPage}>
<DimensionsWidget
Expand All @@ -80,7 +75,6 @@ class App extends React.Component {
/>
</div>
<Page pageComponent={this.pageComponent} />
{!this.state.inHexapodPage ? <NavDetailed /> : null}
</div>
<div id="plot" className="border" hidden={!this.state.inHexapodPage}>
<HexapodPlot
Expand All @@ -89,7 +83,7 @@ class App extends React.Component {
/>
</div>
</div>
{this.state.inHexapodPage ? <NavDetailed /> : null}
<NavDetailed />
</Router>
)
}
Expand Down
5 changes: 3 additions & 2 deletions src/components/DimensionsWidget.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class DimensionsWidget extends Component {
get toggleSwitch() {
const props = {
id: "DimensionsWidgetSwitch",
value: this.state.isFine ? "1x" : "5x",
value: this.state.isFine ? "0.01 mm" : "1 mm",
handleChange: this.toggleMode,
showValue: true,
}
Expand All @@ -32,7 +32,7 @@ class DimensionsWidget extends Component {

get NumberInputFields() {
const { minVal, maxVal } = RANGE_PARAMS.dimensionInputs
const stepVal = this.state.isFine ? 1 : 5
const stepVal = this.state.isFine ? 0.01 : 1
const dimensions = this.props.params.dimensions

const numberInputFields = DIMENSION_NAMES.map(name => {
Expand All @@ -51,6 +51,7 @@ class DimensionsWidget extends Component {

render = () => (
<Card title={<h2>{this.sectionName}</h2>} other={this.toggleSwitch}>
<p>All dimensions are in millimetres.</p>
{this.NumberInputFields}
<ResetButton reset={this.reset} />
</Card>
Expand Down
2 changes: 1 addition & 1 deletion src/components/HexapodPlot.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class HexapodPlot extends React.Component {

render() {
if (!this.state.ready) {
return <p>Loading your cute robot...</p>
return <p>Loading 3D robot model...</p>
}

if (!this.props.hexapod) {
Expand Down
95 changes: 34 additions & 61 deletions src/components/Nav.js
Original file line number Diff line number Diff line change
@@ -1,77 +1,50 @@
import React from "react"
import { URL_LINKS, PATH_LINKS } from "./vars"
import { Link } from "react-router-dom"
import { Link, NavLink } from "react-router-dom"
import { PATH_LINKS } from "./vars"

const NAV_BULLETS_PREFIX = "navBullet"
const NAV_DETAILED_PREFIX = "navDetailed"

const BulletPageLink = ({ link, showDesc }) => (
const BulletPageLink = ({ link }) => (
<li>
<Link to={link.path} className="link-icon">
<span>
{link.icon} {showDesc ? link.description : null}
<NavLink
to={link.path}
exact={link.path === "/"}
className="nav-link"
activeClassName="is-active"
>
<span className="nav-link-icon" aria-hidden="true">
{link.icon}
</span>
</Link>
<span>{link.description}</span>
</NavLink>
</li>
)

const BulletUrlLink = ({ path, description, icon }) => (
<li>
<a
href={path}
className="link-icon"
target="_blank"
rel="noopener noreferrer"
children={
<span>
{icon} {description}
</span>
}
/>
</li>
)

const NavBullets = () => (
<ul id="top-bar">
{URL_LINKS.map(link => (
<BulletUrlLink
path={link.url}
key={NAV_BULLETS_PREFIX + link.url}
icon={link.icon}
/>
))}

{PATH_LINKS.map(link => (
<BulletPageLink key={NAV_BULLETS_PREFIX + link.path} link={link} />
))}
</ul>
)

const NavDetailed = () => (
<footer>
<nav id="nav">
<ul className="grid-cols-1 no-bullet">
{URL_LINKS.map(link => (
<BulletUrlLink
path={link.url}
key={NAV_DETAILED_PREFIX + link.url}
icon={link.icon}
description={link.description}
/>
))}
const Nav = () => (
<header className="app-header">
<Link to="/" className="brand-lockup" aria-label="Hexapod Robot Simulator home">
<span className="brand-mark" aria-hidden="true">
H
</span>
<span className="brand-copy">
<strong>HEXAPOD</strong>
<small>Robot simulator</small>
</span>
</Link>

<nav className="primary-navigation" aria-label="Primary navigation">
<ul id="top-bar">
{PATH_LINKS.map(link => (
<BulletPageLink
key={NAV_DETAILED_PREFIX + link.path}
link={link}
showDesc={true}
/>
<BulletPageLink key={link.path} link={link} />
))}
</ul>
</nav>
</footer>
</header>
)

const Nav = () => <NavBullets />
const NavDetailed = () => (
<footer className="app-footer">
<span>Hexapod Robot Simulator</span>
<span>Prototype kinematics · millimetres · tail omitted</span>
</footer>
)

export { Nav, NavDetailed }
4 changes: 2 additions & 2 deletions src/components/generic/SmallWidgets.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ const ToggleSwitch = ({ id, value, handleChange, showValue }) => (
)

const Card = ({ title, other, children }) => (
<div>
<section className="card">
<div className="card-header">
{title}
{other}
</div>
{children}
</div>
</section>
)

const BasicButton = ({ handleClick, children }) => (
Expand Down
Loading