Arduino Firmware

M5Stack StickC Plus 2 · MCP2515 CAN Bus · WebSocket Bridge

Hardware

M5Stack StickC Plus 2

CAN Speed

500 kbps (OBD-II)

Transport

WebSocket :81

M5StickCPlus2M5Stack official

Board support + display/button drivers

mcp_cancoryjfowler/MCP_CAN_lib

MCP2515 SPI CAN controller driver

WebSocketsLinks2004/arduinoWebSockets

WebSocket server on ESP32

ArduinoJsonbblanchon/ArduinoJson v6.x

JSON serialisation for CAN frames

01

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".
02

Install Libraries

In Arduino IDE go to Sketch → Include Library → Manage Libraries and install all four dependencies listed above.
03

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.
04

Select Board & Port

Tools → Board → M5Stack → M5Stick-C-Plus2
Tools → Port → select the COM/tty port for your device.
05

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_M5StickCPlus2.ino
/*
 * 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.