SignalScope API (Dev) | Full Beginner Documentation | Bored Systems 

SignalScope API (Dev)

1) What This Is

  • Deterministic inline CAN gateway runtime.
  • HTTP API to observe, decode, mutate, replay, and load DBC.
  • Framework/runtime only (no product-specific CAN behavior baked into core).

2) What You Need

  • An ESP32 board with two CAN channels total.
  • PlatformIO (VSCode extension or CLI).
  • A USB cable and serial access.
  • Basic terminal use for copy/paste commands.

3) First 5-Minute Bring-Up

platformio run -t upload
platformio run -t uploadfs

# connect wifi:
# SSID: SignalScope-AP
# PASS: signalscope

curl "http://192.168.4.1/api/status"

API Basics (Important)

Type Used By What To Send Example
GET + query /api/status, /api/frame_cache, /api/signal_cache, /api/rules URL params only /api/frame_cache?limit=40
POST form /api/observe, /api/rules/stage, /api/rules/value, /api/replay/send application/x-www-form-urlencoded mode=specific&ids=0x280:A_TO_B
POST plain text /api/rules, /api/replay, /api/replay/load, /api/dbc text/plain apply_commit or CSV/DBC body text

IDs can be decimal (640) or hex (0x280). Direction values are A_TO_B or B_TO_A.

Endpoint Map (All Core Routes)

Endpoint Method Body Type Purpose
/api/statusGETnoneHealth, counters, recent frames, active/staged rules, DBC/replay state.
/api/frame_cacheGETnoneFrame cache keyed by (can_id,direction).
/api/signal_cacheGETnoneDecoded signal cache by numeric signal index.
/api/observePOSTformSet observation mode: none, specific, all.
/api/rules/stagePOSTformStage a BIT_RANGE or RAW_MASK rule.
/api/rulesPOSTtextRule action token: apply_commit, revert, clear_staging, clear_rules.
/api/rulesGETnoneList active rules and priorities.
/api/rules/valuePOSTformUpdate dynamic rule value by rule_id.
/api/rules/enablePOSTformEnable/disable rule by ID or identity fields.
/api/replay/loadPOSTtextLoad replay CSV text.
/api/replayPOSTtext or formStart/stop replay, set loop mode and start delay.
/api/replay/sendPOSTformCreate timed send frames quickly without external CSV.
/api/dbcPOSTtextUpload DBC text directly.
/api/dbc/autoloadPOSToptionalLoad first valid DBC from /dbc folder.
/api/mutations/stagePOSTformLegacy compatibility alias of /api/rules/stage.
/api/mutationsPOSTtextLegacy compatibility alias of /api/rules.
/api/mutations/togglePOSTformLegacy toggle route.

Request and Success Response Schemas

Use this as a contract reference. Examples below are representative success payloads from current handlers.

GET /api/status

Request: no body.

curl "http://192.168.4.1/api/status"

Success 200 (shape):

