Mobility Model Zoo
Automotive security · can-ids experimental · v0.2.0

picket-forest

CAN intrusion detector for microcontrollers, calibrated on your vehicle's normal traffic.

For researchers, students and embedded engineers who study in-vehicle network security on a test bench and want an open, measured detector.

Experimental: not ready for production use. See the limits.

Licence Apache-2.0 · commercial Output picket-forest-v2

Why use it

  • Calibrated on the vehicle

    Learns normal frame counts per identifier from benign traffic of the target vehicle and judges every frame against them.

  • Small enough for a microcontroller

    Plain C: 16.5 KiB1 of flash and 11.94 KiB2 of memory on an ESP32 (emulator); C and Python agree.

  • Measured on unseen vehicles

    At worst 0.8613 false alarms per hour on unknown vehicles of the benchmark.

  • Open and reproducible

    Code, features and thresholds are public; the release gate reproduces every example result from the C code.

Not for

  • Vehicles on public roads; it is not a validated or approved security mechanism
  • Active countermeasures such as blocking or injecting frames
  • CAN FD or automotive Ethernet

All limits

Quality and speed

How good, how fast, measured on a frozen benchmark and named hardware.

Benchmark can-ids-v3 (frozen)
Attack-class F1 on ROAD
0.99684
Measured against ROAD injection labels, benchmark can-ids-v3.
Worst false alarms per hour, unknown vehicles
0.8613
Measured against can-train-and-test attack labels (official splits), benchmark can-ids-v3.
Flash (ESP32, emulator)
16.5 KiB1
Measured on ESP32 in Espressif QEMU, 240 MHz nominal.
Memory (ESP32, emulator)
11.94 KiB2
Measured on ESP32 in Espressif QEMU, 240 MHz nominal.

All metrics of version 0.2.0, with what each one is measured against.

Limits and status

Where to stop trusting it.

Experimental: not ready for production use.

Not met yet

  • Shuffled-label control of the pre-registered evaluation: failed; the calibrated features are themselves anomaly signals
  • Budgets measured on the reference board: the numbers of this release come from an emulator, not a board

Known limits

  • False alarms depend on the calibration: it must cover driving, idle, parked and diagnostics.
  • A legitimate identifier missing from the calibration floods alarms.
  • Masquerade attacks are not detected, and there is no adversarial evaluation.

Evidence: How it is built and measured · Results of v0.2.0 · Release notes · Compliance record of v0.2.0

Intended use

Research, teaching and bench testing of in-vehicle intrusion detection on classic CAN; a reproducible baseline for calibrated, microcontroller-sized CAN intrusion detection; a starting point for a security sensor that reports events to an IDS manager. Status: experimental, not ready for production use (see limitations).

Out of scope

Deployment in vehicles on public roads: the model is not a validated or approved safety or security mechanism and makes no ISO/SAE 21434 or UNECE R155 claim; no safety-critical use without a vehicle-specific validation. Active countermeasures (blocking or injecting frames) based on its output. CAN FD and automotive Ethernet.

All limitations
  • Not ready for production use: release status experimental (constitution 2.2.0).
  • False alarms depend on the calibration: on ROAD's test drives 0 per hour with 142 min of calibration, about 15 per hour with 60 min, 87 with 15 min and 140 with 5 min. A calibration must cover the vehicle's operating modes (driving, idle, parked, diagnostics).
  • One new legitimate identifier after calibration floods alarms: on the AutoHack C-CAN bus one identifier missing from the calibration caused several hundred false alarms per hour; a diagnostic dongle plugged in after calibration causes about 1,100 per hour. After a fuzzing burst the unknown-identifier state can keep alarming for up to 2 s (see example 03).
  • The rules decide. The trained forest is reported as information only: on unseen vehicles it raised up to 19.6 false alarms per hour and missed injected fabrication attacks that the rules catch.
  • The shuffled-label control failed its pre-registered rule (the calibrated features are themselves anomaly signals); the test captures of can-train-and-test and ROAD were scored several times during development (all logged in the repository, feature 010).
  • Evidence on unseen vehicles: can-train-and-test unknown-vehicle splits (at most 0.86 false alarms per hour), KCID Ford Focus 4.9 h and Traverse 22 h without an alarm (the KCID Honda CR-V log shows only 6 identifiers behind a gateway and is weak evidence). Device numbers come from an emulator (ESP32 in QEMU); latency there is not representative.
  • Masquerade (a silenced ECU replaced at its normal timing) is not detected. Classic CAN only; no CAN FD.
  • No adversarial evaluation: an attacker who knows the rules can stay below the calibrated counts. The code, features and thresholds are public; do not rely on it as the only line of defence.
  • Calibration is done on the host and compiled into the device; on-device calibration is not implemented.
