Writing Vehicle and ECU Specifications

ECUmulator Studio · Authoring Guide · September 2026

This guide takes you from three editable JSON files to one simulated ECU with UDS and OBD-II handlers. It describes the shipped native runtime and Studio; no Python installation is required. The demo is educational, not a reproduction of a real vehicle. Its VIN is a fictitious 17-character test string.

1. Start with the included demo

Choose Help → Load Demo Vehicle → Pilot Authoring Demo — UDS + OBD-II. The menu label may also include the file path. Expect one logical ECU, POWERTRAIN, with two diagnostic handlers.

FilePurpose
pilot-vehicle.jsonVehicle name, VIN and references to both handlers
pilot-uds.jsonUDS addresses, timing and identification DIDs
pilot-obd2.jsonOBD-II addresses, 24 live PIDs, freeze-frame data and VIN

The macOS installer installs reference files under /usr/local/cansole/share/specs/pilot/. Copy the whole directory before editing:

mkdir -p "$HOME/Documents/ECUmulator"
cp -R /usr/local/cansole/share/specs/pilot "$HOME/Documents/ECUmulator/pilot"

Keep all three files together and open your copied vehicle file in Studio. Reload after making external text edits. The same demo is bundled inside the application for offline use; do not edit files inside the application bundle. The appendices contain the complete files if you need to recreate them.

Other included reference vehicles

The catalog covers distinct protocol/addressing combinations:

ExamplePurpose
Pilot Authoring Demo — UDS + OBD-IIShared 11-bit ECU with two handlers; this tutorial
OBD2 11-Bit Spark DemoStandalone OBD-II, 11-bit CAN, two ECUs
OBD2 29-Bit Diesel DemoStandalone OBD-II, 29-bit CAN, two ECUs
UDS Demo VehicleStandalone UDS
Porsche PDK KWP FlashKWP over ISO-TP; advanced session/flash example
VW EDC16CP34 TP2.0KWP over TP2.0, with explicit transport configuration

The test-fixture collection in the source repository is larger. FD smoke tests and duplicate regression scenarios are not part of the customer catalog.

2. Vehicle, ECU and handler

A vehicle contains named ECU entries. Each source file describes one diagnostic protocol handler. Entries with the same vehicle-level name belong to one logical ECU:

{
  "name": "Pilot Authoring Demo — UDS + OBD-II",
  "vin": "PILOT000000000001",
  "ecus": [
    { "name": "POWERTRAIN", "path": "pilot-uds.json" },
    { "name": "POWERTRAIN", "path": "pilot-obd2.json" }
  ]
}

Both entries deliberately use POWERTRAIN. A different name describes a different logical ECU. The name inside each ECU source file is a descriptive label.

Paths are relative to the vehicle file, not the terminal directory. Studio and ecp load resolve the files and upload their contents as a bundle; the target does not need access to your original paths. Loading an individual ECU file works too, but loads only that handler.

Inline vehicle formats are also supported. This guide uses separate files for easy reuse; Studio saves or exports may use a resolved representation instead.

The vehicle VIN and protocol responses are separate data. Keep the vehicle VIN, UDS DID F190, the OBD-II vin field and its Mode 09 VIN bytes consistent. Changing one field does not guarantee the others change.

3. JSON rules and comments

Use UTF-8 strict JSON. JSON5 is not supported: no // or /* */ comments, trailing commas, single quotes, or unquoted hexadecimal literals. Quote hexadecimal IDs as strings:

{ "request": "0x7E0", "reply": "0x7E8" }

PID objects can contain ordinary comment string fields, as in our examples:

{
  "id": "0x0C",
  "comment": "Engine speed: raw value divided by 4 = 2000 rpm.",
  "simple": "1F 40"
}

These fields explain the data and do not affect runtime responses. They are not JSON comment syntax. Do not add unknown fields indiscriminately: some schema objects reject them. An editor/export operation may also omit fields it does not model. Keep your annotated source files and this guide together.

4. Addresses and diagnostic payloads