{
  "cpu_load_pct": 5,
  "bus_a_util_pct": 12,
  "bus_b_util_pct": 12,
  "bus_total_util_pct": 24,
  "bus_a_ready": true,
  "bus_b_ready": true,
  "ingress_a_frames": 123456,
  "ingress_b_frames": 123320,
  "rx_queue_depth": 0,
  "rx_drops_boot": 0,
  "rx_drops_run": 0,
  "forwarded_frames": 987654,
  "passive_fast_path_frames": 876543,
  "observed_decoded_frames": 55110,
  "active_mutations": 2,
  "staging_mutations": 0,
  "dbc_loaded": true,
  "dbc_message_count": 52,
  "dbc_signal_count": 190,
  "replay_frame_count": 0,
  "replay_playing": false,
  "frame_rate_fps": 240,
  "observation_mode": "specific",
  "decode_all": false,
  "fast_path_avg_us": 250,
  "active_path_avg_us": 750,
  "active_mutation_items": [
    {
      "rule_id": 3,
      "priority": 1,
      "active": true,
      "kind": "BIT_RANGE",
      "can_id": "0x280",
      "direction": "A_TO_B",
      "enabled": true,
      "dynamic": true,
      "start_bit": 0,
      "length": 8,
      "little_endian": true,
      "operation": "REPLACE",
      "replace_value": 42,
      "signal_name": "ExampleSignal"
    }
  ],
  "recent_frames": [
    {
      "id": "0x280",
      "can_id": 640,
      "dlc": 8,
      "direction": "A_TO_B",
      "timestamp_us": 123456789,
      "data": "00 01 02 03 04 05 06 07",
      "rate_hz": 100,
      "total_frames": 5555,
      "mutated": true,
      "message_name": "ExampleMessage",
      "decoded_signals": [
        {
          "index": 0,
          "name": "ExampleSignal",
          "value": 42.000,
          "start_bit": 0,
          "length": 8,
          "little_endian": true,
          "is_signed": false,
          "factor": 1.0,
          "offset": 0.0,
          "generation": 4321,
          "mutated": true
        }
      ]
    }
  ]
}

GET /api/frame_cache?limit=<n>

Request: optional query param limit (firmware caps this to status frame limit).

curl "http://192.168.4.1/api/frame_cache?limit=40"

Success 200:

{
  "ok": true,
  "count": 2,
  "frames": [
    {
      "can_id": 640,
      "direction": "A_TO_B",
      "dlc": 8,
      "timestamp_us": 123456789,
      "rate_hz": 100,
      "mutated": true,
      "data": "00 01 02 03 04 05 06 07"
    }
  ]
}

GET /api/signal_cache?indexes=0,1,2

Request: optional indexes CSV of numeric signal indexes.

curl "http://192.168.4.1/api/signal_cache?indexes=0,1,2"

Success 200:

{
  "ok": true,
  "count": 3,
  "signals": [
    {
      "index": 0,
      "can_id": 640,
      "name": "EngineTorque",
      "value": 145.2500,
      "generation": 987,
      "valid": true,
      "subscribed": true
    }
  ]
}

POST /api/observe

Request form fields: mode = none|specific|all, optional ids for specific.

curl -X POST "http://192.168.4.1/api/observe" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "mode=specific&ids=0x280:A_TO_B,0x1A0:B_TO_A"

Success 200:

{ "ok": true, "mode": "specific" }

Rule Endpoint Schemas

POST /api/rules/stage

BIT_RANGE request fields:

rule_kind=BIT_RANGE, can_id, direction, start_bit, length, replace_value, optional little_endian, dynamic, enabled.

RAW_MASK request fields:

rule_kind=RAW_MASK, can_id, direction, mask, value, optional enabled.

curl -X POST "http://192.168.4.1/api/rules/stage" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "rule_kind=BIT_RANGE&can_id=0x280&direction=A_TO_B&start_bit=0&length=8&replace_value=5&dynamic=1&enabled=1"

Success 200:

{ "ok": true, "rule_id": 3, "staging_count": 1 }

POST /api/rules

Body token: apply_commit | revert | clear_staging | clear_rules

curl -X POST "http://192.168.4.1/api/rules" \
  -H "Content-Type: text/plain" \
  --data-binary "apply_commit"

Success 200:

{ "ok": true, "action": "apply_commit" }

GET /api/rules

curl "http://192.168.4.1/api/rules"

Success 200:

{
  "ok": true,
  "count": 1,
  "rules": [
    {
      "rule_id": 3,
      "priority": 1,
      "active": true,
      "kind": "BIT_RANGE",
      "can_id": 640,
      "direction": "A_TO_B",
      "start_bit": 0,
      "length": 8,
      "dynamic": true,
      "replace_value": 5
    }
  ]
}

POST /api/rules/value

Request form fields: rule_id, value, optional enabled.

