Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ jobs:
target: esp32
- path: 'components/mcp266/example'
target: esp32
- path: 'components/mcp266/webapp_example'
target: esp32s3
command: 'IDF_COMPONENT_MANAGER=0 idf.py build'
- path: 'components/bdc_driver/example'
target: esp32s3
- path: 'components/binary-log/example'
Expand Down
1 change: 1 addition & 0 deletions components/mcp266/idf_component.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ maintainers:
documentation: "https://esp-cpp.github.io/espp/motorcontrol/mcp266.html"
examples:
- path: example
- path: webapp_example
tags:
- cpp
- Component
Expand Down
477 changes: 477 additions & 0 deletions components/mcp266/web/mcp266_console.html

Large diffs are not rendered by default.

55 changes: 55 additions & 0 deletions components/mcp266/webapp_example/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# The following lines of boilerplate have to be in your project's CMakeLists
# in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.20)

# This example needs the managed `espressif/esp_tinyusb` component (required by
# usb_device). It supports two build modes:
#
# * DEFAULT (component manager ON) - how an end user builds it from the
# component registry: the manager fetches esp_tinyusb (and its tinyusb
# dependency) and the espp/* dependencies from the registry. EXTRA_COMPONENT_DIRS
# is narrowed to just the components this example uses so the manager does not
# scan every espp manifest (some board components declare target-specific
# constraints that would fail on esp32s3).
#
# * MANAGER OFF (IDF_COMPONENT_MANAGER=0) - used by CI so the build does not need
# the (as-yet unpublished) espp/* components in the registry: every espp
# dependency resolves locally from EXTRA_COMPONENT_DIRS, and esp_tinyusb /
# tinyusb come from the vendored git submodules under external/ (esp_tinyusb
# ships inside the espressif/esp-usb monorepo, so its component subdir is added).
include($ENV{IDF_PATH}/tools/cmake/project.cmake)

set(EXTRA_COMPONENT_DIRS
"../../../components/base_component"
"../../../components/canopen"
"../../../components/dispatcher"
"../../../components/format"
"../../../components/logger"
"../../../components/mcp266"
"../../../components/stream_frame"
"../../../components/task"
"../../../components/twai"
"../../../components/usb_device"
)

# With the component manager disabled, esp_tinyusb/tinyusb are not fetched from
# the registry; add the vendored submodule copies to the component search path.
# esp_tinyusb's own CMakeLists adds `tinyusb` to its REQUIRES when the manager is
# off, so both component directories must be discoverable here.
if(DEFINED ENV{IDF_COMPONENT_MANAGER} AND "$ENV{IDF_COMPONENT_MANAGER}" STREQUAL "0")
list(APPEND EXTRA_COMPONENT_DIRS
"../../../external/esp-usb/device/esp_tinyusb"
"../../../external/tinyusb"
)
endif()

set(
COMPONENTS
"main esptool_py base_component canopen dispatcher format logger mcp266 stream_frame task twai usb_device esp_tinyusb"
CACHE STRING
"List of components to include"
)

project(mcp266_webapp_example)

set(CMAKE_CXX_STANDARD 20)
70 changes: 70 additions & 0 deletions components/mcp266/webapp_example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# MCP266 Web Console Example