Both handlers use physical request ID 0x7E0 and reply ID 0x7E8. The OBD-II handler additionally accepts functional request ID 0x7DF. The UDS auxiliary list is empty in this example.

FieldMeaning
requestCAN arbitration ID sent by the tester
replyCAN arbitration ID emitted by the ECU
auxiliaryAdditional request IDs, such as functional requests

The handlers share addresses but own different services: OBD-II Modes 01–0A and UDS services such as 22. They form one ECU rather than independent responders that reject each other's requests. If you add another ECU, choose its physical address pair deliberately; renaming it alone does not change its address.

Spec values contain data, not CAN/ISO-TP headers:

The first CAN byte is the payload length. The runtime adds the service/PID prefix and ISO-TP framing. Long responses, such as VINs, require flow control from the tester. This demo uses Classic CAN, not CAN-FD.

5. UDS identification DIDs

A UDS file has kind: "uds" and a matching uds object. Its identifiers array defines ReadDataByIdentifier (service 22) responses:

{
  "id": "0xF187",
  "name": "Part Number",
  "type": "ascii",
  "content": "PILOT-ECU-01"
}

Request payload 22 F1 87 returns 62 F1 87 followed by the ASCII content. Do not include that response prefix in content. The demo supplies VIN (F190), part number (F187), hardware version (F191) and software version (F195). Use distinct DID IDs when adding entries.

The complete demo contains 16 DIDs: ten text identifiers and six binary measurements/status values. F100–F105 are demo-defined, not standardized measurement definitions. F100 (rpm), F101 (coolant), F102 (vehicle speed) and F103 (voltage) mirror the corresponding OBD-II values.

For binary content, use a bytes field rather than putting hex text in content:

{
  "id": "0xF100",
  "name": "Demo Engine Speed",
  "bytes": "1F 40"
}

Request 22 F1 00 returns 62 F1 00 1F 40. Here content: "1F 40" would encode the text characters instead of those two bytes. Use either content or bytes.

The timing fields p2ServerMax, p2extServerMax and s3SessionTimeout are in seconds. The demo uses 0.05, 2.0 and 5.0 respectively.

Security levels, routines, memory layout, DTCs and startup actions are empty. No seed/key prerequisite is needed for the identification examples. Extend these features only when your test scenario needs them; Studio's other help topics cover their configuration. Labels explain a DID; the ID and content determine the actual response.

6. OBD-II live PIDs

obd2.frames[0].pids supplies Mode 01 current data. Give every PID a unique ID within its frame and exactly one source: simple, sequence or dynamic.

A simple value is a hex byte string containing only PID data. For example, "32" means the single byte 0x32 (decimal 50), not the characters “3” and “2”. In the table, A and B denote the first and second bytes.

PIDExample meaningBytes / decoding
01Monitor status, MIL off00 07 FF 00
03Fuel system 1 closed loop02 00
04Load ≈ 50.2%80; A × 100 / 255
05Coolant 90 °C82; A − 40
06Short-term trim 0%80; (A − 128) × 100 / 128
07Long-term trim 0%80; same encoding
0AFuel pressure 300 kPa64; A × 3
0BManifold pressure 100 kPa64; A
0CEngine speed 2000 rpm1F 40; (256A + B) / 4
0DVehicle speed 50 km/h32; A
0ETiming advance 0°80; A / 2 − 64
0FIntake air 25 °C41; A − 40
10Air flow 25 g/s09 C4; (256A + B) / 100
11Throttle ≈ 25.1%40; A × 100 / 255
1COBD compliance example01
1FRun time 10, 20, 30 secondsSequence
2FTank level 25–75%Dynamic
33Barometric pressure 100 kPa64; A
42Module voltage 14 V36 B0; (256A + B) / 1000
46Ambient temperature 20 °C3C; A − 40
51Gasoline fuel type01
F1Custom lab integer: 466012 34; unsigned big-endian
F2Custom lab text: DEMO44 45 4D 4F; ASCII
F3Custom lab status: 0, 1, 2Sequence

Supported-PID bitmap requests (00, 20, etc.) are handled by the runtime; do not maintain bitmap bytes manually here. Monitor status and compliance fields are fixed sample values, not an automatic model of the engine or fault state.

