1
0
forked from IQ.Lvbs/IQ.Pilot

IQ.Pilot Prebuilt Release @ ab07000

This commit is contained in:
IQ.Lvbs history cleanup
2026-08-22 23:42:42 -05:00
commit 9f9c9a70cc
3729 changed files with 778697 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
Welcome to the jungle
======
Firmware for the Panda Jungle testing board.
Available for purchase at the [comma shop](https://comma.ai/shop/panda-jungle).
## udev rules
To make the jungle usable without root permissions, you might need to setup udev rules for it.
On ubuntu, this should do the trick:
``` bash
sudo tee /etc/udev/rules.d/12-panda_jungle.rules <<EOF
SUBSYSTEM=="usb", ATTRS{idVendor}=="3801", ATTRS{idProduct}=="ddcf", MODE="0666"
SUBSYSTEM=="usb", ATTRS{idVendor}=="3801", ATTRS{idProduct}=="ddef", MODE="0666"
EOF
sudo udevadm control --reload-rules && sudo udevadm trigger
```
## updating the firmware
Updating the firmware is easy! In the `board/jungle/` folder, run:
``` bash
./flash.py
```
If you somehow bricked your jungle, you'll need a [comma key](https://comma.ai/shop/products/comma-key) to put the microcontroller in DFU mode for the V1.
For V2, the onboard button serves this purpose. When powered on while holding the button to put it in DFU mode, running `./recover.sh` in `board/` should unbrick it.

View File

@@ -0,0 +1,159 @@
# python library to interface with panda
import os
import struct
from functools import wraps
from panda import Panda, PandaDFU
from panda.python.constants import McuType
BASEDIR = os.path.dirname(os.path.realpath(__file__))
FW_PATH = os.path.join(BASEDIR, "../obj/")
def ensure_jungle_health_packet_version(fn):
@wraps(fn)
def wrapper(self, *args, **kwargs):
if self.health_version != self.HEALTH_PACKET_VERSION:
raise RuntimeError(f"Jungle firmware ({self.health_version}) doesn't match the \
library's health packet version ({self.HEALTH_PACKET_VERSION}). \
Reflash jungle.")
return fn(self, *args, **kwargs)
return wrapper
class PandaJungleDFU(PandaDFU):
def recover(self):
fn = os.path.join(FW_PATH, self._mcu_type.config.bootstub_fn.replace("panda", "panda_jungle"))
with open(fn, "rb") as f:
code = f.read()
self.program_bootstub(code)
self.reset()
class PandaJungle(Panda):
USB_PIDS = (0xddef, 0xddcf)
HW_TYPE_UNKNOWN = b'\x00'
HW_TYPE_V2 = b'\x02'
H7_DEVICES = [HW_TYPE_V2, ]
SUPPORTED_DEVICES = H7_DEVICES
HEALTH_PACKET_VERSION = 1
HEALTH_STRUCT = struct.Struct("<IffffffHHHHHHHHHHHH")
HARNESS_ORIENTATION_NONE = 0
HARNESS_ORIENTATION_1 = 1
HARNESS_ORIENTATION_2 = 2
@classmethod
def spi_connect(cls, serial, ignore_version=False):
return None, None, None, None
def flash(self, fn=None, code=None, reconnect=True):
if not fn:
fn = os.path.join(FW_PATH, self._mcu_type.config.app_fn.replace("panda", "panda_jungle"))
super().flash(fn=fn, code=code, reconnect=reconnect)
def recover(self, timeout: int | None = 60, reset: bool = True) -> bool:
dfu_serial = self.get_dfu_serial()
if reset:
self.reset(enter_bootstub=True)
self.reset(enter_bootloader=True)
if not self.wait_for_dfu(dfu_serial, timeout=timeout):
return False
dfu = PandaJungleDFU(dfu_serial)
dfu.recover()
# reflash after recover
self.connect(True, True)
self.flash()
return True
def get_mcu_type(self) -> McuType:
hw_type = self.get_type()
if hw_type in PandaJungle.H7_DEVICES:
return McuType.H7
raise ValueError(f"unknown HW type: {hw_type}")
def up_to_date(self, fn=None) -> bool:
if fn is None:
fn = os.path.join(FW_PATH, self.get_mcu_type().config.app_fn.replace("panda", "panda_jungle"))
return super().up_to_date(fn=fn)
# ******************* health *******************
@ensure_jungle_health_packet_version
def health(self):
dat = self._handle.controlRead(PandaJungle.REQUEST_IN, 0xd2, 0, 0, self.HEALTH_STRUCT.size)
a = self.HEALTH_STRUCT.unpack(dat)
return {
"uptime": a[0],
"ch1_power": a[1],
"ch2_power": a[2],
"ch3_power": a[3],
"ch4_power": a[4],
"ch5_power": a[5],
"ch6_power": a[6],
"ch1_sbu1_voltage": a[7] / 1000.0,
"ch1_sbu2_voltage": a[8] / 1000.0,
"ch2_sbu1_voltage": a[9] / 1000.0,
"ch2_sbu2_voltage": a[10] / 1000.0,
"ch3_sbu1_voltage": a[11] / 1000.0,
"ch3_sbu2_voltage": a[12] / 1000.0,
"ch4_sbu1_voltage": a[13] / 1000.0,
"ch4_sbu2_voltage": a[14] / 1000.0,
"ch5_sbu1_voltage": a[15] / 1000.0,
"ch5_sbu2_voltage": a[16] / 1000.0,
"ch6_sbu1_voltage": a[17] / 1000.0,
"ch6_sbu2_voltage": a[18] / 1000.0,
}
# ******************* control *******************
# Returns tuple with health packet version and CAN packet/USB packet version
def get_packets_versions(self):
dat = self._handle.controlRead(PandaJungle.REQUEST_IN, 0xdd, 0, 0, 3)
if dat and len(dat) == 3:
a = struct.unpack("BBB", dat)
return (a[0], a[1], a[2])
return (-1, -1, -1)
# ******************* jungle stuff *******************
def set_panda_power(self, enabled):
self._handle.controlWrite(PandaJungle.REQUEST_OUT, 0xa0, int(enabled), 0, b'')
def set_panda_individual_power(self, port, enabled):
self._handle.controlWrite(PandaJungle.REQUEST_OUT, 0xa3, int(port), int(enabled), b'')
def set_harness_orientation(self, mode):
self._handle.controlWrite(PandaJungle.REQUEST_OUT, 0xa1, int(mode), 0, b'')
def set_ignition(self, enabled):
self._handle.controlWrite(PandaJungle.REQUEST_OUT, 0xa2, int(enabled), 0, b'')
def set_can_silent(self, silent):
self._handle.controlWrite(PandaJungle.REQUEST_OUT, 0xf5, int(silent), 0, b'')
def set_generated_can(self, enabled):
self._handle.controlWrite(PandaJungle.REQUEST_OUT, 0xa4, int(enabled), 0, b'')
# ******************* serial *******************
def debug_read(self):
ret = []
while 1:
lret = bytes(self._handle.controlRead(PandaJungle.REQUEST_IN, 0xe0, 0, 0, 0x40))
if len(lret) == 0:
break
ret.append(lret)
return b''.join(ret)
# ******************* header pins *******************
def set_header_pin(self, pin_num, enabled):
self._handle.controlWrite(Panda.REQUEST_OUT, 0xf7, int(pin_num), int(enabled), b'')

27
panda/board/jungle/flash.py Executable file
View File

@@ -0,0 +1,27 @@
#!/usr/bin/env python3
import os
import subprocess
import argparse
from panda import PandaJungle
board_path = os.path.dirname(os.path.realpath(__file__))
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--all", action="store_true", help="Recover all panda jungle devices")
args = parser.parse_args()
subprocess.check_call(f"scons -C {board_path}/.. -u -j$(nproc) .", shell=True)
if args.all:
serials = PandaJungle.list()
print(f"found {len(serials)} panda jungles(s) - {serials}")
else:
serials = [None]
for s in serials:
with PandaJungle(serial=s) as p:
print("flashing", p.get_usb_serial())
p.flash()
exit(1 if len(serials) == 0 else 0)

33
panda/board/jungle/recover.py Executable file
View File

@@ -0,0 +1,33 @@
#!/usr/bin/env python3
import os
import time
import subprocess
import argparse
from panda import PandaJungle, PandaJungleDFU
board_path = os.path.dirname(os.path.realpath(__file__))
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--all", action="store_true", help="Recover all panda jungle devices")
args = parser.parse_args()
subprocess.check_call(f"scons -C {board_path}/.. -u -j$(nproc) .", shell=True)
serials = PandaJungle.list() if args.all else [None]
for s in serials:
with PandaJungle(serial=s) as p:
print(f"putting {p.get_usb_serial()} in DFU mode")
p.reset(enter_bootstub=True)
p.reset(enter_bootloader=True)
# wait for reset panda jungles to come back up
time.sleep(1)
dfu_serials = PandaJungleDFU.list()
print(f"found {len(dfu_serials)} panda jungle(s) in DFU - {dfu_serials}")
for s in dfu_serials:
print("flashing", s)
PandaJungleDFU(s).recover()
exit(1 if len(dfu_serials) == 0 else 0)

View File

@@ -0,0 +1,29 @@
#!/usr/bin/env python3
import time
import re
from panda import PandaJungle
RED = '\033[91m'
GREEN = '\033[92m'
def colorize_errors(value):
if isinstance(value, str):
if re.search(r'(?i)No error', value):
return f'{GREEN}{value}\033[0m'
elif re.search(r'(?i)(?<!No error\s)(err|error)', value):
return f'{RED}{value}\033[0m'
return str(value)
if __name__ == "__main__":
jungle = PandaJungle()
while True:
print(chr(27) + "[2J") # clear screen
print("Connected to " + ("internal panda" if jungle.is_internal() else "External panda") + f" id: {jungle.get_serial()[0]}: {jungle.get_version()}")
for bus in range(3):
print(f"\nBus {bus}:")
health = jungle.can_health(bus)
for key, value in health.items():
print(f"{key}: {colorize_errors(value)} ", end=" ")
print()
time.sleep(1)

View File

@@ -0,0 +1,44 @@
#!/usr/bin/env python3
import os
import time
from collections import defaultdict
import binascii
from panda import PandaJungle
# fake
def sec_since_boot():
return time.time()
def can_printer():
p = PandaJungle()
print(f"Connected to: {p._serial}: {p.get_version()}")
time.sleep(1)
p.can_clear(0xFFFF)
start = sec_since_boot()
lp = sec_since_boot()
all_msgs = defaultdict(list)
canbus = os.getenv("CAN")
if canbus == "3":
p.set_obd(True)
canbus = "1"
while True:
can_recv = p.can_recv()
for addr, dat, bus in can_recv:
if canbus is None or str(bus) == canbus:
all_msgs[(addr, bus)].append((dat))
if sec_since_boot() - lp > 0.1:
dd = chr(27) + "[2J"
dd += "%5.2f\n" % (sec_since_boot() - start)
for (addr, bus), dat_log in sorted(all_msgs.items()):
dd += "%d: %s(%6d): %s\n" % (bus, "%04X(%4d)" % (addr, addr), len(dat_log), binascii.hexlify(dat_log[-1]).decode())
print(dd)
lp = sec_since_boot()
if __name__ == "__main__":
can_printer()

View File

@@ -0,0 +1,38 @@
#!/usr/bin/env python3
import os
import sys
import time
from panda import PandaJungle
setcolor = ["\033[1;32;40m", "\033[1;31;40m"]
unsetcolor = "\033[00m"
if __name__ == "__main__":
while True:
try:
claim = os.getenv("CLAIM") is not None
serials = PandaJungle.list()
if os.getenv("SERIAL"):
serials = [x for x in serials if x==os.getenv("SERIAL")]
panda_jungles = [PandaJungle(x, claim=claim) for x in serials]
if not len(panda_jungles):
sys.exit("no panda jungles found")
while True:
for i, panda_jungle in enumerate(panda_jungles):
while True:
ret = panda_jungle.debug_read()
if len(ret) > 0:
sys.stdout.write(setcolor[i] + ret.decode('ascii') + unsetcolor)
sys.stdout.flush()
else:
break
time.sleep(0.01)
except Exception as e:
print(e)
print("panda jungle disconnected!")
time.sleep(0.5)

View File

@@ -0,0 +1,68 @@
#!/usr/bin/env python3
import os
import time
import contextlib
import random
from termcolor import cprint
from panda import PandaJungle
# This script is intended to be used in conjunction with the echo.py test script from panda.
# It sends messages on bus 0, 1, 2 and checks for a reversed response to be sent back.
#################################################################
############################# UTILS #############################
#################################################################
def print_colored(text, color):
cprint(text + " "*40, color, end="\r")
def get_test_string():
return b"test"+os.urandom(4)
#################################################################
############################# TEST ##############################
#################################################################
def test_loopback():
for bus in range(3):
# Clear can
jungle.can_clear(bus)
# Send a random message
address = random.randint(1, 2000)
data = get_test_string()
jungle.can_send(address, data, bus)
time.sleep(0.1)
# Make sure it comes back reversed
incoming = jungle.can_recv()
found = False
for message in incoming:
incomingAddress, incomingData, incomingBus = message
if incomingAddress == address and incomingData == data[::-1] and incomingBus == bus:
found = True
break
if not found:
cprint("\nFAILED", "red")
raise AssertionError
#################################################################
############################# MAIN ##############################
#################################################################
jungle = None
counter = 0
if __name__ == "__main__":
# Connect to jungle silently
print_colored("Connecting to jungle", "blue")
with open(os.devnull, "w") as devnull:
with contextlib.redirect_stdout(devnull):
jungle = PandaJungle()
jungle.set_panda_power(True)
jungle.set_ignition(False)
# Run test
while True:
jungle.can_clear(0xFFFF)
test_loopback()
counter += 1
print_colored(str(counter) + " loopback cycles complete", "blue")

View File

@@ -0,0 +1,9 @@
#!/usr/bin/env python3
from panda import PandaJungle
if __name__ == "__main__":
for p in PandaJungle.list():
pp = PandaJungle(p)
print(f"{pp.get_serial()[0]}: {pp.get_version()}")

View File

@@ -0,0 +1,21 @@
#!/usr/bin/env python3
import time
from pprint import pprint
from panda import PandaJungle
if __name__ == "__main__":
i = 0
pi = 0
pj = PandaJungle()
while True:
st = time.monotonic()
while time.monotonic() - st < 1:
pj.health()
pj.can_health(0)
i += 1
pprint(pj.health())
print(f"Speed: {i - pi}Hz")
pi = i

View File

@@ -0,0 +1,141 @@
#!/usr/bin/env python3
import os
import time
import contextlib
import random
from termcolor import cprint
from iqdbc.car.structs import CarParams
from panda import Panda, PandaJungle
NUM_PANDAS_PER_TEST = 1
FOR_RELEASE_BUILDS = os.getenv("RELEASE") is not None # Release builds do not have ALLOUTPUT mode
BUS_SPEEDS = [125, 500, 1000]
#################################################################
############################# UTILS #############################
#################################################################
# To suppress the connection text
def silent_panda_connect(serial):
with open(os.devnull, "w") as devnull:
with contextlib.redirect_stdout(devnull):
panda = Panda(serial)
return panda
def print_colored(text, color):
cprint(text + " "*40, color, end="\r")
def connect_to_pandas():
print_colored("Connecting to pandas", "blue")
# Connect to pandas
pandas = []
for serial in panda_serials:
pandas.append(silent_panda_connect(serial))
print_colored("Connected", "blue")
def start_with_orientation(orientation):
print_colored("Restarting pandas with orientation " + str(orientation), "blue")
jungle.set_panda_power(False)
jungle.set_harness_orientation(orientation)
time.sleep(4)
jungle.set_panda_power(True)
time.sleep(2)
connect_to_pandas()
def can_loopback(sender):
receivers = list(filter((lambda p: (p != sender)), [jungle] + pandas))
for bus in range(4):
obd = False
if bus == 3:
obd = True
bus = 1
# Clear buses
for receiver in receivers:
receiver.set_obd(obd)
receiver.can_clear(bus) # TX
receiver.can_clear(0xFFFF) # RX
# Send a random string
addr = 0x18DB33F1 if FOR_RELEASE_BUILDS else random.randint(1, 2000)
string = b"test"+os.urandom(4)
sender.set_obd(obd)
time.sleep(0.2)
sender.can_send(addr, string, bus)
time.sleep(0.2)
# Check if all receivers have indeed received them in their receiving buffers
for receiver in receivers:
content = receiver.can_recv()
# Check amount of messages
if len(content) != 1:
raise Exception("Amount of received CAN messages (" + str(len(content)) + ") does not equal 1. Bus: " + str(bus) +" OBD: " + str(obd))
# Check content
if content[0][0] != addr or content[0][1] != string:
raise Exception("Received CAN message content or address does not match")
# Check bus
if content[0][2] != bus:
raise Exception("Received CAN message bus does not match")
#################################################################
############################# TEST ##############################
#################################################################
def test_loopback():
# disable safety modes
for panda in pandas:
panda.set_safety_mode(CarParams.SafetyModel.elm327 if FOR_RELEASE_BUILDS else CarParams.SafetyModel.allOutput)
# perform loopback with jungle as a sender
can_loopback(jungle)
# perform loopback with each possible panda as a sender
for panda in pandas:
can_loopback(panda)
# enable safety modes
for panda in pandas:
panda.set_safety_mode(CarParams.SafetyModel.silent)
#################################################################
############################# MAIN ##############################
#################################################################
jungle = None
pandas = [] # type: ignore
panda_serials = []
counter = 0
if __name__ == "__main__":
# Connect to jungle silently
print_colored("Connecting to jungle", "blue")
with open(os.devnull, "w") as devnull:
with contextlib.redirect_stdout(devnull):
jungle = PandaJungle()
jungle.set_panda_power(True)
jungle.set_ignition(False)
# Connect to <NUM_PANDAS_PER_TEST> new pandas before starting tests
print_colored("Waiting for " + str(NUM_PANDAS_PER_TEST) + " pandas to be connected", "yellow")
while True:
connected_serials = Panda.list()
if len(connected_serials) == NUM_PANDAS_PER_TEST:
panda_serials = connected_serials
break
start_with_orientation(PandaJungle.HARNESS_ORIENTATION_1)
# Set bus speeds
for device in pandas + [jungle]:
for bus in range(len(BUS_SPEEDS)):
device.set_can_speed_kbps(bus, BUS_SPEEDS[bus])
# Run test
while True:
test_loopback()
counter += 1
print_colored(str(counter) + " loopback cycles complete", "blue")

View File

@@ -0,0 +1,21 @@
#!/usr/bin/env python3
import os
import random
from iqdbc.car.structs import CarParams
from panda import PandaJungle
def get_test_string():
return b"test" + os.urandom(10)
if __name__ == "__main__":
p = PandaJungle()
p.set_safety_mode(CarParams.SafetyModel.allOutput)
print("Spamming all buses...")
while True:
at = random.randint(1, 2000)
st = get_test_string()[0:8]
bus = random.randint(0, 2)
p.can_send(at, st, bus)
# print("Sent message on bus: ", bus)

View File

@@ -0,0 +1,18 @@
#!/usr/bin/env python
import sys
from panda import PandaJungle
if __name__ == "__main__":
jungle = PandaJungle()
# If first argument specified, it sets the ignition (0 or 1)
if len(sys.argv) > 1:
if sys.argv[1] == '1':
jungle.set_ignition(True)
else:
jungle.set_ignition(False)
jungle.set_harness_orientation(1)
jungle.set_panda_power(True)