Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
82505e5
spi debug (#2292)
adeebshihadeh Oct 18, 2025
fde8839
Add comma body firmware (#2291)
jhuang2006 Oct 25, 2025
246f2ee
SocketPanda improvements (#2297)
pd0wm Oct 29, 2025
a97509c
Fix mcu_type for deprecated pandas (#2296)
pd0wm Oct 29, 2025
427ca00
skip 1024 samples to settle, around 22ms (#2295)
briskspirit Oct 29, 2025
57fe9f8
Fix mcu_type in jungle (#2300)
robbederks Oct 30, 2025
bc7e1fc
Cuatro siren (#2294)
briskspirit Nov 3, 2025
6938b39
add double buffer for microphone (#2299)
briskspirit Nov 3, 2025
f4d4e3e
fix size check
robbederks Dec 5, 2025
2f0c3a3
Revert `mcu_type` changes (#2303)
robbederks Dec 5, 2025
b69f369
it's just unsupported
adeebshihadeh Dec 5, 2025
a1adc14
cleanup fan scripts
robbederks Dec 8, 2025
c2dcd04
garbage collect always-true condition check (#2305)
jyoung8607 Dec 10, 2025
25d6786
CI: use tags for cppcheck update
adeebshihadeh Dec 20, 2025
8b4fdc7
lil more
adeebshihadeh Dec 20, 2025
f79e9a1
Adjust `gitversion` handling to include null terminator in length cal…
downquark7 Dec 28, 2025
51594b1
fix up fan HITL test (#2317)
adeebshihadeh Jan 12, 2026
dcaaca1
Build everything before jungle recover (#2316)
robbederks Jan 14, 2026
b752294
Align delay and compensate (#2318)
robbederks Jan 22, 2026
1427702
rm -rf drivers/spi/; kernel driver isn't needed
adeebshihadeh Feb 2, 2026
9898f04
[bot] Update cppcheck to 2.19.1 (#2254)
commaci-public Feb 2, 2026
ef5d44e
Revert "[bot] Update cppcheck to 2.19.1 (#2254)"
adeebshihadeh Feb 4, 2026
9bf0b79
remove mcu_type (#2324)
andiradulescu Feb 9, 2026
2bb08ec
fix usb connect on macos (#2326)
andiradulescu Feb 9, 2026
a038cb6
Revert "remove mcu_type (#2324)"
adeebshihadeh Feb 11, 2026
1a46f5e
remove mcu_type (#2327)
andiradulescu Feb 11, 2026
40f65ec
update pyproject.toml: include panda.python and panda.board (#2328)
pd0wm Feb 12, 2026
4765159
improve HITL robustness (#2333)
adeebshihadeh Feb 15, 2026
c61d672
windows: fix fcntl import (#2329)
pd0wm Feb 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,22 @@ jobs:
- uses: actions/checkout@v4
- run: ./test.sh

windows:
name: windows pip package test
runs-on: windows-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- uses: astral-sh/setup-uv@v5
- name: Install package with uv
run: uv pip install --system .
- name: Verify importing panda
run: python -c "from panda import Panda"


misra_linter:
name: MISRA C:2012 Linter
runs-on: ubuntu-latest
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/update-cppcheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ jobs:
- name: Get latest cppcheck version
id: version
run: |
LATEST=$(curl -fsSL https://api.github.com/repos/danmar/cppcheck/releases/latest | jq -r .tag_name)
# Tags are sorted by time (newest first), so get the first version-like tag
LATEST=$(curl -fsSL "https://api.github.com/repos/danmar/cppcheck/tags?per_page=20" | \
jq -r '.[].name' | \
grep -E '^[0-9]+\.[0-9]+(\.[0-9]+)?$' | \
head -n 1)
echo "vers=$LATEST" >> "$GITHUB_OUTPUT"
- name: Update VERS in install.sh
run: |
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,6 @@ tests/safety/coverage.info
*.profraw
*.profdata
mull.yml

.claude/
TASK.md
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ WORKDIR $WORKDIR

# deps install
COPY pyproject.toml __init__.py setup.sh $WORKDIR
RUN mkdir -p $WORKDIR/python/ && touch $WORKDIR/__init__.py
RUN mkdir -p $WORKDIR/python/ $WORKDIR/board/body/ $WORKDIR/board/jungle/ && \
touch $WORKDIR/__init__.py $WORKDIR/board/__init__.py $WORKDIR/board/body/__init__.py $WORKDIR/board/jungle/__init__.py
RUN apt-get update && apt-get install -y --no-install-recommends sudo && DEBIAN_FRONTEND=noninteractive $WORKDIR/setup.sh

# second pass for the opendbc moving tag
Expand Down
2 changes: 1 addition & 1 deletion Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def docker_run(String step_label, int timeout_mins, String cmd) {
def phone(String ip, String step_label, String cmd) {
withCredentials([file(credentialsId: 'id_rsa', variable: 'key_file')]) {
def ssh_cmd = """
ssh -tt -o StrictHostKeyChecking=no -i ${key_file} 'comma@${ip}' /usr/bin/bash <<'END'
ssh -tt -o StrictHostKeyChecking=no -o ConnectTimeout=30 -o ConnectionAttempts=3 -o ServerAliveInterval=10 -o ServerAliveCountMax=3 -i ${key_file} 'comma@${ip}' /usr/bin/bash <<'END'

set -e

Expand Down
7 changes: 5 additions & 2 deletions SConscript
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,8 @@ base_project_h7 = {
# Common autogenerated includes
with open("board/obj/gitversion.h", "w") as f:
version = get_version(BUILDER, BUILD_TYPE)
f.write(f'extern const uint8_t gitversion[{len(version)}];\n')
f.write(f'const uint8_t gitversion[{len(version)}] = "{version}";\n')
f.write(f'extern const uint8_t gitversion[{len(version)+1}];\n')
f.write(f'const uint8_t gitversion[{len(version)+1}] = "{version}";\n')

with open("board/obj/version", "w") as f:
f.write(f'{get_version(BUILDER, BUILD_TYPE)}')
Expand All @@ -161,6 +161,9 @@ if os.getenv("FINAL_PROVISIONING"):
flags += ["-DFINAL_PROVISIONING"]
build_project("panda_jungle_h7", base_project_h7, "./board/jungle/main.c", flags)

# body fw
build_project("body_h7", base_project_h7, "./board/body/main.c", ["-DPANDA_BODY"])

# test files
if GetOption('extras'):
SConscript('tests/libpanda/SConscript')
3 changes: 3 additions & 0 deletions __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@

# panda jungle
from .board.jungle import PandaJungle, PandaJungleDFU # noqa: F401

# panda body
from .board.body import PandaBody # noqa: F401
2 changes: 1 addition & 1 deletion board/boards/cuatro.h
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ board board_cuatro = {
.read_current_mA = cuatro_read_current_mA,
.set_fan_enabled = cuatro_set_fan_enabled,
.set_ir_power = unused_set_ir_power,
.set_siren = unused_set_siren,
.set_siren = fake_siren_set,
.set_bootkick = cuatro_set_bootkick,
.read_som_gpio = tres_read_som_gpio,
.set_amp_enabled = cuatro_set_amp_enabled
Expand Down
2 changes: 1 addition & 1 deletion board/boards/tres.h
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ board board_tres = {
.read_current_mA = unused_read_current,
.set_fan_enabled = tres_set_fan_enabled,
.set_ir_power = tres_set_ir_power,
.set_siren = fake_siren_set,
.set_siren = fake_i2c_siren_set,
.set_bootkick = tres_set_bootkick,
.read_som_gpio = tres_read_som_gpio,
.set_amp_enabled = unused_set_amp_enabled
Expand Down
42 changes: 42 additions & 0 deletions board/body/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# python helpers for the body panda
import struct

from panda import Panda

class PandaBody(Panda):

MOTOR_LEFT: int = 1
MOTOR_RIGHT: int = 2

# ****************** Motor Control *****************
@staticmethod
def _ensure_valid_motor(motor: int) -> None:
if motor not in (PandaBody.MOTOR_LEFT, PandaBody.MOTOR_RIGHT):
raise ValueError("motor must be MOTOR_LEFT or MOTOR_RIGHT")

def motor_set_speed(self, motor: int, speed: int) -> None:
self._ensure_valid_motor(motor)
assert -100 <= speed <= 100, "speed must be between -100 and 100"
speed_param = speed if speed >= 0 else (256 + speed)
self._handle.controlWrite(Panda.REQUEST_OUT, 0xe0, motor, speed_param, b'')

def motor_set_target_rpm(self, motor: int, rpm: float) -> None:
self._ensure_valid_motor(motor)
target_deci_rpm = int(round(rpm * 10.0))
assert -32768 <= target_deci_rpm <= 32767, "target rpm out of range (-3276.8 to 3276.7)"
target_param = target_deci_rpm if target_deci_rpm >= 0 else (target_deci_rpm + (1 << 16))
self._handle.controlWrite(Panda.REQUEST_OUT, 0xe4, motor, target_param, b'')

def motor_stop(self, motor: int) -> None:
self._ensure_valid_motor(motor)
self._handle.controlWrite(Panda.REQUEST_OUT, 0xe1, motor, 0, b'')

def motor_get_encoder_state(self, motor: int) -> tuple[int, float]:
self._ensure_valid_motor(motor)
dat = self._handle.controlRead(Panda.REQUEST_IN, 0xe2, motor, 0, 8)
position, rpm_milli = struct.unpack("<ii", dat)
return position, rpm_milli / 1000.0

def motor_reset_encoder(self, motor: int) -> None:
self._ensure_valid_motor(motor)
self._handle.controlWrite(Panda.REQUEST_OUT, 0xe3, motor, 0, b'')
21 changes: 21 additions & 0 deletions board/body/boards/board_body.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#include "board/body/motor_control.h"

void board_body_init(void) {
motor_init();
motor_encoder_init();
motor_speed_controller_init();
motor_encoder_reset(1);
motor_encoder_reset(2);

// Initialize CAN pins
set_gpio_pullup(GPIOD, 0, PULL_NONE);
set_gpio_alternate(GPIOD, 0, GPIO_AF9_FDCAN1);
set_gpio_pullup(GPIOD, 1, PULL_NONE);
set_gpio_alternate(GPIOD, 1, GPIO_AF9_FDCAN1);
}

board board_body = {
.led_GPIO = {GPIOC, GPIOC, GPIOC},
.led_pin = {7, 7, 7},
.init = board_body_init,
};
21 changes: 21 additions & 0 deletions board/body/boards/board_declarations.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#pragma once

#include <stdint.h>
#include <stdbool.h>

// ******************** Prototypes ********************
typedef void (*board_init)(void);
typedef void (*board_init_bootloader)(void);
typedef void (*board_enable_can_transceiver)(uint8_t transceiver, bool enabled);

struct board {
GPIO_TypeDef * const led_GPIO[3];
const uint8_t led_pin[3];
const uint8_t led_pwm_channels[3]; // leave at 0 to disable PWM
board_init init;
board_init_bootloader init_bootloader;
const bool has_spi;
};

// ******************* Definitions ********************
#define HW_TYPE_BODY 0xB1U
76 changes: 76 additions & 0 deletions board/body/can.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#pragma once

#include <stdbool.h>
#include <stdint.h>

#include "board/can.h"
#include "board/health.h"
#include "board/body/motor_control.h"
#include "board/drivers/can_common_declarations.h"
#include "opendbc/safety/declarations.h"

#define BODY_CAN_ADDR_MOTOR_SPEED 0x201U
#define BODY_CAN_MOTOR_SPEED_PERIOD_US 10000U
#define BODY_BUS_NUMBER 0U

static struct {
bool pending;
int32_t left_target_deci_rpm;
int32_t right_target_deci_rpm;
} body_can_command;

void body_can_send_motor_speeds(uint8_t bus, float left_speed_rpm, float right_speed_rpm) {
CANPacket_t pkt;
pkt.bus = bus;
pkt.addr = BODY_CAN_ADDR_MOTOR_SPEED;
pkt.returned = 0;
pkt.rejected = 0;
pkt.extended = 0;
pkt.fd = 0;
pkt.data_len_code = 8;
int16_t left_speed_deci = left_speed_rpm * 10;
int16_t right_speed_deci = right_speed_rpm * 10;
pkt.data[0] = (uint8_t)((left_speed_deci >> 8) & 0xFFU);
pkt.data[1] = (uint8_t)(left_speed_deci & 0xFFU);
pkt.data[2] = (uint8_t)((right_speed_deci >> 8) & 0xFFU);
pkt.data[3] = (uint8_t)(right_speed_deci & 0xFFU);
pkt.data[4] = 0U;
pkt.data[5] = 0U;
pkt.data[6] = 0U;
pkt.data[7] = 0U;
can_set_checksum(&pkt);
can_send(&pkt, bus, true);
}

void body_can_process_target(int16_t left_target_deci_rpm, int16_t right_target_deci_rpm) {
body_can_command.left_target_deci_rpm = (int32_t)left_target_deci_rpm;
body_can_command.right_target_deci_rpm = (int32_t)right_target_deci_rpm;
body_can_command.pending = true;
}

void body_can_init(void) {
body_can_command.pending = false;
can_silent = false;
can_loopback = false;
(void)set_safety_hooks(SAFETY_BODY, 0U);
set_gpio_output(GPIOD, 2U, 0); // Enable CAN transceiver
can_init_all();
}

void body_can_periodic(uint32_t now) {
if (body_can_command.pending) {
body_can_command.pending = false;
float left_target_rpm = ((float)body_can_command.left_target_deci_rpm) * 0.1f;
float right_target_rpm = ((float)body_can_command.right_target_deci_rpm) * 0.1f;
motor_speed_controller_set_target_rpm(BODY_MOTOR_LEFT, left_target_rpm);
motor_speed_controller_set_target_rpm(BODY_MOTOR_RIGHT, right_target_rpm);
}

static uint32_t last_motor_speed_tx_us = 0;
if ((now - last_motor_speed_tx_us) >= BODY_CAN_MOTOR_SPEED_PERIOD_US) {
float left_speed_rpm = motor_encoder_get_speed_rpm(BODY_MOTOR_LEFT);
float right_speed_rpm = motor_encoder_get_speed_rpm(BODY_MOTOR_RIGHT);
body_can_send_motor_speeds(BODY_BUS_NUMBER, left_speed_rpm, right_speed_rpm);
last_motor_speed_tx_us = now;
}
}
49 changes: 49 additions & 0 deletions board/body/flash.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env python3
import argparse
import os
import subprocess

from panda import Panda

BODY_DIR = os.path.dirname(os.path.realpath(__file__))
BOARD_DIR = os.path.abspath(os.path.join(BODY_DIR, ".."))
REPO_ROOT = os.path.abspath(os.path.join(BOARD_DIR, ".."))
DEFAULT_FIRMWARE = os.path.join(BOARD_DIR, "obj", "body_h7.bin.signed")


def build_body() -> None:
subprocess.check_call(
f"scons -C {REPO_ROOT} -j$(nproc) board/obj/body_h7.bin.signed",
shell=True,
)


if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("firmware", nargs="?", help="Optional path to firmware binary to flash")
parser.add_argument("--all", action="store_true", help="Flash all Panda devices")
parser.add_argument(
"--wait-usb",
action="store_true",
help="Wait for the panda to reconnect over USB after flashing (defaults to skipping reconnect).",
)
args = parser.parse_args()

firmware_path = os.path.abspath(args.firmware) if args.firmware is not None else DEFAULT_FIRMWARE

build_body()

if not os.path.isfile(firmware_path):
parser.error(f"firmware file not found: {firmware_path}")

if args.all:
serials = Panda.list()
print(f"found {len(serials)} panda(s) - {serials}")
else:
serials = [None]

for s in serials:
with Panda(serial=s) as p:
print("flashing", p.get_usb_serial())
p.flash(firmware_path, reconnect=args.wait_usb)
exit(1 if len(serials) == 0 else 0)
Loading
Loading