Custom OBD-II PIDs

The runtime accepts custom PID entries in the same pids array. For example:

{
  "id": "0xF1",
  "comment": "Custom lab value: unsigned big-endian integer 4660.",
  "simple": "12 34"
}

Mode 01 request 01 F1 returns 41 F1 12 34. The runtime includes configured PIDs in its supported-PID bitmaps, including the E0 bitmap for these examples.

Rules:

Sequences change on reads

{
  "id": "0x1F",
  "sequence": {
    "responses": ["00 0A", "00 14", "00 1E"],
    "repeat": "loop"
  }
}

Successive reads return 10, 20 and 30 seconds, then repeat. This is a repeatable display test, not an elapsed-time counter. Other clients polling that PID advance the sequence too.

Generators change with time

{
  "id": "0x2F",
  "dynamic": {
    "function": "sawtooth",
    "min": 25,
    "max": 75,
    "period": 60,
    "encoding": { "type": "uint8", "scale": 2.55 }
  }
}

This generator varies a physical percentage over 60 seconds. Encoding performs the inverse of the tester's decoding: raw = percentage × 255 / 100. Integer encoding quantizes the result. Sawtooth wraps at the end of the cycle. Use a curve for more elaborate patterns; see Dynamic Values in the help.

Do not assume a first read starts at the beginning of the cycle. Use static values for exact response checks.

7. Freeze frames and VIN

frames[0] holds live values. frames[1] supplies the first Mode 02 freeze frame (request index 00): coolant at 50 °C and engine speed at 1000 rpm. These are configured snapshot values, not an automatic capture when a DTC occurs.

The informational array supplies four Mode 09 values: VIN (02), calibration ID (04), a demonstration calibration verification number (06) and ECU name (0A). The VIN value contains a count byte (01) followed by its 17 ASCII bytes. The calibration ID has one 16-byte ASCII item; the CVN is four fixed sample bytes, not a checksum calculated from the ECU data. Keep the VIN consistent with the UDS VIN. The tutorial regression test compares all four VIN source representations and both diagnostic responses byte for byte.

Other OBD-II service arrays and DTCs are empty. The demo does not model oxygen sensor tests, on-board monitor results, control operations or fault lifecycles.

8. Load and test

Studio

Open the copied vehicle wrapper, confirm POWERTRAIN with two handlers, and enable the Internal Bus. Select and enable External CAN for a physical bench. Set the adapter's bit rate to match your tester.

Native CLI: direct bridge

Open a new Terminal window after installing Studio so ecum and ecp are on PATH.

Activate your pilot license in Studio first, or run ecum --license-import /path/to/customer.license as the same OS user. Check activation with ecum --license-status. A remote runtime needs a license on its own host; Studio does not transmit your license when connecting.

ecum --vehicle "$HOME/Documents/ECUmulator/pilot/pilot-vehicle.json" --socketcan gs_usb

The direct bridge uses 500 kbit/s Classic CAN and stops with Ctrl-C. The short selector chooses the first gs_usb adapter. Narrow it to gs_usb:1d50:606f or use a concrete inventory ID if several match. Other backend names include toucan, openport, peak_usb_fd and socketcan (Linux).

Native CLI: control server

In the first terminal:

ecum --control-host 127.0.0.1 --control-port 29190

In a second terminal:

ecp --host 127.0.0.1 --port 29190 load "$HOME/Documents/ECUmulator/pilot/pilot-vehicle.json"
ecp --host 127.0.0.1 --port 29190 can-interfaces
ecp --host 127.0.0.1 --port 29190 patch externalCan '{"interface":"gs_usb","bitrate":500000,"canFd":false,"enabled":true}'

Direct bridge and control server are separate modes: do not combine --socketcan and --control-port. ecp controls the runtime; it is not a diagnostic CAN tester.

Expected responses

These are diagnostic payloads without ISO-TP framing. Send physical requests on 0x7E0 and receive on 0x7E8.