curl -X POST "http://192.168.4.1/api/rules/value" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "rule_id=3&value=42"

Success 200:

{ "ok": true }

POST /api/rules/enable

Request form fields: rule_id + enabled, or identity fields (can_id, direction, plus start_bit/length for bit rules).

curl -X POST "http://192.168.4.1/api/rules/enable" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "rule_id=3&enabled=0"

Success 200:

{ "ok": true }

Replay and DBC Endpoint Schemas

POST /api/replay/load

Request: plain-text CSV body, optional ?direction=A_TO_B query default.

curl -X POST "http://192.168.4.1/api/replay/load?direction=A_TO_B" \
  -H "Content-Type: text/plain" \
  --data-binary "0,0x280,8,00,01,02,03,04,05,06,07,A_TO_B"

Success 200:

{ "ok": true, "frames": 1 }

POST /api/replay

Text mode: body start, start LOOP_RAW, or stop.

Form mode: action=start|stop, optional loop_mode, start_delay_us.

curl -X POST "http://192.168.4.1/api/replay" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "action=start&loop_mode=LOOP_RAW&start_delay_us=50000"

Success 200 (start):

{ "ok": true, "action": "start", "start_delay_us": 50000 }

Success 200 (stop):

{ "ok": true, "action": "stop" }

POST /api/replay/send

Request form fields: can_id, direction, dlc, data, repeat, interval_us, start_delay_us, auto_start.

curl -X POST "http://192.168.4.1/api/replay/send" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "can_id=0x280&direction=A_TO_B&dlc=8&data=0001020304050607&repeat=10&interval_us=10000&start_delay_us=50000&auto_start=1"

Success 200:

{
  "ok": true,
  "frames": 10,
  "repeat": 10,
  "interval_us": 10000,
  "start_delay_us": 50000,
  "started": true
}

POST /api/dbc

Request: plain-text DBC body. If empty, runtime attempts /dbc/active.dbc fallback.

curl -X POST "http://192.168.4.1/api/dbc" \
  -H "Content-Type: text/plain" \
  --data-binary @"C:\\path\\your.dbc"

Success 200:

{ "ok": true, "messages": 52, "signals": 190 }

POST /api/dbc/autoload

curl -X POST "http://192.168.4.1/api/dbc/autoload"

Success 200:

{ "ok": true, "autoloaded": true, "messages": 52, "signals": 190 }

Observation + Decode

Decode is opt-in. Load a DBC, then choose observation mode:

  • mode=none: decode disabled.
  • mode=all: decode every observed frame path.
  • mode=specific: decode only specific can_id:direction keys.
curl -X POST "http://192.168.4.1/api/observe" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "mode=specific&ids=0x280:A_TO_B,0x1A0:B_TO_A"

curl "http://192.168.4.1/api/signal_cache?indexes=0,1,2"

Frame/Signal Polling

Use these to build your own app UI:

curl "http://192.168.4.1/api/frame_cache?limit=40"
curl "http://192.168.4.1/api/signal_cache"

Signal cache notes:

  • index: numeric signal index (fast for runtime).
  • generation: increments when value updates.
  • subscribed: whether signal is currently selected for decode flow.
  • valid: false until signal has been decoded at least once.

Rules: Exact Format and Workflow

Important: posting a rule is always 2 steps: stage then commit.

A) Stage a BIT_RANGE Rule

Required fields:

  • rule_kind=BIT_RANGE
  • can_id, direction
  • start_bit (0-63), length (1-64)
  • replace_value (or op_value1 fallback)

Optional fields and defaults:

  • little_endian default true
  • dynamic default false (set true for runtime value updates)
  • enabled default true
curl -X POST "http://192.168.4.1/api/rules/stage" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "rule_kind=BIT_RANGE&can_id=0x280&direction=A_TO_B&start_bit=0&length=8&replace_value=5&dynamic=1&enabled=1"