Quickstart

Install, calibrate, detect.

Needs a host with Python to build the calibration table and an ESP32-class microcontroller (or the host build) to run the detector. Classic CAN only.

  1. Install version 0.2.0

    Requires Python 3.12 or newer. New here? Start with the five-minute quickstart.

    shell
    pip install "mobility-model-zoo[edge] @ git+https://github.com/mhabedank/mobility-model-zoo@picket-forest/v0.2.0"
  2. Run it on CAN frames

    python
    from huggingface_hub import hf_hub_download
    
    from mobility_model_zoo.security.can_ids import picket_forest as pf
    
    model = pf.load_model(hf_hub_download("mobility-model-zoo/picket-forest", "model.joblib", revision="v0.2.0"))
    table = pf.load_table(hf_hub_download("mobility-model-zoo/picket-forest", "calibration_table.json", revision="v0.2.0"))
    capture = pf.load_capture(hf_hub_download("mobility-model-zoo/picket-forest", "examples/01-fabrication.json", revision="v0.2.0"))
    scores, alarms = pf.score(capture, table, model)
    print(pf.summary(capture, scores, alarms))
  3. Get this output

    For the example 01-fabrication.json, the released C code returns:

    json · expected result of v0.2.0, reproduced bit-exactly by the release gate
    {
      "expected_alarms": [
        1155
      ]
    }
  4. Use it on your own data

    python
    from mobility_model_zoo.security.can_ids import picket_forest as pf
    
    # candump logs (candump -L) of benign traffic of your vehicle: driving, idle and parked, 60 minutes or more
    benign = [pf.read_candump(path) for path in ["drive.log", "idle.log", "parked.log"]]
    table = pf.calibrate(benign)
    pf.save_table(table, "my_vehicle_table.json")  # use it in place of calibration_table.json above
    # For the device, `zoo-security can-ids v2 table drive.log idle.log parked.log` also writes picket_forest_table.h

Interface and examples

Interface

What goes in, what comes out.

Calibration (host, once per vehicle): benign candump logs of the vehicle -> picket_forest_table.h for the device and calibration_table.json for the Python reference (zoo-security can-ids v2 table); cover driving, idle and parked phases, at least 60 minutes. Input (device): one classic CAN frame at a time (timestamp in microseconds, 11-bit identifier, DLC, up to 8 data bytes). Output: the rules decision per frame (count above the calibrated maximum by more than 25 %, unknown-identifier burst, DLC change), an alarm flag from the alarm stage (3 flagged frames within 200 ms, then 1 s hold-off) and the forest score (share of trees voting attack) as information.

Input

  • Classic CAN frames of one vehicle, one at a time and in bus order: timestamp in microseconds, 11-bit identifier, DLC and up to eight data bytes. CAN FD and extended identifiers are not supported.
  • The calibration table of the same vehicle (picket_forest_table.h), made on the host from benign traffic of that vehicle before use (at least 60 minutes covering driving, idle and parked phases).
  • On the device, picket_forest_v2_process() takes one frame and returns 1 if that frame raises an alarm; it can also hand out the forest score and the rules decision of the frame.