RequestExpected responseCheck
01 0541 05 82Live coolant: 90 °C
01 0C41 0C 1F 40Engine speed: 2000 rpm
01 0D41 0D 32Vehicle speed: 50 km/h
01 1041 10 09 C4Air flow: 25 g/s
01 4241 42 36 B0Module voltage: 14 V
01 F141 F1 12 34Custom lab integer: 4660
01 F241 F2 44 45 4D 4FCustom lab text: DEMO
22 F1 0062 F1 00 1F 40UDS engine speed: 2000 rpm
02 0C 0042 0C 00 0F A0Freeze frame: 1000 rpm
22 F1 9162 F1 91 48 57 30 31Hardware version: HW01
22 F1 9062 F1 90 + 17 ASCII bytesUDS VIN
09 0249 02 01 + 17 ASCII bytesOBD-II VIN

Mode 02 responses echo the requested freeze-frame index after the PID, before the snapshot data: 42 PID FRAME DATA….

Without hardware, inject a complete Classic CAN single frame:

ecum --vehicle "$HOME/Documents/ECUmulator/pilot/pilot-vehicle.json" --request '7E0#0201050000000000'

Expect a reply containing 03 41 05 82. The request's leading 02 is the ISO-TP length. Long responses need flow control, so use an ISO-TP tester for VIN checks.

9. Create a variant

  1. Copy all three files.
  2. Change the vehicle name and logical ECU names.
  3. Adjust request and reply IDs together.
  4. Update all VIN representations.
  5. Change one static DID/PID and verify its exact response.
  6. Add sequences or generators after the static version works.
  7. Reload and repeat the checks after every edit.

Keep an untouched demo copy to distinguish transport faults from spec changes. Keep encoding formulas beside non-obvious values. A small verified scenario is a better starting point than many unverified fields.

10. Troubleshooting

SymptomFirst checks
JSON fails to loadQuotes, commas, brackets; no JSON5; inspect the reported file
ECU file missingCopy all files; paths are relative to the vehicle
Two separate ECUsVehicle entries must have exactly the same name
No diagnostic replyLoaded handler, enabled ECU/bus, adapter, bit rate, request ID
VIN stops after first frameTester must send ISO-TP flow control
Incorrect valueRaw bytes vs. physical units; scale and byte order
Unexpected response prefixDo not include service/PID/DID or ISO-TP headers in data
Sequence skips valuesAnother client may be polling it
File edit has no effectReload/apply the edited vehicle; check the active target
No CAN frame console traceDirect-bridge frame logs are Debug; normal output is Info

Printing and sharing

Choose Save printable guide… in this help topic. Open the saved HTML in a browser, choose Print, and select Save as PDF on macOS. It contains the complete guide and JSON appendices, works offline, and supports A4 and Letter. Application navigation is not printed.

Complete example files

The following appendices are generated from the actual shipped demo files. Copy their contents exactly, including the file names.

Appendix A: pilot-vehicle.json

{
  "name": "Pilot Authoring Demo \u2014 UDS + OBD-II",
  "vin": "PILOT000000000001",
  "ecus": [
    {
      "name": "POWERTRAIN",
      "path": "pilot-uds.json"
    },
    {
      "name": "POWERTRAIN",
      "path": "pilot-obd2.json"
    }
  ]
}

Appendix B: pilot-uds.json