B) Stage a RAW_MASK Rule

Required fields:

  • rule_kind=RAW_MASK
  • can_id, direction
  • mask (8 bytes), value (8 bytes)

Hex parser accepts the first 16 hex nibbles and ignores separators. These both work:

  • mask=FF00FF00FF00FF00
  • mask=FF 00 FF 00 FF 00 FF 00
curl -X POST "http://192.168.4.1/api/rules/stage" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "rule_kind=RAW_MASK&can_id=0x280&direction=A_TO_B&mask=00000000000000FF&value=0000000000000001&enabled=1"

C) Commit, List, Update, Enable/Disable, Clear

# commit staged table (atomic swap)
curl -X POST "http://192.168.4.1/api/rules" \
  -H "Content-Type: text/plain" \
  --data-binary "apply_commit"

# list active rules and get rule_id
curl "http://192.168.4.1/api/rules"

# update dynamic value (rule_id range 0..95)
curl -X POST "http://192.168.4.1/api/rules/value" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "rule_id=3&value=42"

# disable or enable
curl -X POST "http://192.168.4.1/api/rules/enable" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "rule_id=3&enabled=0"

# other actions
# revert | clear_staging | clear_rules
curl -X POST "http://192.168.4.1/api/rules" -H "Content-Type: text/plain" --data-binary "clear_staging"

Replay/Send Timing (microseconds)

Quick timed send without writing CSV:

# every 10ms for 10 frames, start after 50ms
curl -X POST "http://192.168.4.1/api/replay/send" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "can_id=0x280&direction=A_TO_B&dlc=8&data=0001020304050607&repeat=10&interval_us=10000&start_delay_us=50000&auto_start=1"

repeat is clamped to 1..256. dlc is clamped to 0..8.

CSV Replay Control

# load CSV text
curl -X POST "http://192.168.4.1/api/replay/load?direction=A_TO_B" \
  -H "Content-Type: text/plain" \
  --data-binary "0,0x280,8,00,01,02,03,04,05,06,07,A_TO_B"

# start once
curl -X POST "http://192.168.4.1/api/replay" \
  -H "Content-Type: text/plain" \
  --data-binary "start"

# start looping raw
curl -X POST "http://192.168.4.1/api/replay" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data "action=start&loop_mode=LOOP_RAW&start_delay_us=50000"

# stop
curl -X POST "http://192.168.4.1/api/replay" -H "Content-Type: text/plain" --data-binary "stop"

Loop values: PLAY_ONCE, LOOP_RAW, LOOP_WITH_COUNTER_CONTINUATION.

DBC Upload

# upload local file directly as plain text
curl -X POST "http://192.168.4.1/api/dbc" \
  -H "Content-Type: text/plain" \
  --data-binary @"C:\\path\\to\\your.dbc"

After successful DBC load, runtime resets DBC-related state:

  • signal cache reset
  • subscriptions cleared
  • observation mode set to none
  • replay stopped
  • rules cleared

DBC Autoload

curl -X POST "http://192.168.4.1/api/dbc/autoload"

Boot/autoload priority:

  1. /dbc/active.dbc
  2. /dbc/default.dbc
  3. /dbc/vw_pq.dbc
  4. first valid *.dbc in /dbc

Browser JavaScript Examples (No Framework)

const BASE = "http://192.168.4.1";

async function getJson(path) {
  const r = await fetch(BASE + path);
  return r.json();
}

async function postForm(path, obj) {
  const body = new URLSearchParams(obj);
  const r = await fetch(BASE + path, {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body
  });
  return r.json();
}

async function postText(path, text) {
  const r = await fetch(BASE + path, {
    method: "POST",
    headers: { "Content-Type": "text/plain" },
    body: text
  });
  return r.json();
}