Guarantees

  • expected_scores has one entry per frame, in the order of frames.

  • expected_alarms lists the indices of the frames that raise an alarm, ascending; every index is a position in frames.

  • The rules on the calibrated features decide; the forest score is information and does not raise an alarm by itself.

  • The C code of the release and the Python reference give identical scores and alarms for every frame (checked bit-exactly by the release gate).

Worked examples

The released C code reproduces every result exactly.

Inputs and expected results of version 0.2.0, as published with the release. The release gate reproduces every result bit-exactly with the C code of this version. The inputs are synthetic.

Example 1: 01-fabrication.json

Synthetic traffic with an injected fabrication attack.

input · first 5 of 3000 frames: [time in µs, ID, DLC, data]
[0, 270, 8, [0, 32, 16, 16, 0, 0, 14, 78]]
[3295, 284, 8, [0, 129, 64, 16, 0, 0, 28, 237]]
[3455, 1285, 8, [0, 132, 66, 16, 0, 0, 5, 219]]
[4514, 845, 8, [0, 16, 8, 16, 0, 0, 77, 117]]
[6487, 1551, 8, [0, 10, 5, 16, 0, 0, 15, 46]]
frames that raise an alarm
{
  "expected_alarms": [
    1155
  ]
}

Full example 01-fabrication.json, with the expected score of every frame.

Example 2: 02-dos.json

Synthetic traffic with an injected dos attack.

input · first 5 of 3000 frames: [time in µs, ID, DLC, data]
[0, 270, 8, [0, 32, 16, 16, 0, 0, 14, 78]]
[3295, 284, 8, [0, 129, 64, 16, 0, 0, 28, 237]]
[3455, 1285, 8, [0, 132, 66, 16, 0, 0, 5, 219]]
[4514, 845, 8, [0, 16, 8, 16, 0, 0, 77, 117]]
[6487, 1551, 8, [0, 10, 5, 16, 0, 0, 15, 46]]
frames that raise an alarm
{
  "expected_alarms": [
    1151,
    2930
  ]
}

Full example 02-dos.json, with the expected score of every frame.

Example 3: 03-fuzzing.json

Synthetic traffic with an injected fuzzing attack.

input · first 5 of 3000 frames: [time in µs, ID, DLC, data]
[0, 270, 8, [0, 32, 16, 16, 0, 0, 14, 78]]
[3295, 284, 8, [0, 129, 64, 16, 0, 0, 28, 237]]
[3455, 1285, 8, [0, 132, 66, 16, 0, 0, 5, 219]]
[4514, 845, 8, [0, 16, 8, 16, 0, 0, 77, 117]]
[6487, 1551, 8, [0, 10, 5, 16, 0, 0, 15, 46]]
frames that raise an alarm
{
  "expected_alarms": [
    1152,
    2131,
    2581
  ]
}

Full example 03-fuzzing.json, with the expected score of every frame.

Details and documents

All metrics

Every number of version 0.2.0 and what it is measured against.