{
  "name": "Pilot Powertrain UDS",
  "kind": "uds",
  "uds": {
    "arbitrationInfo": {
      "request": "0x7E0",
      "reply": "0x7E8",
      "auxiliary": []
    },
    "p2ServerMax": 0.05,
    "p2extServerMax": 2.0,
    "s3SessionTimeout": 5.0,
    "identifiers": [
      {
        "id": "0xF190",
        "name": "VIN",
        "type": "ascii",
        "content": "PILOT000000000001"
      },
      {
        "id": "0xF187",
        "name": "Part Number",
        "type": "ascii",
        "content": "PILOT-ECU-01"
      },
      {
        "id": "0xF191",
        "name": "Hardware Version",
        "type": "ascii",
        "content": "HW01"
      },
      {
        "id": "0xF195",
        "name": "Software Version",
        "type": "ascii",
        "content": "SW01"
      },
      {
        "id": "0xF180",
        "name": "Boot Software Identification",
        "type": "ascii",
        "content": "PILOT-BOOT-1.0"
      },
      {
        "id": "0xF181",
        "name": "Application Software Identification",
        "type": "ascii",
        "content": "PILOT-APP-1.0"
      },
      {
        "id": "0xF182",
        "name": "Application Data Identification",
        "type": "ascii",
        "content": "PILOT-CAL-2026"
      },
      {
        "id": "0xF18C",
        "name": "ECU Serial Number",
        "type": "ascii",
        "content": "PILOT-SERIAL-0001"
      },
      {
        "id": "0xF197",
        "name": "System Name",
        "type": "ascii",
        "content": "PILOT POWERTRAIN"
      },
      {
        "id": "0xF19E",
        "name": "Configuration Identification",
        "type": "ascii",
        "content": "PILOT-CONFIG-A"
      },
      {
        "id": "0xF100",
        "name": "Demo Engine Speed",
        "comment": "Demo-defined: unsigned big-endian raw / 4 = 2000 rpm; same value as OBD PID 0C.",
        "bytes": "1F 40"
      },
      {
        "id": "0xF101",
        "name": "Demo Coolant Temperature",
        "comment": "Demo-defined: raw - 40 = 90 degrees C; same value as OBD PID 05.",
        "bytes": "82"
      },
      {
        "id": "0xF102",
        "name": "Demo Vehicle Speed",
        "comment": "Demo-defined: raw = 50 km/h; same value as OBD PID 0D.",
        "bytes": "32"
      },
      {
        "id": "0xF103",
        "name": "Demo Module Voltage",
        "comment": "Demo-defined: unsigned big-endian raw / 1000 = 14 V; same value as OBD PID 42.",
        "bytes": "36 B0"
      },
      {
        "id": "0xF104",
        "name": "Demo Status Bits",
        "comment": "Demo-defined bit mask: bits 0 and 2 set.",
        "bytes": "05"
      },
      {
        "id": "0xF105",
        "name": "Demo Counter",
        "comment": "Demo-defined unsigned big-endian 32-bit counter: 123456.",
        "bytes": "00 01 E2 40"
      }
    ],
    "securityLevels": [],
    "routines": [],
    "memoryLayout": [],
    "dtcs": [],
    "startupActions": []
  }
}

Appendix C: pilot-obd2.json