Turns an ESP32-S3 into a **WebUSB / Web Serial front-end** for a Basicmicro
MCP266 motor controller: the hosted
[MCP266 console web app](https://esp-cpp.github.io/espp/apps/mcp266_console.html)
connects over native USB and can

- **configure** each axis' position loop (clamp + fallback P gain) and CiA 402
software limits, and clear faults / e-stop,
- **command** profile-position moves (target, velocity, accel, decel), and
- **view** live per-axis status — position, velocity, DS402 state, target-reached
— plus device telemetry (battery voltage, temperature).

Unlike the [CAN bridge](../../canopen/can_bridge_example) (which forwards raw CAN
and runs CANopen in the browser), this example runs the `espp::Mcp266` driver
**on the device** and exposes a small high-level protocol (see
`main/mcp266_protocol.hpp`, dispatcher **module id 6**), so the web app needs no
CANopen/DS402 knowledge. Both the vendor (WebUSB) and CDC (Web Serial) interfaces
carry the same protocol; the system console/logs stay on the built-in
USB-Serial-JTAG.

## Wiring & configuration

The ESP32-S3 is the CANopen **master** of the MCP266 node. Connect the TWAI
TX/RX GPIOs to a 3.3 V CAN transceiver (e.g. SN65HVD230) on a 120 Ω-terminated
bus, at the baudrate configured on the MCP266 in Basicmicro Motion Studio.

Defaults (change in `main/mcp266_webapp_example.cpp`):

| Setting | Value |
|---------|-------|
| TWAI TX | GPIO 17 |
| TWAI RX | GPIO 16 |
| CAN baudrate | 1000000 |
| MCP266 node id | 10 |

## Protocol (module 6)

Framed with `stream_frame` and routed by `espp::Dispatcher`. Requests use type
high-nibble 6; replies/events use high-nibble E (reply flag set).

| Type | Dir | Meaning |
|------|-----|---------|
| `0x60` START | H→D | NMT-start the node + clear faults |
| `0x61` RESET_FAULTS | H→D | clear latched CiA 402 faults (both axes) |
| `0x62` RESET_ESTOP | H→D | attempt an e-stop reset |
| `0x63` CONFIGURE_POSITION_LOOP | H→D | `[axis u8][min i32][max i32][fallback_p i32]` |
| `0x64` SET_POSITION_LIMITS | H→D | CiA 402 software limits: `[axis u8][min i32][max i32]` |
| `0x65` MOVE_TO_POSITION | H→D | `[axis u8][target i32][vel u32][accel u32][decel u32]` |
| `0x66` DRIVE_SPEED | H→D | `[axis u8][qpps i32]` (inert on tested firmware) |
| `0x67` DRIVE_DUTY | H→D | `[axis u8][duty i16]` (inert on tested firmware) |
| `0x68` GET_STATUS | H→D | request one STATUS snapshot |
| `0x69` SET_STATUS_STREAM | H→D | `[enable u8][period_ms u16]` |
| `0x6A` GET_DEVICE_INFO | H→D | request DEVICE_INFO |
| `0xE0` STATUS | D→H | per-axis `[pos i32][vel i32][statusword u16]` ×2, then `[battery_dV u16][temp_dC u16][flags u8]` |
| `0xE1` OK | D→H | `[request_type u8]` |
| `0xE2` ERROR | D→H | `[request_type u8][code u32][utf8 message]` |
| `0xE3` DEVICE_INFO | D→H | `[device_type u32][utf8 name]` |

`axis` is `0` = M1, `1` = M2.

## Build & flash

```
idf.py set-target esp32s3
idf.py build flash monitor
```

Then open the MCP266 console web app and Connect (WebUSB or Web Serial). Click
**Start node**, tick **Live status**, then configure a loop and command a move.
5 changes: 5 additions & 0 deletions components/mcp266/webapp_example/main/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
idf_component_register(
SRC_DIRS "."
INCLUDE_DIRS "."
REQUIRES mcp266 canopen twai usb_device dispatcher stream_frame task logger
)
70 changes: 70 additions & 0 deletions components/mcp266/webapp_example/main/mcp266_protocol.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#pragma once

// Wire protocol for the USB <-> MCP266 web console example.
//
// Unlike the CAN bridge (which forwards raw CAN frames and runs CANopen in the
// browser), this example runs the espp::Mcp266 driver ON the device and exposes
// a small, HIGH-LEVEL command protocol: the browser sends "configure axis",
// "move to position", "get status" and the firmware translates each to Mcp266
// calls over CANopen. So the web app needs no CANopen/DS402 knowledge.
//
// Framed with the espp stream_frame v2 codec and routed by an espp::Dispatcher
// on MODULE ID 6. Every frame sets module = 6. The `type` byte's high nibble is
// 6 for host->device requests and E for device->host replies/events; the reply
// types (0xE_) additionally set the frame reply flag (build_frame derives it
// from the type's high bit). Both the vendor (WebUSB) and CDC (Web Serial)
// interfaces carry this same protocol.
//
// Axis selector byte: 0 = M1, 1 = M2.

#include <cstdint>

namespace mcp266_protocol {

/// Dispatcher module id owned by the MCP266 console protocol.
static constexpr uint8_t kModuleId = 6;

/// Axis selector used in request payloads (matches espp::Mcp266::Axis order).
enum : uint8_t {
kAxisM1 = 0,
kAxisM2 = 1,
};

/// Host -> device (requests, high nibble 6).
enum : uint8_t {
kStart = 0x60, ///< NMT-start the node + clear latched faults (no payload)
kResetFaults = 0x61, ///< clear latched CiA 402 faults on both axes (no payload)
kResetEstop = 0x62, ///< attempt an e-stop reset (no payload)
kConfigurePositionLoop = 0x63, ///< [axis u8][min i32][max i32][fallback_p i32]
kSetPositionLimits = 0x64, ///< CiA 402 software limits: [axis u8][min i32][max i32]
kMoveToPosition = 0x65, ///< [axis u8][target i32][vel u32][accel u32][decel u32]
kDriveSpeed = 0x66, ///< [axis u8][qpps i32] (inert on tested firmware)
kDriveDuty = 0x67, ///< [axis u8][duty i16] (inert on tested firmware)
kGetStatus = 0x68, ///< request one STATUS snapshot (no payload)
kSetStatusStream = 0x69, ///< [enable u8][period_ms u16] periodic STATUS streaming.
///< period_ms is clamped to [50, 10000] on the device (0 =
///< default 200 ms); each snapshot issues eight blocking SDO
///< reads, so smaller periods are rejected to protect the bus.
kGetDeviceInfo = 0x6A, ///< request DEVICE_INFO (no payload)
};

/// Device -> host (replies / events, high nibble E => reply flag set).
enum : uint8_t {
kStatus = 0xE0, ///< status snapshot (see StatusPayload layout below)
kOk = 0xE1, ///< ack for a request: [request_type u8]
kError = 0xE2, ///< failure: [request_type u8][code u32][utf8 message]
kDeviceInfo = 0xE3, ///< [device_type u32][utf8 name]
};

/// STATUS payload layout (all multi-byte fields little-endian), 25 bytes:
/// per axis M1 then M2:
/// [position i32][velocity i32][statusword u16] (10 bytes each)
/// then device-level:
/// [battery_decivolts u16] (tenths of a volt)
/// [temp_decidegrees u16] (tenths of a degree C)
/// [flags u8] (bit0 = node responded to the last poll)
static constexpr uint8_t kStatusFlagOnline = 0x01;
static constexpr uint8_t kAxisStatusSize = 10; ///< i32 + i32 + u16
static constexpr uint8_t kStatusPayloadSize = 2 * kAxisStatusSize + 2 + 2 + 1; // = 25

} // namespace mcp266_protocol
Loading
Loading