Quality
MetricValueWhat it isMeasured againstItems
f1_known_vehicle_known_attack_median0.6216attack-class F1 per frame, known vehicle known attack, median over 4 setsmeasured against can-train-and-test attack labels (official splits), benchmark can-ids-v34
f1_known_vehicle_known_attack_worst0.2616attack-class F1 per frame, known vehicle known attack, worst over 4 setsmeasured against can-train-and-test attack labels (official splits), benchmark can-ids-v34
fa_per_hour_default_known_vehicle_known_attack_median0.4305false alarms per hour of the alarm rule, known vehicle known attack, median over 4 setsmeasured against can-train-and-test attack labels (official splits), benchmark can-ids-v34
fa_per_hour_default_known_vehicle_known_attack_worst7.383false alarms per hour of the alarm rule, known vehicle known attack, worst over 4 setsmeasured against can-train-and-test attack labels (official splits), benchmark can-ids-v34
f1_unknown_vehicle_known_attack_median0.5482attack-class F1 per frame, unknown vehicle known attack, median over 4 setsmeasured against can-train-and-test attack labels (official splits), benchmark can-ids-v34
f1_unknown_vehicle_known_attack_worst0.1792attack-class F1 per frame, unknown vehicle known attack, worst over 4 setsmeasured against can-train-and-test attack labels (official splits), benchmark can-ids-v34
fa_per_hour_default_unknown_vehicle_known_attack_median0false alarms per hour of the alarm rule, unknown vehicle known attack, median over 4 setsmeasured against can-train-and-test attack labels (official splits), benchmark can-ids-v34
fa_per_hour_default_unknown_vehicle_known_attack_worst0.687false alarms per hour of the alarm rule, unknown vehicle known attack, worst over 4 setsmeasured against can-train-and-test attack labels (official splits), benchmark can-ids-v34
f1_known_vehicle_unknown_attack_median0.4927attack-class F1 per frame, known vehicle unknown attack, median over 4 setsmeasured against can-train-and-test attack labels (official splits), benchmark can-ids-v34
f1_known_vehicle_unknown_attack_worst0.1466attack-class F1 per frame, known vehicle unknown attack, worst over 4 setsmeasured against can-train-and-test attack labels (official splits), benchmark can-ids-v34
fa_per_hour_default_known_vehicle_unknown_attack_median0false alarms per hour of the alarm rule, known vehicle unknown attack, median over 4 setsmeasured against can-train-and-test attack labels (official splits), benchmark can-ids-v34
fa_per_hour_default_known_vehicle_unknown_attack_worst7.857false alarms per hour of the alarm rule, known vehicle unknown attack, worst over 4 setsmeasured against can-train-and-test attack labels (official splits), benchmark can-ids-v34
f1_unknown_vehicle_unknown_attack_median0.7689attack-class F1 per frame, unknown vehicle unknown attack, median over 4 setsmeasured against can-train-and-test attack labels (official splits), benchmark can-ids-v34
f1_unknown_vehicle_unknown_attack_worst0.2616attack-class F1 per frame, unknown vehicle unknown attack, worst over 4 setsmeasured against can-train-and-test attack labels (official splits), benchmark can-ids-v34
fa_per_hour_default_unknown_vehicle_unknown_attack_median0false alarms per hour of the alarm rule, unknown vehicle unknown attack, median over 4 setsmeasured against can-train-and-test attack labels (official splits), benchmark can-ids-v34
fa_per_hour_default_unknown_vehicle_unknown_attack_worst0.861false alarms per hour of the alarm rule, unknown vehicle unknown attack, worst over 4 setsmeasured against can-train-and-test attack labels (official splits), benchmark can-ids-v34
f1_road0.9968attack-class F1 per frame on ROAD (fabrication, fuzzing)measured against ROAD injection labels, benchmark can-ids-v35
fa_per_hour_road_ambient0false alarms per hour on the ROAD test ambient capturesmeasured against ROAD ambient captures (no attacks), benchmark can-ids-v34
fa_per_hour_kcid_ford_focus0false alarms per hour on 4.9 h of benign Ford Focus traffic (other drivers and days), calibration 15 min; vehicle in no training datameasured against KCID benign traces (no attacks), benchmark kcid6
fa_per_hour_kcid_honda_cr_v0false alarms per hour on 48.0 h of benign Honda CR-V traffic (other drivers and days), calibration 15 min; vehicle in no training datameasured against KCID benign traces (no attacks), benchmark kcid2
fa_per_hour_kcid_traverse_obd0false alarms per hour on 22.0 h of Traverse traffic with OBD requests, calibration 60 min including the diagnostic IDsmeasured against KCID benign traces (no attacks), benchmark kcid23
forest_score_auroc_road0.9565AUROC of the forest score (information output, not the decision) on ROAD (fabrication, fuzzing)measured against ROAD injection labels, benchmark can-ids-v35
Speed and memory
MetricValueWhat it isMeasured on
latency_us_host0.954 uswhole chain per frame, host build incl. file I/Omeasured on host arm64 (Darwin)
latency_us_esp32_qemu25 uswhole chain per frame, median (emulator: not representative)measured on ESP32 in Espressif QEMU, 240 MHz nominal
latency_p99_us_esp32_qemu58 uswhole chain per frame, 99th percentile (emulator: not representative)measured on ESP32 in Espressif QEMU, 240 MHz nominal
flash_kb_esp32_qemu16.5 KiBdetector code and constants (features, forest, alarm stage), from the object filesmeasured on ESP32 in Espressif QEMU, 240 MHz nominal
ram_kb_esp32_qemu11.94 KiBdetector static data plus the detector statemeasured on ESP32 in Espressif QEMU, 240 MHz nominal
latency_us25 uswhole chain per frame, median (emulator: not representative)measured on ESP32 in Espressif QEMU, 240 MHz nominal
latency_p99_us58 uswhole chain per frame, 99th percentile (emulator: not representative)measured on ESP32 in Espressif QEMU, 240 MHz nominal
flash_kb16.5 KiBdetector code and constants (features, forest, alarm stage), from the object filesmeasured on ESP32 in Espressif QEMU, 240 MHz nominal
ram_kb11.94 KiBdetector static data plus the detector statemeasured on ESP32 in Espressif QEMU, 240 MHz nominal