{
  "name": "Pilot Powertrain OBD-II",
  "kind": "obd2",
  "obd2": {
    "arbitrationInfo": {
      "request": "0x7E0",
      "reply": "0x7E8",
      "auxiliary": [
        "0x7DF"
      ]
    },
    "vin": "PILOT000000000001",
    "frames": [
      {
        "pids": [
          {
            "id": "0x01",
            "comment": "Monitor status: MIL off, zero stored DTC count.",
            "simple": "00 07 FF 00"
          },
          {
            "id": "0x03",
            "comment": "Fuel system 1 closed loop.",
            "simple": "02 00"
          },
          {
            "id": "0x04",
            "comment": "Engine load: A * 100 / 255 = approximately 50.2%.",
            "simple": "80"
          },
          {
            "id": "0x05",
            "comment": "Coolant: A - 40 = 90 degrees C.",
            "simple": "82"
          },
          {
            "id": "0x06",
            "comment": "Short-term fuel trim: (A - 128) * 100 / 128 = 0%.",
            "simple": "80"
          },
          {
            "id": "0x07",
            "comment": "Long-term fuel trim: 0%.",
            "simple": "80"
          },
          {
            "id": "0x0A",
            "comment": "Fuel pressure: A * 3 = 300 kPa.",
            "simple": "64"
          },
          {
            "id": "0x0B",
            "comment": "Manifold pressure: A = 100 kPa.",
            "simple": "64"
          },
          {
            "id": "0x0C",
            "comment": "Engine speed: (256*A + B) / 4 = 2000 rpm.",
            "simple": "1F 40"
          },
          {
            "id": "0x0D",
            "comment": "Vehicle speed: A = 50 km/h.",
            "simple": "32"
          },
          {
            "id": "0x0E",
            "comment": "Timing advance: A / 2 - 64 = 0 degrees.",
            "simple": "80"
          },
          {
            "id": "0x0F",
            "comment": "Intake air: A - 40 = 25 degrees C.",
            "simple": "41"
          },
          {
            "id": "0x10",
            "comment": "Air flow: (256*A + B) / 100 = 25 g/s.",
            "simple": "09 C4"
          },
          {
            "id": "0x11",
            "comment": "Throttle: A * 100 / 255 = approximately 25.1%.",
            "simple": "40"
          },
          {
            "id": "0x1C",
            "comment": "OBD-II compliance example: CARB.",
            "simple": "01"
          },
          {
            "id": "0x33",
            "comment": "Barometric pressure: A = 100 kPa.",
            "simple": "64"
          },
          {
            "id": "0x42",
            "comment": "Module voltage: (256*A + B) / 1000 = 14 V.",
            "simple": "36 B0"
          },
          {
            "id": "0x46",
            "comment": "Ambient temperature: A - 40 = 20 degrees C.",
            "simple": "3C"
          },
          {
            "id": "0x51",
            "comment": "Fuel type: gasoline.",
            "simple": "01"
          },
          {
            "id": "0x1F",
            "comment": "Successive reads return 10, 20, 30 seconds and repeat; not elapsed time.",
            "sequence": {
              "responses": [
                "00 0A",
                "00 14",
                "00 1E"
              ],
              "repeat": "loop"
            }
          },
          {
            "id": "0x2F",
            "comment": "Tank level: 25-75 percent over 60 seconds. Raw = percentage * 2.55.",
            "dynamic": {
              "function": "sawtooth",
              "min": 25,
              "max": 75,
              "period": 60,
              "encoding": {
                "type": "uint8",
                "scale": 2.55
              }
            }
          },
          {
            "id": "0xF1",
            "comment": "Custom lab PID, not a standard measurement: unsigned big-endian demo value 4660. Tester must know this definition.",
            "simple": "12 34"
          },
          {
            "id": "0xF2",
            "comment": "Custom lab PID: four ASCII bytes spelling DEMO. Tester must know this definition.",
            "simple": "44 45 4D 4F"
          },
          {
            "id": "0xF3",
            "comment": "Custom lab PID: one-byte status sequence 0, 1, 2, then repeat.",
            "sequence": {
              "responses": [
                "00",
                "01",
                "02"
              ],
              "repeat": "loop"
            }
          }
        ]
      },
      {
        "pids": [
          {
            "id": "0x05",
            "comment": "Freeze-frame coolant: 50 degrees C.",
            "simple": "5A"
          },
          {
            "id": "0x0C",
            "comment": "Freeze-frame engine speed: 1000 rpm.",
            "simple": "0F A0"
          }
        ]
      }
    ],
    "informational": [
      {
        "id": "0x02",
        "comment": "One item (01), followed by 17 VIN ASCII bytes. Keep consistent with UDS VIN.",
        "simple": "01 50 49 4C 4F 54 30 30 30 30 30 30 30 30 30 30 30 31"
      },
      {
        "id": "0x04",
        "comment": "Mode 09 calibration ID: one item (01), followed by a 16-byte ASCII ID.",
        "simple": "01 50 49 4C 4F 54 2D 43 41 4C 2D 30 30 30 30 30 31 20"
      },
      {
        "id": "0x06",
        "comment": "Mode 09 calibration verification number: one item, then four demonstration bytes. Not a computed checksum.",
        "simple": "01 12 34 56 78"
      },
      {
        "id": "0x0A",
        "comment": "Mode 09 ECU name; demonstration ASCII text.",
        "simple": "50 49 4C 4F 54 20 50 4F 57 45 52 54 52 41 49 4E"
      }
    ],
    "oxygenSensorMonitoring": [],
    "onBoardMonitoring": [],
    "controlOperations": [],
    "dtcs": []
  }
}