// Example: stage + commit rule
await postForm("/api/rules/stage", {
  rule_kind: "BIT_RANGE",
  can_id: "0x280",
  direction: "A_TO_B",
  start_bit: "0",
  length: "8",
  replace_value: "5",
  dynamic: "1"
});
await postText("/api/rules", "apply_commit");

Using SignalScope API with Other Boards (Exact Code to Edit)

You usually only edit main.cpp and platformio.ini. Keep core/ unchanged.

A) Pin Constants + Core Selector in main.cpp

constexpr int kBusARxPin = 6;
constexpr int kBusATxPin = 7;
constexpr int kMcpCsPin = 10;
constexpr int kMcpSclkPin = 12;
constexpr int kMcpMosiPin = 11;
constexpr int kMcpMisoPin = 13;
constexpr int kMcpRstPin = 9;

// true: CAN core1 / UI core0 (on dual core)
// false: both tasks pinned to core0
constexpr bool kUseDualCore = true;

B) CAN Bus Driver Glue in main.cpp (real code)

// These are real functions in SignalScope API main.cpp.
// Copy this pattern and replace with your board/backend specifics.

bool initBusA() {
    twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(
        static_cast<gpio_num_t>(kBusATxPin),
        static_cast<gpio_num_t>(kBusARxPin),
        TWAI_MODE_NORMAL);
    twai_timing_config_t t_config = TWAI_TIMING_CONFIG_500KBITS();
    twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();
    g_config.tx_queue_len = 64;
    g_config.rx_queue_len = 128;
    if (twai_driver_install(&g_config, &t_config, &f_config) != ESP_OK) return false;
    return twai_start() == ESP_OK;
}

bool initBusB() {
    SPI.begin(kMcpSclkPin, kMcpMisoPin, kMcpMosiPin, kMcpCsPin);
    if (can_mcp.reset() != MCP2515::ERROR_OK) return false;
    if (can_mcp.setBitrate(CAN_500KBPS) != MCP2515::ERROR_OK) return false;
    return can_mcp.setNormalMode() == MCP2515::ERROR_OK;
}

// Keep Direction mapping:
//   readBusA(...) sets out_frame.direction = Direction::A_TO_B
//   readBusB(...) sets out_frame.direction = Direction::B_TO_A
//   txDriver(...) routes A_TO_B to writeBusB, and B_TO_A to writeBusA

C) Build Target in platformio.ini

[env:your-board]
platform = https://github.com/pioarduino/platform-espressif32.git#55.03.36
board = your_board_id
framework = arduino

board_build.flash_size = 16MB
board_build.filesystem = littlefs
board_build.partitions = partitions.csv

D) Validate Port

curl "http://192.168.4.1/api/status"

# check these fields:
# bus_a_ready == true
# bus_b_ready == true
# ingress_a_frames / ingress_b_frames rising
# rx_drops_run not climbing in steady state

Common API Errors (and What They Mean)

Error Where Typical Cause
invalid_mask_or_value/api/rules/stageMask/value did not contain at least 16 hex nibbles.
unknown_action/api/rulesBody token was not one of the supported action strings.
invalid_rule_id/api/rules/value, /api/rules/enableRule id missing/out of range and identity lookup failed.
rule_not_found/api/rules/value, /api/rules/enableRule id exists in request but not in engine state.
empty_replay_body/api/replay/loadNo CSV text in request body.
replay_empty/api/replay startReplay was started before loading frames.
invalid_data_bytes/api/replay/senddata payload was not valid hex.
empty_dbc_body_or_upload/api/dbcNo plain-text DBC body and no /dbc/active.dbc fallback.
dbc_parse_failed/api/dbcDBC text is invalid.
no_valid_dbc_in_folder/api/dbc/autoloadNo valid .dbc found in /dbc.
© Bored Systems
Theme Settings
Color Scheme
Light
Dark
Layout Mode
Fluid
Boxed
Topbar Color
Light
Dark
Menu Color
Light
Dark
Layout Position