M5Stack StickC Plus 2 · MCP2515 CAN Bus · WebSocket Bridge
Hardware
M5Stack StickC Plus 2
CAN Speed
500 kbps (OBD-II)
Transport
WebSocket :81
Board support + display/button drivers
MCP2515 SPI CAN controller driver
WebSocket server on ESP32
JSON serialisation for CAN frames
Install Arduino IDE & Board Package
Open Arduino IDE → Preferences → Additional Boards Manager URLs and add: https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/arduino/package_m5stack_index.json Then go to Tools → Board → Boards Manager and install "M5Stack".
Install Libraries
In Arduino IDE go to Sketch → Include Library → Manage Libraries and install all four dependencies listed above.
Configure WiFi Credentials
Edit the two lines near the top of the sketch: #define WIFI_SSID "YOUR_WIFI_SSID" #define WIFI_PASS "YOUR_WIFI_PASSWORD" Replace with your actual network name and password.
Select Board & Port
Tools → Board → M5Stack → M5Stick-C-Plus2 Tools → Port → select the COM/tty port for your device.
Upload & Connect Dashboard
Click Upload. Once the device boots, note the IP shown on its screen. In the JeepCANDash Device Connection page enter ws://<IP>:81 as the WebSocket URL.
/*
* JeepCANDash — M5Stack StickC Plus 2 + MCP2515 Firmware
* =========================================================
* Hardware : M5Stack StickC Plus 2 (ESP32-PICO-V3-02)
* CAN HAT : MCP2515 module wired to the HAT 8-pin header
* Purpose : Receive CAN frames at 500 kbps, forward them
* over WebSocket (port 81) as JSON so the
* JeepCANDash browser dashboard can consume them.
*
* SPI wiring (HAT header):
* StickC G0 → MCP2515 SI (MOSI)
* StickC G36 → MCP2515 SO (MISO)
* StickC G26 → MCP2515 SCK (Clock)
* StickC G25 → MCP2515 CS (Chip Select, active LOW)
* StickC G37 → MCP2515 INT (Interrupt, optional)
* StickC 3V3 → MCP2515 VCC
* StickC GND → MCP2515 GND
*
* Dependencies (install via Arduino Library Manager):
* - M5StickCPlus2 (M5Stack official)
* - mcp_can (coryjfowler/MCP_CAN_lib)
* - WebSockets (Links2004/arduinoWebSockets)
* - ArduinoJson (bblanchon/ArduinoJson v6.x)
*
* Board: "M5Stick-C-Plus2" in Arduino IDE
* (requires M5Stack board package)
*/
#include <M5StickCPlus2.h>
#include <WiFi.h>
#include <WebSocketsServer.h>
#include <mcp_can.h>
#include <SPI.h>
#include <ArduinoJson.h>
// ── WiFi credentials ──────────────────────────────────────
#define WIFI_SSID "YOUR_WIFI_SSID" #define WIFI_PASS"YOUR_WIFI_PASSWORD"
// ── MCP2515 SPI pins (HAT header) ─────────────────────────
#define CAN_CS_PIN 25 // G25 → CS
#define CAN_INT_PIN 37 // G37 → INT (optional, set -1 to disable)
#define SPI_MOSI 0 // G0
#define SPI_MISO 36 // G36
#define SPI_SCK 26 // G26
// ── CAN bus speed ─────────────────────────────────────────
// Common OBD-II / Jeep CAN speeds: CAN_500KBPS, CAN_250KBPS, CAN_125KBPS
#define CAN_SPEED CAN_500KBPS
#define MCP_CLOCK MCP_8MHZ // Change to MCP_16MHZ if your module uses 16 MHz crystal
// ── WebSocket server ──────────────────────────────────────
#define WS_PORT 81
// ── Globals ───────────────────────────────────────────────
MCP_CAN CAN(CAN_CS_PIN);
WebSocketsServer webSocket(WS_PORT);
uint8_t connectedClients = 0;
uint32_t frameCount = 0;
uint32_t lastDisplayMs = 0;
bool canReady = false;
// ── Forward declarations ──────────────────────────────────
void initDisplay();
void updateDisplay();
void onWebSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length);
void sendCanFrameToClients(uint32_t id, uint8_t ext, uint8_t len, uint8_t *buf);
// ─────────────────────────────────────────────────────────
void setup() {
// Initialise M5StickC Plus 2 (display, IMU, power, buttons)
M5.begin();
M5.Lcd.setRotation(3);
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setTextSize(1);
Serial.begin(115200);
Serial.println("[BOOT] JeepCANDash firmware starting…");
// ── Initialise custom SPI bus for MCP2515 ──────────────
SPI.begin(SPI_SCK, SPI_MISO, SPI_MOSI, CAN_CS_PIN);
// ── Initialise MCP2515 ─────────────────────────────────
M5.Lcd.setCursor(4, 4);
M5.Lcd.setTextColor(YELLOW);
M5.Lcd.print("Init MCP2515…");
uint8_t retries = 0;
while (CAN.begin(MCP_ANY, CAN_SPEED, MCP_CLOCK) != CAN_OK) {
Serial.println("[CAN] Init failed, retrying…");
delay(500);
if (++retries > 10) {
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setCursor(4, 4);
M5.Lcd.setTextColor(RED);
M5.Lcd.print("CAN INIT FAILED");
Serial.println("[CAN] FATAL: could not initialise MCP2515");
while (true) delay(1000);
}
}
// Set MCP2515 to normal mode (receive all frames, no filter)
CAN.setMode(MCP_NORMAL);
canReady = true;
Serial.println("[CAN] MCP2515 ready at 500 kbps");
// Optional: configure INT pin as input
if (CAN_INT_PIN >= 0) {
pinMode(CAN_INT_PIN, INPUT);
}
// ── Connect to WiFi ────────────────────────────────────
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setCursor(4, 4);
M5.Lcd.setTextColor(CYAN);
M5.Lcd.printf("WiFi: %s", WIFI_SSID);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);
Serial.printf("[WiFi] Connecting to %s", WIFI_SSID);
uint8_t wifiRetries = 0;
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
M5.Lcd.print(".");
if (++wifiRetries > 40) {
// Continue without WiFi — CAN frames will still be read
Serial.println("
[WiFi] Could not connect. Running in CAN-only mode.");
break;
}
}
if (WiFi.status() == WL_CONNECTED) {
Serial.printf("
[WiFi] Connected. IP: %s
", WiFi.localIP().toString().c_str());
}
// ── Start WebSocket server ─────────────────────────────
webSocket.begin();
webSocket.onEvent(onWebSocketEvent);
Serial.printf("[WS] WebSocket server started on port %d
", WS_PORT);
initDisplay();
}
// ─────────────────────────────────────────────────────────
void loop() {
M5.update(); // Poll buttons / power
webSocket.loop(); // Service WebSocket clients
// ── Read CAN frame if available ────────────────────────
bool frameAvailable = false;
if (CAN_INT_PIN >= 0) {
// Interrupt-driven: check INT pin (active LOW)
frameAvailable = (digitalRead(CAN_INT_PIN) == LOW);
} else {
// Polling mode
frameAvailable = (CAN.checkReceive() == CAN_MSGAVAIL);
}
if (frameAvailable) {
uint32_t canId = 0;
uint8_t ext = 0;
uint8_t dlc = 0;
uint8_t buf[8] = {0};
if (CAN.readMsgBuf(&canId, &ext, &dlc, buf) == CAN_OK) {
frameCount++;
sendCanFrameToClients(canId, ext, dlc, buf);
// Debug to Serial
Serial.printf("[CAN] ID=0x%03X ext=%d dlc=%d data=", canId, ext, dlc);
for (uint8_t i = 0; i < dlc; i++) Serial.printf("%02X ", buf[i]);
Serial.println();
}
}
// ── Update display every 500 ms ────────────────────────
if (millis() - lastDisplayMs > 500) {
lastDisplayMs = millis();
updateDisplay();
}
// ── Button A (M5 button): reset frame counter ──────────
if (M5.BtnA.wasPressed()) {
frameCount = 0;
Serial.println("[BTN] Frame counter reset");
}
}
// ─────────────────────────────────────────────────────────
// WebSocket event handler
// ─────────────────────────────────────────────────────────
void onWebSocketEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) {
switch (type) {
case WStype_CONNECTED:
connectedClients++;
Serial.printf("[WS] Client #%d connected. Total: %d
", num, connectedClients);
// Send a hello/handshake message
{
StaticJsonDocument<128> hello;
hello["type"] = "hello";
hello["device"] = "M5StickCPlus2";
hello["version"] = "1.0.0";
hello["canSpeed"]= "500kbps";
String out;
serializeJson(hello, out);
webSocket.sendTXT(num, out);
}
break;
case WStype_DISCONNECTED:
if (connectedClients > 0) connectedClients--;
Serial.printf("[WS] Client #%d disconnected. Total: %d
", num, connectedClients);
break;
case WStype_TEXT:
// Handle commands from dashboard (e.g. {"cmd":"reset"})
{
StaticJsonDocument<128> cmd;
DeserializationError err = deserializeJson(cmd, payload, length);
if (!err) {
const char *c = cmd["cmd"];
if (c && strcmp(c, "reset") == 0) {
frameCount = 0;
Serial.println("[WS] Remote reset command received");
}
}
}
break;
default:
break;
}
}
// ─────────────────────────────────────────────────────────
// Build JSON payload and broadcast to all WS clients
// ─────────────────────────────────────────────────────────
void sendCanFrameToClients(uint32_t id, uint8_t ext, uint8_t len, uint8_t *buf) {
if (connectedClients == 0) return;
// Format: {"type":"can","ts":12345,"id":"7E8","ext":0,"dlc":8,"data":"0241000000000000"}
StaticJsonDocument<256> doc;
doc["type"] = "can";
doc["ts"] = millis();
doc["id"] = id;
doc["ext"] = (bool)ext;
doc["dlc"] = len;
// Encode data bytes as hex string
char hexBuf[17] = {0};
for (uint8_t i = 0; i < len && i < 8; i++) {
sprintf(hexBuf + i * 2, "%02X", buf[i]);
}
doc["data"] = hexBuf;
String out;
serializeJson(doc, out);
webSocket.broadcastTXT(out);
}
// ─────────────────────────────────────────────────────────
// Initial display layout
// ─────────────────────────────────────────────────────────
void initDisplay() {
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setTextColor(WHITE);
M5.Lcd.setCursor(4, 2);
M5.Lcd.setTextSize(1);
M5.Lcd.print("JeepCANDash");
M5.Lcd.drawFastHLine(0, 12, M5.Lcd.width(), DARKGREY);
}
// ─────────────────────────────────────────────────────────
// Refresh status display
// ─────────────────────────────────────────────────────────
void updateDisplay() {
// Clear data area only (below header line)
M5.Lcd.fillRect(0, 14, M5.Lcd.width(), M5.Lcd.height() - 14, BLACK);
// WiFi / IP
M5.Lcd.setCursor(4, 16);
if (WiFi.status() == WL_CONNECTED) {
M5.Lcd.setTextColor(GREEN);
M5.Lcd.printf("IP: %s", WiFi.localIP().toString().c_str());
} else {
M5.Lcd.setTextColor(RED);
M5.Lcd.print("WiFi: disconnected");
}
// WS port + clients
M5.Lcd.setCursor(4, 28);
M5.Lcd.setTextColor(CYAN);
M5.Lcd.printf("WS :%d clients:%d", WS_PORT, connectedClients);
// CAN status
M5.Lcd.setCursor(4, 40);
M5.Lcd.setTextColor(canReady ? GREEN : RED);
M5.Lcd.printf("CAN: %s", canReady ? "OK 500kbps" : "ERROR");
// Frame counter
M5.Lcd.setCursor(4, 52);
M5.Lcd.setTextColor(YELLOW);
M5.Lcd.printf("Frames: %lu", frameCount);
// Hint
M5.Lcd.setCursor(4, 64);
M5.Lcd.setTextColor(DARKGREY);
M5.Lcd.print("[A] reset counter");
}
After flashing, open the Device Connection page in this dashboard and enter ws://<device-ip>:81 as the WebSocket URL. The M5Stack display shows the assigned IP address once WiFi connects.