What the numbers are measured against

  1. detector code and constants (features, forest, alarm stage), from the object files; measured on ESP32 in Espressif QEMU, 240 MHz nominal. performance.flash_kb in zoo/models/picket-forest/results/0.2.0/performance.json.
  2. detector static data plus the detector state; measured on ESP32 in Espressif QEMU, 240 MHz nominal. performance.ram_kb in zoo/models/picket-forest/results/0.2.0/performance.json.
  3. false alarms per hour of the alarm rule, unknown vehicle unknown attack, worst over 4 sets; measured against can-train-and-test attack labels (official splits), benchmark can-ids-v3. quality.fa_per_hour_default_unknown_vehicle_unknown_attack_worst in zoo/models/picket-forest/results/0.2.0/quality.json.
  4. attack-class F1 per frame on ROAD (fabrication, fuzzing); measured against ROAD injection labels, benchmark can-ids-v3. quality.f1_road in zoo/models/picket-forest/results/0.2.0/quality.json.
Licence and provenance

What you may do with it, and what it was trained on.

LicenceApache-2.0
UsageCommercial use permitted · licence Apache-2.0.
Training dataTraining-data summary and source attribution of v0.2.0
AI ActAI Act note of v0.2.0
Compliance recordCompliance record of v0.2.0
Model filesmobility-model-zoo/picket-forest on Hugging Face · files of v0.2.0
RecipeHow the model is built and measured
Codegithub.com/mhabedank/mobility-model-zoo
PrivacyTexts can contain personal data. The model does not remove it; redact texts before sharing outputs. How the training texts were collected and how to object: privacy notice.
Versions

Every version stays available and never changes.

VersionDateStatusOutput formatChangesFiles
0.2.02026-10-10experimentalpicket-forest-v2Calibrate on the vehicle, then detect. A per-vehicle table learned on the host from benign traffic; 10 features relative to it (frame counts per identifier in 8 time buckets, unknown-identifier counts, DLC, payload changes), computed by the C library msml_cal on host and device alike. Rules on these features decide (count above the calibrated maximum by more than 25 %, unknown-identifier burst, DLC change), then the alarm stage; the forest score is published as information (owner decision 2026-10-11). can-ids-v3: ROAD F1 0.997, 0 false alarms per hour on the ROAD test drives, at most 0.86 per hour on unknown vehicles; KCID Ford Focus and Traverse 0 alarms in 26.9 h. Experimental: calibration-dependent false alarms, new identifiers flood alarms, emulator-only device evidence.v0.2.0
Cite

How to cite picket-forest

bibtex
@misc{mobility-model-zoo-picket-forest,
  title  = {Picket (forest): CAN bus intrusion detection on a microcontroller, mobility-model-zoo},
  author = {Habedank, Martin},
  year   = {2026},
  url    = {https://huggingface.co/mobility-model-zoo/picket-forest}
}