Merge remote-tracking branch 'upstream/master' into civic22_long

pull/25364/head
royjr 3 years ago
commit f5c35f97d1
  1. 27
      Jenkinsfile
  2. 21
      RELEASES.md
  3. 2
      cereal
  4. 4
      docs/CARS.md
  5. 2
      launch_env.sh
  6. 1
      release/files_common
  7. 15
      selfdrive/boardd/panda_comms.h
  8. 179
      selfdrive/boardd/spi.cc
  9. 2
      selfdrive/car/gm/carstate.py
  10. 17
      selfdrive/car/gm/interface.py
  11. 2
      selfdrive/car/hyundai/values.py
  12. 4
      selfdrive/car/interfaces.py
  13. 4
      selfdrive/car/tests/test_models.py
  14. 5
      selfdrive/car/volkswagen/values.py
  15. 55
      selfdrive/controls/controlsd.py
  16. 151
      selfdrive/controls/lib/drive_helpers.py
  17. 1
      selfdrive/controls/lib/events.py
  18. 14
      selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py
  19. 13
      selfdrive/controls/lib/longitudinal_planner.py
  20. 95
      selfdrive/controls/tests/test_cruise_speed.py
  21. 6
      selfdrive/manager/test/test_manager.py
  22. 12
      selfdrive/test/longitudinal_maneuvers/maneuver.py
  23. 19
      selfdrive/test/longitudinal_maneuvers/plant.py
  24. 9
      selfdrive/test/longitudinal_maneuvers/test_longitudinal.py
  25. 2
      selfdrive/test/process_replay/ref_commit
  26. 6
      selfdrive/ui/translations/main_ko.ts
  27. 4
      selfdrive/ui/translations/main_pt-BR.ts
  28. 20
      selfdrive/ui/translations/main_zh-CHT.ts
  29. 12
      system/hardware/tici/agnos.json
  30. 3
      system/hardware/tici/hardware.py
  31. 2
      tools/cabana/SConscript
  32. 7
      tools/cabana/binaryview.cc
  33. 2
      tools/cabana/binaryview.h
  34. 12
      tools/cabana/canmessages.cc
  35. 2
      tools/cabana/canmessages.h
  36. 11
      tools/cabana/chartswidget.cc
  37. 2
      tools/cabana/chartswidget.h
  38. 75
      tools/cabana/commands.cc
  39. 63
      tools/cabana/commands.h
  40. 83
      tools/cabana/dbcmanager.cc
  41. 27
      tools/cabana/dbcmanager.h
  42. 130
      tools/cabana/detailwidget.cc
  43. 9
      tools/cabana/detailwidget.h
  44. 8
      tools/cabana/historylog.cc
  45. 2
      tools/cabana/historylog.h
  46. 46
      tools/cabana/mainwin.cc
  47. 5
      tools/cabana/settings.cc
  48. 1
      tools/cabana/settings.h
  49. 137
      tools/cabana/signaledit.cc
  50. 23
      tools/cabana/signaledit.h
  51. 78
      tools/cabana/tests/test_cabana.cc
  52. 2
      tools/cabana/videowidget.cc
  53. 7
      tools/camerastream/compressed_vipc.py

27
Jenkinsfile vendored

@ -111,7 +111,7 @@ pipeline {
R3_PUSH = "${env.BRANCH_NAME == 'master' ? '1' : ' '}" R3_PUSH = "${env.BRANCH_NAME == 'master' ? '1' : ' '}"
} }
steps { steps {
phone_steps("tici", [ phone_steps("tici-needs-can", [
["build master-ci", "cd $SOURCE_DIR/release && TARGET_DIR=$TEST_DIR EXTRA_FILES='tools/' ./build_devel.sh"], ["build master-ci", "cd $SOURCE_DIR/release && TARGET_DIR=$TEST_DIR EXTRA_FILES='tools/' ./build_devel.sh"],
["build openpilot", "cd selfdrive/manager && ./build.py"], ["build openpilot", "cd selfdrive/manager && ./build.py"],
["check dirty", "release/check-dirty.sh"], ["check dirty", "release/check-dirty.sh"],
@ -122,16 +122,24 @@ pipeline {
} }
} }
stage('loopback-tests') {
agent { docker { image 'ghcr.io/commaai/alpine-ssh'; args '--user=root' } }
steps {
phone_steps("tici-loopback", [
["build openpilot", "cd selfdrive/manager && ./build.py"],
["test boardd loopback", "python selfdrive/boardd/tests/test_boardd_loopback.py"],
])
}
}
stage('HW + Unit Tests') { stage('HW + Unit Tests') {
agent { docker { image 'ghcr.io/commaai/alpine-ssh'; args '--user=root' } } agent { docker { image 'ghcr.io/commaai/alpine-ssh'; args '--user=root' } }
steps { steps {
phone_steps("tici2", [ phone_steps("tici-common", [
["build", "cd selfdrive/manager && ./build.py"], ["build", "cd selfdrive/manager && ./build.py"],
["test power draw", "python system/hardware/tici/test_power_draw.py"], ["test power draw", "python system/hardware/tici/test_power_draw.py"],
["test boardd loopback", "python selfdrive/boardd/tests/test_boardd_loopback.py"],
["test loggerd", "python selfdrive/loggerd/tests/test_loggerd.py"], ["test loggerd", "python selfdrive/loggerd/tests/test_loggerd.py"],
["test encoder", "LD_LIBRARY_PATH=/usr/local/lib python selfdrive/loggerd/tests/test_encoder.py"], ["test encoder", "LD_LIBRARY_PATH=/usr/local/lib python selfdrive/loggerd/tests/test_encoder.py"],
["test sensord", "python selfdrive/sensord/tests/test_sensord.py"],
["test pigeond", "python selfdrive/sensord/tests/test_pigeond.py"], ["test pigeond", "python selfdrive/sensord/tests/test_pigeond.py"],
]) ])
} }
@ -159,27 +167,32 @@ pipeline {
} }
} }
stage('sensord (LSM-C)') { stage('sensord') {
agent { docker { image 'ghcr.io/commaai/alpine-ssh'; args '--user=root' } } agent { docker { image 'ghcr.io/commaai/alpine-ssh'; args '--user=root' } }
steps { steps {
phone_steps("tici-lsmc", [ phone_steps("tici-lsmc", [
["build", "cd selfdrive/manager && ./build.py"], ["build", "cd selfdrive/manager && ./build.py"],
["test sensord", "cd selfdrive/sensord/tests && python -m unittest test_sensord.py"], ["test sensord", "cd selfdrive/sensord/tests && python -m unittest test_sensord.py"],
]) ])
phone_steps("tici-bmx-lsm", [
["build", "cd selfdrive/manager && ./build.py"],
["test sensord", "cd selfdrive/sensord/tests && python -m unittest test_sensord.py"],
])
} }
} }
stage('replay') { stage('replay') {
agent { docker { image 'ghcr.io/commaai/alpine-ssh'; args '--user=root' } } agent { docker { image 'ghcr.io/commaai/alpine-ssh'; args '--user=root' } }
steps { steps {
phone_steps("tici3", [ phone_steps("tici-common", [
["build", "cd selfdrive/manager && ./build.py"], ["build", "cd selfdrive/manager && ./build.py"],
["model replay", "cd selfdrive/test/process_replay && ./model_replay.py"], ["model replay", "cd selfdrive/test/process_replay && ./model_replay.py"],
]) ])
} }
} }
}
}
} }
} }
} }

@ -1,19 +1,20 @@
Version 0.8.17 (2022-11-XX) Version 0.8.17 (2022-11-21)
======================== ========================
* New driving model * New driving model
* Internal feature space information content increased tenfold during training (to ~700 bits), this makes the model dramatically more accurate * Internal feature space information content increased tenfold during training (to ~700 bits), which makes the model dramatically more accurate
* Less reliance on previous frames makes model more reactive and snappy * Less reliance on previous frames makes model more reactive and snappy
* Trained in new reprojective simulator * Trained in new reprojective simulator
* Model trained in openpilot was trained in 36hrs from scratch, compared to around 1 week of previous releases * Trained in 36hrs from scratch, compared to one week for previous releases
* Model training now simulates lateral and longitudinal behavior, this allows openpilot to slow down for turns, stop at traffic lights, etc,... in experimental mode * Training now simulates both lateral and longitudinal behavior, which allows openpilot to slow down for turns, stop at traffic lights, and more in experimental mode
* New driver monitoring model * Driver monitoring updates
* New end-to-end distracted trigger * New bigger model with added end-to-end distracted trigger
* Reduced false positives during driver calibration
* Experimental driving mode * Experimental driving mode
* End-to-end longitudinal control * End-to-end longitudinal control
* Stops for red lights and stop signs * Stops for traffic lights and stop signs
* Slows down for turns
* openpilot defaults to chill mode, enable experimental in settings * openpilot defaults to chill mode, enable experimental in settings
* Self-tuning torque lateral controller parameters * Self-tuning torque controller: learns parameters live for each car
* Parameters learned live for each car
* Torque controller used on all Toyota, Lexus, Hyundai, Kia, and Genesis models * Torque controller used on all Toyota, Lexus, Hyundai, Kia, and Genesis models
* UI updates * UI updates
* Multi-language in navigation * Multi-language in navigation
@ -21,8 +22,10 @@ Version 0.8.17 (2022-11-XX)
* Improved update experience * Improved update experience
* Border turns grey while overriding steering * Border turns grey while overriding steering
* Bookmark events while driving; view them in comma connect * Bookmark events while driving; view them in comma connect
* New onroad visualization for experimental mode
* AGNOS 6 * AGNOS 6
* tools: new and improved cabana thanks to deanlee! * tools: new and improved cabana thanks to deanlee!
* Experimental longitudinal support for Volkswagen, CAN-FD Hyundai, and new GM models
* Genesis GV70 2022-23 support thanks to zunichky and sunnyhaibin! * Genesis GV70 2022-23 support thanks to zunichky and sunnyhaibin!
* Hyundai Santa Cruz 2021-22 support thanks to sunnyhaibin! * Hyundai Santa Cruz 2021-22 support thanks to sunnyhaibin!
* Kia Sportage 2023 support thanks to sunnyhaibin! * Kia Sportage 2023 support thanks to sunnyhaibin!

@ -1 +1 @@
Subproject commit cdba1aafec5e36505ef6ace675568e1f15003c47 Subproject commit afafa0a2a537d775842ab2e1bf20cb9a33b34f9a

@ -107,7 +107,7 @@ A supported vehicle is one that just works when you install a comma three. All s
|Kia|Sportage 2023|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai N| |Kia|Sportage 2023|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai N|
|Kia|Sportage Hybrid 2023|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai N| |Kia|Sportage Hybrid 2023|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai N|
|Kia|Stinger 2018-20|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai C| |Kia|Stinger 2018-20|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai C|
|Kia|Telluride 2020|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai H| |Kia|Telluride 2020-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Hyundai H|
|Lexus|CT Hybrid 2017-18|Lexus Safety System+|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Toyota| |Lexus|CT Hybrid 2017-18|Lexus Safety System+|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Toyota|
|Lexus|ES 2019-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Toyota| |Lexus|ES 2019-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Toyota|
|Lexus|ES Hybrid 2017-18|Lexus Safety System+|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Toyota| |Lexus|ES Hybrid 2017-18|Lexus Safety System+|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Toyota|
@ -150,7 +150,7 @@ A supported vehicle is one that just works when you install a comma three. All s
|Škoda|Octavia 2015, 2018-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| |Škoda|Octavia 2015, 2018-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533|
|Škoda|Octavia RS 2016|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| |Škoda|Octavia RS 2016|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533|
|Škoda|Scala 2020|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533[<sup>9</sup>](#footnotes)| |Škoda|Scala 2020|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533[<sup>9</sup>](#footnotes)|
|Škoda|Superb 2015-18|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533| |Škoda|Superb 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|J533|
|Toyota|Alphard 2019-20|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Toyota| |Toyota|Alphard 2019-20|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Toyota|
|Toyota|Alphard Hybrid 2021|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Toyota| |Toyota|Alphard Hybrid 2021|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|Toyota|
|Toyota|Avalon 2016|Toyota Safety Sense P|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Toyota| |Toyota|Avalon 2016|Toyota Safety Sense P|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|Toyota|

@ -7,7 +7,7 @@ export OPENBLAS_NUM_THREADS=1
export VECLIB_MAXIMUM_THREADS=1 export VECLIB_MAXIMUM_THREADS=1
if [ -z "$AGNOS_VERSION" ]; then if [ -z "$AGNOS_VERSION" ]; then
export AGNOS_VERSION="6.1" export AGNOS_VERSION="6.2"
fi fi
if [ -z "$PASSIVE" ]; then if [ -z "$PASSIVE" ]; then

@ -95,6 +95,7 @@ selfdrive/boardd/panda_comms.h
selfdrive/boardd/panda_comms.cc selfdrive/boardd/panda_comms.cc
selfdrive/boardd/set_time.py selfdrive/boardd/set_time.py
selfdrive/boardd/pandad.py selfdrive/boardd/pandad.py
selfdrive/boardd/tests/test_boardd_loopback.py
selfdrive/car/__init__.py selfdrive/car/__init__.py
selfdrive/car/docs_definitions.py selfdrive/car/docs_definitions.py

@ -5,11 +5,16 @@
#include <cstdint> #include <cstdint>
#include <vector> #include <vector>
#include <linux/spi/spidev.h>
#include <libusb-1.0/libusb.h> #include <libusb-1.0/libusb.h>
#define TIMEOUT 0 #define TIMEOUT 0
#define SPI_BUF_SIZE 1024 #define SPI_BUF_SIZE 1024
const bool PANDA_NO_RETRY = getenv("PANDA_NO_RETRY");
// comms base class // comms base class
class PandaCommsHandle { class PandaCommsHandle {
@ -29,7 +34,7 @@ public:
virtual int bulk_read(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout=TIMEOUT) = 0; virtual int bulk_read(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout=TIMEOUT) = 0;
protected: protected:
std::mutex hw_lock; std::recursive_mutex hw_lock;
}; };
class PandaUsbHandle : public PandaCommsHandle { class PandaUsbHandle : public PandaCommsHandle {
@ -65,9 +70,11 @@ public:
private: private:
int spi_fd = -1; int spi_fd = -1;
int spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t max_rx_len);
int wait_for_ack();
uint8_t tx_buf[SPI_BUF_SIZE]; uint8_t tx_buf[SPI_BUF_SIZE];
uint8_t rx_buf[SPI_BUF_SIZE]; uint8_t rx_buf[SPI_BUF_SIZE];
int wait_for_ack(spi_ioc_transfer &transfer, uint8_t ack);
int bulk_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t rx_len);
int spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t max_rx_len);
int spi_transfer_retry(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t max_rx_len);
}; };

@ -2,10 +2,12 @@
#include <linux/spi/spidev.h> #include <linux/spi/spidev.h>
#include <cassert> #include <cassert>
#include <cmath>
#include <cstring> #include <cstring>
#include "common/util.h" #include "common/util.h"
#include "common/swaglog.h" #include "common/swaglog.h"
#include "panda/board/comms_definitions.h"
#include "selfdrive/boardd/panda_comms.h" #include "selfdrive/boardd/panda_comms.h"
@ -22,13 +24,6 @@ struct __attribute__((packed)) spi_header {
uint16_t max_rx_len; uint16_t max_rx_len;
}; };
struct __attribute__((packed)) spi_control_packet {
uint16_t request;
uint16_t param1;
uint16_t param2;
uint16_t length;
};
PandaSpiHandle::PandaSpiHandle(std::string serial) : PandaCommsHandle(serial) { PandaSpiHandle::PandaSpiHandle(std::string serial) : PandaCommsHandle(serial) {
LOGD("opening SPI panda: %s", serial.c_str()); LOGD("opening SPI panda: %s", serial.c_str());
@ -40,7 +35,7 @@ PandaSpiHandle::PandaSpiHandle(std::string serial) : PandaCommsHandle(serial) {
spi_fd = open(serial.c_str(), O_RDWR); spi_fd = open(serial.c_str(), O_RDWR);
if (spi_fd < 0) { if (spi_fd < 0) {
LOGE("failed setting SPI mode %d", err); LOGE("failed opening SPI device %d", err);
goto fail; goto fail;
} }
@ -85,51 +80,65 @@ void PandaSpiHandle::cleanup() {
int PandaSpiHandle::control_write(uint8_t request, uint16_t param1, uint16_t param2, unsigned int timeout) { int PandaSpiHandle::control_write(uint8_t request, uint16_t param1, uint16_t param2, unsigned int timeout) {
int err; ControlPacket_t packet = {
.request = request,
std::lock_guard lk(hw_lock); .param1 = param1,
do { .param2 = param2,
spi_control_packet packet = { .length = 0
.request = request, };
.param1 = param1, return spi_transfer_retry(0, (uint8_t *) &packet, sizeof(packet), NULL, 0);
.param2 = param2,
.length = 0
};
// TODO: handle error
err = spi_transfer(0, (uint8_t *) &packet, sizeof(packet), NULL, 0);
} while (err < 0 && connected);
return err;
} }
int PandaSpiHandle::control_read(uint8_t request, uint16_t param1, uint16_t param2, unsigned char *data, uint16_t length, unsigned int timeout) { int PandaSpiHandle::control_read(uint8_t request, uint16_t param1, uint16_t param2, unsigned char *data, uint16_t length, unsigned int timeout) {
int err; ControlPacket_t packet = {
.request = request,
.param1 = param1,
.param2 = param2,
.length = length
};
return spi_transfer_retry(0, (uint8_t *) &packet, sizeof(packet), data, length);
}
int PandaSpiHandle::bulk_write(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout) {
return bulk_transfer(endpoint, data, length, NULL, 0);
}
int PandaSpiHandle::bulk_read(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout) {
return bulk_transfer(endpoint, NULL, 0, data, length);
}
int PandaSpiHandle::bulk_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t rx_len) {
std::lock_guard lk(hw_lock); std::lock_guard lk(hw_lock);
do {
spi_control_packet packet = {
.request = request,
.param1 = param1,
.param2 = param2,
.length = length
};
// TODO: handle error const int xfer_size = 0x40;
err = spi_transfer(0, (uint8_t *) &packet, sizeof(packet), data, length);
} while (err < 0 && connected); int ret = 0;
uint16_t length = (tx_data != NULL) ? tx_len : rx_len;
for (int i = 0; i < (int)std::ceil((float)length / xfer_size); i++) {
int d;
if (tx_data != NULL) {
int len = std::min(xfer_size, tx_len - (xfer_size * i));
d = spi_transfer_retry(endpoint, tx_data + (xfer_size * i), len, NULL, 0);
} else {
d = spi_transfer_retry(endpoint, NULL, 0, rx_data + (xfer_size * i), xfer_size);
}
return err; if (d < 0) {
} LOGE("SPI: bulk transfer failed with %d", d);
comms_healthy = false;
return -1;
}
int PandaSpiHandle::bulk_write(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout) { ret += d;
return 0; if ((rx_data != NULL) && d < xfer_size) {
} break;
}
}
int PandaSpiHandle::bulk_read(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout) { return ret;
return 0;
} }
std::vector<std::string> PandaSpiHandle::list() { std::vector<std::string> PandaSpiHandle::list() {
// TODO: list all pandas available over SPI // TODO: list all pandas available over SPI
return {}; return {};
@ -144,6 +153,46 @@ void add_checksum(uint8_t *data, int data_len) {
} }
} }
bool check_checksum(uint8_t *data, int data_len) {
uint8_t checksum = SPI_CHECKSUM_START;
for (uint16_t i = 0U; i < data_len; i++) {
checksum ^= data[i];
}
return checksum == 0U;
}
int PandaSpiHandle::spi_transfer_retry(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t max_rx_len) {
int ret;
std::lock_guard lk(hw_lock);
do {
// TODO: handle error
ret = spi_transfer(endpoint, tx_data, tx_len, rx_data, max_rx_len);
} while (ret < 0 && connected && !PANDA_NO_RETRY);
return ret;
}
int PandaSpiHandle::wait_for_ack(spi_ioc_transfer &transfer, uint8_t ack) {
// TODO: add timeout?
while (true) {
int ret = util::safe_ioctl(spi_fd, SPI_IOC_MESSAGE(1), &transfer);
if (ret < 0) {
LOGE("SPI: failed to send ACK request");
return ret;
}
if (rx_buf[0] == ack) {
break;
} else if (rx_buf[0] == SPI_NACK) {
LOGW("SPI: got NACK");
return -1;
}
}
return 0;
}
int PandaSpiHandle::spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t max_rx_len) { int PandaSpiHandle::spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx_len, uint8_t *rx_data, uint16_t max_rx_len) {
int ret; int ret;
@ -178,19 +227,9 @@ int PandaSpiHandle::spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx
// Wait for (N)ACK // Wait for (N)ACK
tx_buf[0] = 0x12; tx_buf[0] = 0x12;
transfer.len = 1; transfer.len = 1;
while (true) { ret = wait_for_ack(transfer, SPI_HACK);
ret = util::safe_ioctl(spi_fd, SPI_IOC_MESSAGE(1), &transfer); if (ret < 0) {
if (ret < 0) { goto transfer_fail;
LOGE("SPI: failed to send ACK request");
goto transfer_fail;
}
if (rx_buf[0] == SPI_HACK) {
break;
} else if (rx_buf[0] == SPI_NACK) {
LOGW("SPI: got header NACK");
goto transfer_fail;
}
} }
// Send data // Send data
@ -208,44 +247,40 @@ int PandaSpiHandle::spi_transfer(uint8_t endpoint, uint8_t *tx_data, uint16_t tx
// Wait for (N)ACK // Wait for (N)ACK
tx_buf[0] = 0xab; tx_buf[0] = 0xab;
transfer.len = 1; transfer.len = 1;
while (true) { ret = wait_for_ack(transfer, SPI_DACK);
ret = util::safe_ioctl(spi_fd, SPI_IOC_MESSAGE(1), &transfer); if (ret < 0) {
if (ret < 0) { goto transfer_fail;
LOGE("SPI: failed to send ACK request");
goto transfer_fail;
}
if (rx_buf[0] == SPI_DACK) {
break;
} else if (rx_buf[0] == SPI_NACK) {
LOGE("SPI: got data NACK");
goto transfer_fail;
}
} }
// Read data len // Read data len
transfer.len = 2; transfer.len = 2;
transfer.rx_buf = (uint64_t)(rx_buf + 1);
ret = util::safe_ioctl(spi_fd, SPI_IOC_MESSAGE(1), &transfer); ret = util::safe_ioctl(spi_fd, SPI_IOC_MESSAGE(1), &transfer);
if (ret < 0) { if (ret < 0) {
LOGE("SPI: failed to read rx data len"); LOGE("SPI: failed to read rx data len");
goto transfer_fail; goto transfer_fail;
} }
rx_data_len = *(uint16_t *)rx_buf; rx_data_len = *(uint16_t *)(rx_buf+1);
assert(rx_data_len < SPI_BUF_SIZE); assert(rx_data_len < SPI_BUF_SIZE);
// Read data // Read data
transfer.len = rx_data_len + 1; transfer.len = rx_data_len + 1;
transfer.rx_buf = (uint64_t)(rx_buf + 2 + 1);
ret = util::safe_ioctl(spi_fd, SPI_IOC_MESSAGE(1), &transfer); ret = util::safe_ioctl(spi_fd, SPI_IOC_MESSAGE(1), &transfer);
if (ret < 0) { if (ret < 0) {
LOGE("SPI: failed to read rx data"); LOGE("SPI: failed to read rx data");
goto transfer_fail; goto transfer_fail;
} }
// TODO: check checksum if (!check_checksum(rx_buf, rx_data_len + 4)) {
LOGE("SPI: bad checksum");
goto transfer_fail;
}
if (rx_data != NULL) { if (rx_data != NULL) {
memcpy(rx_data, rx_buf, rx_data_len); memcpy(rx_data, rx_buf + 3, rx_data_len);
} }
ret = rx_data_len;
return rx_data_len;
transfer_fail: transfer_fail:
return ret; return ret;

@ -63,7 +63,7 @@ class CarState(CarStateBase):
# Regen braking is braking # Regen braking is braking
if self.CP.transmissionType == TransmissionType.direct: if self.CP.transmissionType == TransmissionType.direct:
ret.brakePressed = ret.brakePressed or pt_cp.vl["EBCMRegenPaddle"]["RegenPaddle"] != 0 ret.regenBraking = pt_cp.vl["EBCMRegenPaddle"]["RegenPaddle"] != 0
ret.gas = pt_cp.vl["AcceleratorPedal2"]["AcceleratorPedal2"] / 254. ret.gas = pt_cp.vl["AcceleratorPedal2"]["AcceleratorPedal2"] / 254.
ret.gasPressed = ret.gas > 1e-5 ret.gasPressed = ret.gas > 1e-5

@ -211,17 +211,20 @@ class CarInterface(CarInterfaceBase):
ret = self.CS.update(self.cp, self.cp_cam, self.cp_loopback) ret = self.CS.update(self.cp, self.cp_cam, self.cp_loopback)
if self.CS.cruise_buttons != self.CS.prev_cruise_buttons and self.CS.prev_cruise_buttons != CruiseButtons.INIT: if self.CS.cruise_buttons != self.CS.prev_cruise_buttons and self.CS.prev_cruise_buttons != CruiseButtons.INIT:
be = create_button_event(self.CS.cruise_buttons, self.CS.prev_cruise_buttons, BUTTONS_DICT, CruiseButtons.UNPRESS) buttonEvents = [create_button_event(self.CS.cruise_buttons, self.CS.prev_cruise_buttons, BUTTONS_DICT, CruiseButtons.UNPRESS)]
# Handle ACCButtons changing buttons mid-press
if self.CS.cruise_buttons != CruiseButtons.UNPRESS and self.CS.prev_cruise_buttons != CruiseButtons.UNPRESS:
buttonEvents.append(create_button_event(CruiseButtons.UNPRESS, self.CS.prev_cruise_buttons, BUTTONS_DICT, CruiseButtons.UNPRESS))
# Suppress resume button if we're resuming from stop so we don't adjust speed. ret.buttonEvents = buttonEvents
if be.type == ButtonType.accelCruise and (ret.cruiseState.enabled and ret.standstill):
be.type = ButtonType.unknown
ret.buttonEvents = [be]
# The ECM allows enabling on falling edge of set, but only rising edge of resume
events = self.create_common_events(ret, extra_gears=[GearShifter.sport, GearShifter.low, events = self.create_common_events(ret, extra_gears=[GearShifter.sport, GearShifter.low,
GearShifter.eco, GearShifter.manumatic], GearShifter.eco, GearShifter.manumatic],
pcm_enable=self.CP.pcmCruise) pcm_enable=self.CP.pcmCruise, enable_buttons=(ButtonType.decelCruise,))
if not self.CP.pcmCruise:
if any(b.type == ButtonType.accelCruise and b.pressed for b in ret.buttonEvents):
events.add(EventName.buttonEnable)
# Enabling at a standstill with brake is allowed # Enabling at a standstill with brake is allowed
# TODO: verify 17 Volt can enable for the first time at a stop and allow for all GMs # TODO: verify 17 Volt can enable for the first time at a stop and allow for all GMs

@ -142,7 +142,7 @@ CAR_INFO: Dict[str, Optional[Union[HyundaiCarInfo, List[HyundaiCarInfo]]]] = {
], ],
CAR.PALISADE: [ CAR.PALISADE: [
HyundaiCarInfo("Hyundai Palisade 2020-22", "All", "https://youtu.be/TAnDqjF4fDY?t=456", harness=Harness.hyundai_h), HyundaiCarInfo("Hyundai Palisade 2020-22", "All", "https://youtu.be/TAnDqjF4fDY?t=456", harness=Harness.hyundai_h),
HyundaiCarInfo("Kia Telluride 2020", "All", harness=Harness.hyundai_h), HyundaiCarInfo("Kia Telluride 2020-22", "All", harness=Harness.hyundai_h),
], ],
CAR.VELOSTER: HyundaiCarInfo("Hyundai Veloster 2019-20", min_enable_speed=5. * CV.MPH_TO_MS, harness=Harness.hyundai_e), CAR.VELOSTER: HyundaiCarInfo("Hyundai Veloster 2019-20", min_enable_speed=5. * CV.MPH_TO_MS, harness=Harness.hyundai_e),
CAR.SONATA_HYBRID: HyundaiCarInfo("Hyundai Sonata Hybrid 2020-22", "All", harness=Harness.hyundai_a), CAR.SONATA_HYBRID: HyundaiCarInfo("Hyundai Sonata Hybrid 2020-22", "All", harness=Harness.hyundai_a),

@ -250,8 +250,8 @@ class CarInterfaceBase(ABC):
# Enable OP long on falling edge of enable buttons (defaults to accelCruise and decelCruise, overridable per-port) # Enable OP long on falling edge of enable buttons (defaults to accelCruise and decelCruise, overridable per-port)
if not self.CP.pcmCruise and (b.type in enable_buttons and not b.pressed): if not self.CP.pcmCruise and (b.type in enable_buttons and not b.pressed):
events.add(EventName.buttonEnable) events.add(EventName.buttonEnable)
# Disable on rising edge of cancel for both stock and OP long # Disable on rising and falling edge of cancel for both stock and OP long
if b.type == ButtonType.cancel and b.pressed: if b.type == ButtonType.cancel:
events.add(EventName.buttonCancel) events.add(EventName.buttonCancel)
# Handle permanent and temporary steering faults # Handle permanent and temporary steering faults

@ -251,8 +251,8 @@ class TestCarModelBase(unittest.TestCase):
if CS.brakePressed and not self.safety.get_brake_pressed_prev(): if CS.brakePressed and not self.safety.get_brake_pressed_prev():
if self.CP.carFingerprint in (HONDA.PILOT, HONDA.PASSPORT, HONDA.RIDGELINE) and CS.brake > 0.05: if self.CP.carFingerprint in (HONDA.PILOT, HONDA.PASSPORT, HONDA.RIDGELINE) and CS.brake > 0.05:
brake_pressed = False brake_pressed = False
safety_brake_pressed = self.safety.get_brake_pressed_prev() or self.safety.get_regen_braking_prev() checks['brakePressed'] += brake_pressed != self.safety.get_brake_pressed_prev()
checks['brakePressed'] += brake_pressed != safety_brake_pressed checks['regenBraking'] += CS.regenBraking != self.safety.get_regen_braking_prev()
if self.CP.pcmCruise: if self.CP.pcmCruise:
# On most pcmCruise cars, openpilot's state is always tied to the PCM's cruise state. # On most pcmCruise cars, openpilot's state is always tied to the PCM's cruise state.

@ -233,7 +233,7 @@ CAR_INFO: Dict[str, Union[VWCarInfo, List[VWCarInfo]]] = {
CAR.SKODA_KAROQ_MK1: VWCarInfo("Škoda Karoq 2019-21"), CAR.SKODA_KAROQ_MK1: VWCarInfo("Škoda Karoq 2019-21"),
CAR.SKODA_KODIAQ_MK1: VWCarInfo("Škoda Kodiaq 2018-19"), CAR.SKODA_KODIAQ_MK1: VWCarInfo("Škoda Kodiaq 2018-19"),
CAR.SKODA_SCALA_MK1: VWCarInfo("Škoda Scala 2020", footnotes=[Footnote.VW_EXP_LONG, Footnote.VW_MQB_A0]), CAR.SKODA_SCALA_MK1: VWCarInfo("Škoda Scala 2020", footnotes=[Footnote.VW_EXP_LONG, Footnote.VW_MQB_A0]),
CAR.SKODA_SUPERB_MK3: VWCarInfo("Škoda Superb 2015-18"), CAR.SKODA_SUPERB_MK3: VWCarInfo("Škoda Superb 2015-22"),
CAR.SKODA_OCTAVIA_MK3: [ CAR.SKODA_OCTAVIA_MK3: [
VWCarInfo("Škoda Octavia 2015, 2018-19"), VWCarInfo("Škoda Octavia 2015, 2018-19"),
VWCarInfo("Škoda Octavia RS 2016"), VWCarInfo("Škoda Octavia RS 2016"),
@ -1064,6 +1064,7 @@ FW_VERSIONS = {
}, },
CAR.SKODA_SUPERB_MK3: { CAR.SKODA_SUPERB_MK3: {
(Ecu.engine, 0x7e0, None): [ (Ecu.engine, 0x7e0, None): [
b'\xf1\x8704L906026ET\xf1\x891343',
b'\xf1\x8704L906026FP\xf1\x891196', b'\xf1\x8704L906026FP\xf1\x891196',
b'\xf1\x8704L906026KB\xf1\x894071', b'\xf1\x8704L906026KB\xf1\x894071',
b'\xf1\x8704L906026KD\xf1\x894798', b'\xf1\x8704L906026KD\xf1\x894798',
@ -1074,9 +1075,11 @@ FW_VERSIONS = {
b'\xf1\x870CW300042H \xf1\x891601', b'\xf1\x870CW300042H \xf1\x891601',
b'\xf1\x870D9300011T \xf1\x894801', b'\xf1\x870D9300011T \xf1\x894801',
b'\xf1\x870D9300012 \xf1\x894940', b'\xf1\x870D9300012 \xf1\x894940',
b'\xf1\x870D9300041H \xf1\x894905',
b'\xf1\x870GC300043 \xf1\x892301', b'\xf1\x870GC300043 \xf1\x892301',
], ],
(Ecu.srs, 0x715, None): [ (Ecu.srs, 0x715, None): [
b'\xf1\x875Q0959655AE\xf1\x890130\xf1\x82\x12111200111121001121110012211292221111',
b'\xf1\x875Q0959655AE\xf1\x890130\xf1\x82\022111200111121001121118112231292221111', b'\xf1\x875Q0959655AE\xf1\x890130\xf1\x82\022111200111121001121118112231292221111',
b'\xf1\x875Q0959655AK\xf1\x890130\xf1\x82\022111200111121001121110012211292221111', b'\xf1\x875Q0959655AK\xf1\x890130\xf1\x82\022111200111121001121110012211292221111',
b'\xf1\x875Q0959655BH\xf1\x890336\xf1\x82\02331310031313100313131013141319331413100', b'\xf1\x875Q0959655BH\xf1\x890336\xf1\x82\02331310031313100313131013141319331413100',

@ -16,8 +16,7 @@ from system.version import is_tested_branch, get_short_branch
from selfdrive.boardd.boardd import can_list_to_can_capnp from selfdrive.boardd.boardd import can_list_to_can_capnp
from selfdrive.car.car_helpers import get_car, get_startup_event, get_one_can from selfdrive.car.car_helpers import get_car, get_startup_event, get_one_can
from selfdrive.controls.lib.lateral_planner import CAMERA_OFFSET from selfdrive.controls.lib.lateral_planner import CAMERA_OFFSET
from selfdrive.controls.lib.drive_helpers import V_CRUISE_INITIAL, update_v_cruise, initialize_v_cruise from selfdrive.controls.lib.drive_helpers import VCruiseHelper, get_lag_adjusted_curvature
from selfdrive.controls.lib.drive_helpers import get_lag_adjusted_curvature
from selfdrive.controls.lib.latcontrol import LatControl from selfdrive.controls.lib.latcontrol import LatControl
from selfdrive.controls.lib.longcontrol import LongControl from selfdrive.controls.lib.longcontrol import LongControl
from selfdrive.controls.lib.latcontrol_pid import LatControlPID from selfdrive.controls.lib.latcontrol_pid import LatControlPID
@ -49,7 +48,6 @@ Desire = log.LateralPlan.Desire
LaneChangeState = log.LateralPlan.LaneChangeState LaneChangeState = log.LateralPlan.LaneChangeState
LaneChangeDirection = log.LateralPlan.LaneChangeDirection LaneChangeDirection = log.LateralPlan.LaneChangeDirection
EventName = car.CarEvent.EventName EventName = car.CarEvent.EventName
ButtonEvent = car.CarState.ButtonEvent
ButtonType = car.CarState.ButtonEvent.Type ButtonType = car.CarState.ButtonEvent.Type
SafetyModel = car.CarParams.SafetyModel SafetyModel = car.CarParams.SafetyModel
@ -173,9 +171,6 @@ class Controls:
self.active = False self.active = False
self.can_rcv_timeout = False self.can_rcv_timeout = False
self.soft_disable_timer = 0 self.soft_disable_timer = 0
self.v_cruise_kph = V_CRUISE_INITIAL
self.v_cruise_cluster_kph = V_CRUISE_INITIAL
self.v_cruise_kph_last = 0
self.mismatch_counter = 0 self.mismatch_counter = 0
self.cruise_mismatch_counter = 0 self.cruise_mismatch_counter = 0
self.can_rcv_timeout_counter = 0 self.can_rcv_timeout_counter = 0
@ -185,11 +180,11 @@ class Controls:
self.events_prev = [] self.events_prev = []
self.current_alert_types = [ET.PERMANENT] self.current_alert_types = [ET.PERMANENT]
self.logged_comm_issue = None self.logged_comm_issue = None
self.button_timers = {ButtonEvent.Type.decelCruise: 0, ButtonEvent.Type.accelCruise: 0}
self.last_actuators = car.CarControl.Actuators.new_message() self.last_actuators = car.CarControl.Actuators.new_message()
self.steer_limited = False self.steer_limited = False
self.desired_curvature = 0.0 self.desired_curvature = 0.0
self.desired_curvature_rate = 0.0 self.desired_curvature_rate = 0.0
self.v_cruise_helper = VCruiseHelper(self.CP)
# TODO: no longer necessary, aside from process replay # TODO: no longer necessary, aside from process replay
self.sm['liveParameters'].valid = True self.sm['liveParameters'].valid = True
@ -219,7 +214,7 @@ class Controls:
controls_state = Params().get("ReplayControlsState") controls_state = Params().get("ReplayControlsState")
if controls_state is not None: if controls_state is not None:
controls_state = log.ControlsState.from_bytes(controls_state) controls_state = log.ControlsState.from_bytes(controls_state)
self.v_cruise_kph = controls_state.vCruise self.v_cruise_helper.v_cruise_kph = controls_state.vCruise
if any(ps.controlsAllowed for ps in self.sm['pandaStates']): if any(ps.controlsAllowed for ps in self.sm['pandaStates']):
self.state = State.enabled self.state = State.enabled
@ -245,12 +240,13 @@ class Controls:
# Block resume if cruise never previously enabled # Block resume if cruise never previously enabled
resume_pressed = any(be.type in (ButtonType.accelCruise, ButtonType.resumeCruise) for be in CS.buttonEvents) resume_pressed = any(be.type in (ButtonType.accelCruise, ButtonType.resumeCruise) for be in CS.buttonEvents)
if not self.CP.pcmCruise and self.v_cruise_kph == V_CRUISE_INITIAL and resume_pressed: if not self.CP.pcmCruise and not self.v_cruise_helper.v_cruise_initialized and resume_pressed:
self.events.add(EventName.resumeBlocked) self.events.add(EventName.resumeBlocked)
# Disable on rising edge of accelerator or brake. Also disable on brake when speed > 0 # Disable on rising edge of accelerator or brake. Also disable on brake when speed > 0
if (CS.gasPressed and not self.CS_prev.gasPressed and self.disengage_on_accelerator) or \ if (CS.gasPressed and not self.CS_prev.gasPressed and self.disengage_on_accelerator) or \
(CS.brakePressed and (not self.CS_prev.brakePressed or not CS.standstill)): (CS.brakePressed and (not self.CS_prev.brakePressed or not CS.standstill)) or \
(CS.regenBraking and (not self.CS_prev.regenBraking or not CS.standstill)):
self.events.add(EventName.pedalPressed) self.events.add(EventName.pedalPressed)
if CS.gasPressed: if CS.gasPressed:
@ -477,20 +473,7 @@ class Controls:
def state_transition(self, CS): def state_transition(self, CS):
"""Compute conditional state transitions and execute actions on state transitions""" """Compute conditional state transitions and execute actions on state transitions"""
self.v_cruise_kph_last = self.v_cruise_kph self.v_cruise_helper.update_v_cruise(CS, self.enabled, self.is_metric)
if CS.cruiseState.available:
# if stock cruise is completely disabled, then we can use our own set speed logic
if not self.CP.pcmCruise:
self.v_cruise_kph = update_v_cruise(self.v_cruise_kph, CS.vEgo, CS.gasPressed, CS.buttonEvents,
self.button_timers, self.enabled, self.is_metric)
self.v_cruise_cluster_kph = self.v_cruise_kph
else:
self.v_cruise_kph = CS.cruiseState.speed * CV.MS_TO_KPH
self.v_cruise_cluster_kph = CS.cruiseState.speedCluster * CV.MS_TO_KPH
else:
self.v_cruise_kph = V_CRUISE_INITIAL
self.v_cruise_cluster_kph = V_CRUISE_INITIAL
# decrement the soft disable timer at every step, as it's reset on # decrement the soft disable timer at every step, as it's reset on
# entrance in SOFT_DISABLING state # entrance in SOFT_DISABLING state
@ -568,9 +551,7 @@ class Controls:
else: else:
self.state = State.enabled self.state = State.enabled
self.current_alert_types.append(ET.ENABLE) self.current_alert_types.append(ET.ENABLE)
if not self.CP.pcmCruise: self.v_cruise_helper.initialize_v_cruise(CS)
self.v_cruise_kph = initialize_v_cruise(CS.vEgo, CS.buttonEvents, self.v_cruise_kph_last)
self.v_cruise_cluster_kph = self.v_cruise_kph
# Check if openpilot is engaged and actuators are enabled # Check if openpilot is engaged and actuators are enabled
self.enabled = self.state in ENABLED_STATES self.enabled = self.state in ENABLED_STATES
@ -618,7 +599,7 @@ class Controls:
if not self.joystick_mode: if not self.joystick_mode:
# accel PID loop # accel PID loop
pid_accel_limits = self.CI.get_pid_accel_limits(self.CP, CS.vEgo, self.v_cruise_kph * CV.KPH_TO_MS) pid_accel_limits = self.CI.get_pid_accel_limits(self.CP, CS.vEgo, self.v_cruise_helper.v_cruise_kph * CV.KPH_TO_MS)
t_since_plan = (self.sm.frame - self.sm.rcv_frame['longitudinalPlan']) * DT_CTRL t_since_plan = (self.sm.frame - self.sm.rcv_frame['longitudinalPlan']) * DT_CTRL
actuators.accel = self.LoC.update(CC.longActive, CS, long_plan, pid_accel_limits, t_since_plan) actuators.accel = self.LoC.update(CC.longActive, CS, long_plan, pid_accel_limits, t_since_plan)
@ -682,16 +663,6 @@ class Controls:
return CC, lac_log return CC, lac_log
def update_button_timers(self, buttonEvents):
# increment timer for buttons still pressed
for k in self.button_timers:
if self.button_timers[k] > 0:
self.button_timers[k] += 1
for b in buttonEvents:
if b.type.raw in self.button_timers:
self.button_timers[b.type.raw] = 1 if b.pressed else 0
def publish_logs(self, CS, start_time, CC, lac_log): def publish_logs(self, CS, start_time, CC, lac_log):
"""Send actuators and hud commands to the car, send controlsstate and MPC logging""" """Send actuators and hud commands to the car, send controlsstate and MPC logging"""
@ -714,7 +685,7 @@ class Controls:
CC.cruiseControl.resume = self.enabled and CS.cruiseState.standstill and speeds[-1] > 0.1 CC.cruiseControl.resume = self.enabled and CS.cruiseState.standstill and speeds[-1] > 0.1
hudControl = CC.hudControl hudControl = CC.hudControl
hudControl.setSpeed = float(self.v_cruise_cluster_kph * CV.KPH_TO_MS) hudControl.setSpeed = float(self.v_cruise_helper.v_cruise_cluster_kph * CV.KPH_TO_MS)
hudControl.speedVisible = self.enabled hudControl.speedVisible = self.enabled
hudControl.lanesVisible = self.enabled hudControl.lanesVisible = self.enabled
hudControl.leadVisible = self.sm['longitudinalPlan'].hasLead hudControl.leadVisible = self.sm['longitudinalPlan'].hasLead
@ -797,8 +768,8 @@ class Controls:
controlsState.engageable = not self.events.any(ET.NO_ENTRY) controlsState.engageable = not self.events.any(ET.NO_ENTRY)
controlsState.longControlState = self.LoC.long_control_state controlsState.longControlState = self.LoC.long_control_state
controlsState.vPid = float(self.LoC.v_pid) controlsState.vPid = float(self.LoC.v_pid)
controlsState.vCruise = float(self.v_cruise_kph) controlsState.vCruise = float(self.v_cruise_helper.v_cruise_kph)
controlsState.vCruiseCluster = float(self.v_cruise_cluster_kph) controlsState.vCruiseCluster = float(self.v_cruise_helper.v_cruise_cluster_kph)
controlsState.upAccelCmd = float(self.LoC.pid.p) controlsState.upAccelCmd = float(self.LoC.pid.p)
controlsState.uiAccelCmd = float(self.LoC.pid.i) controlsState.uiAccelCmd = float(self.LoC.pid.i)
controlsState.ufAccelCmd = float(self.LoC.pid.f) controlsState.ufAccelCmd = float(self.LoC.pid.f)
@ -879,7 +850,6 @@ class Controls:
self.publish_logs(CS, start_time, CC, lac_log) self.publish_logs(CS, start_time, CC, lac_log)
self.prof.checkpoint("Sent") self.prof.checkpoint("Sent")
self.update_button_timers(CS.buttonEvents)
self.CS_prev = CS self.CS_prev = CS
def controlsd_thread(self): def controlsd_thread(self):
@ -888,6 +858,7 @@ class Controls:
self.rk.monitor_time() self.rk.monitor_time()
self.prof.display() self.prof.display()
def main(sm=None, pm=None, logcan=None): def main(sm=None, pm=None, logcan=None):
controls = Controls(sm, pm, logcan) controls = Controls(sm, pm, logcan)
controls.controlsd_thread() controls.controlsd_thread()

@ -22,6 +22,7 @@ CAR_ROTATION_RADIUS = 0.0
# EU guidelines # EU guidelines
MAX_LATERAL_JERK = 5.0 MAX_LATERAL_JERK = 5.0
ButtonEvent = car.CarState.ButtonEvent
ButtonType = car.CarState.ButtonEvent.Type ButtonType = car.CarState.ButtonEvent.Type
CRUISE_LONG_PRESS = 50 CRUISE_LONG_PRESS = 50
CRUISE_NEAREST_FUNC = { CRUISE_NEAREST_FUNC = {
@ -34,68 +35,122 @@ CRUISE_INTERVAL_SIGN = {
} }
def apply_deadzone(error, deadzone): class VCruiseHelper:
if error > deadzone: def __init__(self, CP):
error -= deadzone self.CP = CP
elif error < - deadzone: self.v_cruise_kph = V_CRUISE_INITIAL
error += deadzone self.v_cruise_cluster_kph = V_CRUISE_INITIAL
else: self.v_cruise_kph_last = 0
error = 0. self.button_timers = {ButtonType.decelCruise: 0, ButtonType.accelCruise: 0}
return error self.button_change_states = {btn: {"standstill": False, "enabled": False} for btn in self.button_timers}
@property
def rate_limit(new_value, last_value, dw_step, up_step): def v_cruise_initialized(self):
return clip(new_value, last_value + dw_step, last_value + up_step) return self.v_cruise_kph != V_CRUISE_INITIAL
def update_v_cruise(self, CS, enabled, is_metric):
self.v_cruise_kph_last = self.v_cruise_kph
if CS.cruiseState.available:
if not self.CP.pcmCruise:
# if stock cruise is completely disabled, then we can use our own set speed logic
self._update_v_cruise_non_pcm(CS, enabled, is_metric)
self.v_cruise_cluster_kph = self.v_cruise_kph
self.update_button_timers(CS, enabled)
else:
self.v_cruise_kph = CS.cruiseState.speed * CV.MS_TO_KPH
self.v_cruise_cluster_kph = CS.cruiseState.speedCluster * CV.MS_TO_KPH
else:
self.v_cruise_kph = V_CRUISE_INITIAL
self.v_cruise_cluster_kph = V_CRUISE_INITIAL
def _update_v_cruise_non_pcm(self, CS, enabled, is_metric):
# handle button presses. TODO: this should be in state_control, but a decelCruise press
# would have the effect of both enabling and changing speed is checked after the state transition
if not enabled:
return
long_press = False
button_type = None
# should be CV.MPH_TO_KPH, but this causes rounding errors
v_cruise_delta = 1. if is_metric else 1.6
for b in CS.buttonEvents:
if b.type.raw in self.button_timers and not b.pressed:
if self.button_timers[b.type.raw] > CRUISE_LONG_PRESS:
return # end long press
button_type = b.type.raw
break
else:
for k in self.button_timers.keys():
if self.button_timers[k] and self.button_timers[k] % CRUISE_LONG_PRESS == 0:
button_type = k
long_press = True
break
def update_v_cruise(v_cruise_kph, v_ego, gas_pressed, buttonEvents, button_timers, enabled, metric): if button_type is None:
# handle button presses. TODO: this should be in state_control, but a decelCruise press return
# would have the effect of both enabling and changing speed is checked after the state transition
if not enabled:
return v_cruise_kph
long_press = False # Don't adjust speed when pressing resume to exit standstill
button_type = None cruise_standstill = self.button_change_states[button_type]["standstill"] or CS.cruiseState.standstill
if button_type == ButtonType.accelCruise and cruise_standstill:
return
# should be CV.MPH_TO_KPH, but this causes rounding errors # Don't adjust speed if we've enabled since the button was depressed (some ports enable on rising edge)
v_cruise_delta = 1. if metric else 1.6 if not self.button_change_states[button_type]["enabled"]:
return
for b in buttonEvents:
if b.type.raw in button_timers and not b.pressed:
if button_timers[b.type.raw] > CRUISE_LONG_PRESS:
return v_cruise_kph # end long press
button_type = b.type.raw
break
else:
for k in button_timers.keys():
if button_timers[k] and button_timers[k] % CRUISE_LONG_PRESS == 0:
button_type = k
long_press = True
break
if button_type:
v_cruise_delta = v_cruise_delta * (5 if long_press else 1) v_cruise_delta = v_cruise_delta * (5 if long_press else 1)
if long_press and v_cruise_kph % v_cruise_delta != 0: # partial interval if long_press and self.v_cruise_kph % v_cruise_delta != 0: # partial interval
v_cruise_kph = CRUISE_NEAREST_FUNC[button_type](v_cruise_kph / v_cruise_delta) * v_cruise_delta self.v_cruise_kph = CRUISE_NEAREST_FUNC[button_type](self.v_cruise_kph / v_cruise_delta) * v_cruise_delta
else: else:
v_cruise_kph += v_cruise_delta * CRUISE_INTERVAL_SIGN[button_type] self.v_cruise_kph += v_cruise_delta * CRUISE_INTERVAL_SIGN[button_type]
# If set is pressed while overriding, clip cruise speed to minimum of vEgo # If set is pressed while overriding, clip cruise speed to minimum of vEgo
if gas_pressed and button_type in (ButtonType.decelCruise, ButtonType.setCruise): if CS.gasPressed and button_type in (ButtonType.decelCruise, ButtonType.setCruise):
v_cruise_kph = max(v_cruise_kph, v_ego * CV.MS_TO_KPH) self.v_cruise_kph = max(self.v_cruise_kph, CS.vEgo * CV.MS_TO_KPH)
self.v_cruise_kph = clip(round(self.v_cruise_kph, 1), V_CRUISE_MIN, V_CRUISE_MAX)
v_cruise_kph = clip(round(v_cruise_kph, 1), V_CRUISE_MIN, V_CRUISE_MAX) def update_button_timers(self, CS, enabled):
# increment timer for buttons still pressed
for k in self.button_timers:
if self.button_timers[k] > 0:
self.button_timers[k] += 1
return v_cruise_kph for b in CS.buttonEvents:
if b.type.raw in self.button_timers:
# Start/end timer and store current state on change of button pressed
self.button_timers[b.type.raw] = 1 if b.pressed else 0
self.button_change_states[b.type.raw] = {"standstill": CS.cruiseState.standstill, "enabled": enabled}
def initialize_v_cruise(self, CS):
# initializing is handled by the PCM
if self.CP.pcmCruise:
return
def initialize_v_cruise(v_ego, buttonEvents, v_cruise_last):
for b in buttonEvents:
# 250kph or above probably means we never had a set speed # 250kph or above probably means we never had a set speed
if b.type in (ButtonType.accelCruise, ButtonType.resumeCruise) and v_cruise_last < 250: if any(b.type in (ButtonType.accelCruise, ButtonType.resumeCruise) for b in CS.buttonEvents) and self.v_cruise_kph_last < 250:
return v_cruise_last self.v_cruise_kph = self.v_cruise_kph_last
else:
self.v_cruise_kph = int(round(clip(CS.vEgo * CV.MS_TO_KPH, V_CRUISE_ENABLE_MIN, V_CRUISE_MAX)))
self.v_cruise_cluster_kph = self.v_cruise_kph
return int(round(clip(v_ego * CV.MS_TO_KPH, V_CRUISE_ENABLE_MIN, V_CRUISE_MAX))) def apply_deadzone(error, deadzone):
if error > deadzone:
error -= deadzone
elif error < - deadzone:
error += deadzone
else:
error = 0.
return error
def rate_limit(new_value, last_value, dw_step, up_step):
return clip(new_value, last_value + dw_step, last_value + up_step)
def get_lag_adjusted_curvature(CP, v_ego, psis, curvatures, curvature_rates): def get_lag_adjusted_curvature(CP, v_ego, psis, curvatures, curvature_rates):

@ -597,6 +597,7 @@ EVENTS: Dict[int, Dict[str, Union[Alert, AlertCallbackType]]] = {
EventName.buttonCancel: { EventName.buttonCancel: {
ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage), ET.USER_DISABLE: EngagementAlert(AudibleAlert.disengage),
ET.NO_ENTRY: NoEntryAlert("Cancel Pressed"),
}, },
EventName.brakeHold: { EventName.brakeHold: {

@ -301,8 +301,10 @@ class LongitudinalMpc:
return lead_xv return lead_xv
def set_accel_limits(self, min_a, max_a): def set_accel_limits(self, min_a, max_a):
# TODO this sets a max accel limit, but the minimum limit is only for cruise decel
# needs refactor
self.cruise_min_a = min_a self.cruise_min_a = min_a
self.cruise_max_a = max_a self.max_a = max_a
def update(self, carstate, radarstate, v_cruise, x, v, a, j): def update(self, carstate, radarstate, v_cruise, x, v, a, j):
v_ego = self.x0[1] v_ego = self.x0[1]
@ -317,16 +319,17 @@ class LongitudinalMpc:
lead_0_obstacle = lead_xv_0[:,0] + get_stopped_equivalence_factor(lead_xv_0[:,1]) lead_0_obstacle = lead_xv_0[:,0] + get_stopped_equivalence_factor(lead_xv_0[:,1])
lead_1_obstacle = lead_xv_1[:,0] + get_stopped_equivalence_factor(lead_xv_1[:,1]) lead_1_obstacle = lead_xv_1[:,0] + get_stopped_equivalence_factor(lead_xv_1[:,1])
self.params[:,0] = MIN_ACCEL
self.params[:,1] = self.max_a
# Update in ACC mode or ACC/e2e blend # Update in ACC mode or ACC/e2e blend
if self.mode == 'acc': if self.mode == 'acc':
self.params[:,0] = MIN_ACCEL
self.params[:,1] = self.cruise_max_a
self.params[:,5] = LEAD_DANGER_FACTOR self.params[:,5] = LEAD_DANGER_FACTOR
# Fake an obstacle for cruise, this ensures smooth acceleration to set speed # Fake an obstacle for cruise, this ensures smooth acceleration to set speed
# when the leads are no factor. # when the leads are no factor.
v_lower = v_ego + (T_IDXS * self.cruise_min_a * 1.05) v_lower = v_ego + (T_IDXS * self.cruise_min_a * 1.05)
v_upper = v_ego + (T_IDXS * self.cruise_max_a * 1.05) v_upper = v_ego + (T_IDXS * self.max_a * 1.05)
v_cruise_clipped = np.clip(v_cruise * np.ones(N+1), v_cruise_clipped = np.clip(v_cruise * np.ones(N+1),
v_lower, v_lower,
v_upper) v_upper)
@ -338,9 +341,6 @@ class LongitudinalMpc:
x[:], v[:], a[:], j[:] = 0.0, 0.0, 0.0, 0.0 x[:], v[:], a[:], j[:] = 0.0, 0.0, 0.0, 0.0
elif self.mode == 'blended': elif self.mode == 'blended':
self.params[:,0] = MIN_ACCEL
self.params[:,1] = MAX_ACCEL
self.params[:,5] = 1.0 self.params[:,5] = 1.0
x_obstacles = np.column_stack([lead_0_obstacle, x_obstacles = np.column_stack([lead_0_obstacle,

@ -10,7 +10,7 @@ from common.params import Params
from common.realtime import DT_MDL from common.realtime import DT_MDL
from selfdrive.modeld.constants import T_IDXS from selfdrive.modeld.constants import T_IDXS
from selfdrive.controls.lib.longcontrol import LongCtrlState from selfdrive.controls.lib.longcontrol import LongCtrlState
from selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc from selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc, MIN_ACCEL, MAX_ACCEL
from selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import T_IDXS as T_IDXS_MPC from selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import T_IDXS as T_IDXS_MPC
from selfdrive.controls.lib.drive_helpers import V_CRUISE_MAX, CONTROL_N from selfdrive.controls.lib.drive_helpers import V_CRUISE_MAX, CONTROL_N
from system.swaglog import cloudlog from system.swaglog import cloudlog
@ -69,7 +69,8 @@ class LongitudinalPlanner:
e2e = self.params.get_bool('ExperimentalMode') and self.CP.openpilotLongitudinalControl e2e = self.params.get_bool('ExperimentalMode') and self.CP.openpilotLongitudinalControl
self.mpc.mode = 'blended' if e2e else 'acc' self.mpc.mode = 'blended' if e2e else 'acc'
def parse_model(self, model_msg, model_error): @staticmethod
def parse_model(model_msg, model_error):
if (len(model_msg.position.x) == 33 and if (len(model_msg.position.x) == 33 and
len(model_msg.velocity.x) == 33 and len(model_msg.velocity.x) == 33 and
len(model_msg.acceleration.x) == 33): len(model_msg.acceleration.x) == 33):
@ -103,8 +104,12 @@ class LongitudinalPlanner:
# No change cost when user is controlling the speed, or when standstill # No change cost when user is controlling the speed, or when standstill
prev_accel_constraint = not (reset_state or sm['carState'].standstill) prev_accel_constraint = not (reset_state or sm['carState'].standstill)
accel_limits = [A_CRUISE_MIN, get_max_accel(v_ego)] if self.mpc.mode == 'acc':
accel_limits_turns = limit_accel_in_turns(v_ego, sm['carState'].steeringAngleDeg, accel_limits, self.CP) accel_limits = [A_CRUISE_MIN, get_max_accel(v_ego)]
accel_limits_turns = limit_accel_in_turns(v_ego, sm['carState'].steeringAngleDeg, accel_limits, self.CP)
else:
accel_limits = [MIN_ACCEL, MAX_ACCEL]
accel_limits_turns = [MIN_ACCEL, MAX_ACCEL]
if reset_state: if reset_state:
self.v_desired_filter.x = v_ego self.v_desired_filter.x = v_ego

@ -1,10 +1,17 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import unittest
import numpy as np import numpy as np
from parameterized import parameterized_class
import unittest
from selfdrive.controls.lib.drive_helpers import VCruiseHelper, V_CRUISE_MAX, V_CRUISE_ENABLE_MIN
from cereal import car
from common.conversions import Conversions as CV
from common.params import Params from common.params import Params
from selfdrive.test.longitudinal_maneuvers.maneuver import Maneuver
ButtonEvent = car.CarState.ButtonEvent
ButtonType = car.CarState.ButtonEvent.Type
from selfdrive.test.longitudinal_maneuvers.maneuver import Maneuver
def run_cruise_simulation(cruise, t_end=20.): def run_cruise_simulation(cruise, t_end=20.):
man = Maneuver( man = Maneuver(
@ -19,7 +26,7 @@ def run_cruise_simulation(cruise, t_end=20.):
) )
valid, output = man.evaluate() valid, output = man.evaluate()
assert valid assert valid
return output[-1,3] return output[-1, 3]
class TestCruiseSpeed(unittest.TestCase): class TestCruiseSpeed(unittest.TestCase):
@ -35,5 +42,87 @@ class TestCruiseSpeed(unittest.TestCase):
self.assertAlmostEqual(simulation_steady_state, cruise_speed, delta=.01, msg=f'Did not reach {speed} m/s') self.assertAlmostEqual(simulation_steady_state, cruise_speed, delta=.01, msg=f'Did not reach {speed} m/s')
# TODO: test pcmCruise
@parameterized_class(('pcm_cruise',), [(False,)])
class TestVCruiseHelper(unittest.TestCase):
def setUp(self):
self.CP = car.CarParams(pcmCruise=self.pcm_cruise) # pylint: disable=E1101
self.v_cruise_helper = VCruiseHelper(self.CP)
self.reset_cruise_speed_state()
def reset_cruise_speed_state(self):
# Two resets previous cruise speed
for _ in range(2):
self.v_cruise_helper.update_v_cruise(car.CarState(cruiseState={"available": False}), enabled=False, is_metric=False)
def enable(self, v_ego):
# Simulates user pressing set with a current speed
self.v_cruise_helper.initialize_v_cruise(car.CarState(vEgo=v_ego))
def test_adjust_speed(self):
"""
Asserts speed changes on falling edges of buttons.
"""
self.enable(V_CRUISE_ENABLE_MIN * CV.KPH_TO_MS)
for btn in (ButtonType.accelCruise, ButtonType.decelCruise):
for pressed in (True, False):
CS = car.CarState(cruiseState={"available": True})
CS.buttonEvents = [ButtonEvent(type=btn, pressed=pressed)]
self.v_cruise_helper.update_v_cruise(CS, enabled=True, is_metric=False)
self.assertEqual(pressed, self.v_cruise_helper.v_cruise_kph == self.v_cruise_helper.v_cruise_kph_last)
def test_rising_edge_enable(self):
"""
Some car interfaces may enable on rising edge of a button,
ensure we don't adjust speed if enabled changes mid-press.
"""
# NOTE: enabled is always one frame behind the result from button press in controlsd
for enabled, pressed in ((False, False),
(False, True),
(True, False)):
CS = car.CarState(cruiseState={"available": True})
CS.buttonEvents = [ButtonEvent(type=ButtonType.decelCruise, pressed=pressed)]
self.v_cruise_helper.update_v_cruise(CS, enabled=enabled, is_metric=False)
if pressed:
self.enable(V_CRUISE_ENABLE_MIN * CV.KPH_TO_MS)
# Expected diff on enabling. Speed should not change on falling edge of pressed
self.assertEqual(not pressed, self.v_cruise_helper.v_cruise_kph == self.v_cruise_helper.v_cruise_kph_last)
def test_resume_in_standstill(self):
"""
Asserts we don't increment set speed if user presses resume/accel to exit cruise standstill.
"""
self.enable(0)
for standstill in (True, False):
for pressed in (True, False):
CS = car.CarState(cruiseState={"available": True, "standstill": standstill})
CS.buttonEvents = [ButtonEvent(type=ButtonType.accelCruise, pressed=pressed)]
self.v_cruise_helper.update_v_cruise(CS, enabled=True, is_metric=False)
# speed should only update if not at standstill and button falling edge
should_equal = standstill or pressed
self.assertEqual(should_equal, self.v_cruise_helper.v_cruise_kph == self.v_cruise_helper.v_cruise_kph_last)
def test_initialize_v_cruise(self):
"""
Asserts allowed cruise speeds on enabling with SET.
"""
for v_ego in np.linspace(0, 100, 101):
self.reset_cruise_speed_state()
self.assertFalse(self.v_cruise_helper.v_cruise_initialized)
self.enable(float(v_ego))
self.assertTrue(V_CRUISE_ENABLE_MIN <= self.v_cruise_helper.v_cruise_kph <= V_CRUISE_MAX)
self.assertTrue(self.v_cruise_helper.v_cruise_initialized)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

@ -4,6 +4,7 @@ import signal
import time import time
import unittest import unittest
from common.params import Params
import selfdrive.manager.manager as manager import selfdrive.manager.manager as manager
from selfdrive.manager.process import DaemonProcess from selfdrive.manager.process import DaemonProcess
from selfdrive.manager.process_config import managed_processes from selfdrive.manager.process_config import managed_processes
@ -20,6 +21,10 @@ class TestManager(unittest.TestCase):
os.environ['PASSIVE'] = '0' os.environ['PASSIVE'] = '0'
HARDWARE.set_power_save(False) HARDWARE.set_power_save(False)
# ensure clean CarParams
params = Params()
params.clear_all()
def tearDown(self): def tearDown(self):
manager.manager_cleanup() manager.manager_cleanup()
@ -40,6 +45,7 @@ class TestManager(unittest.TestCase):
Ensure all processes exit cleanly when stopped. Ensure all processes exit cleanly when stopped.
""" """
HARDWARE.set_power_save(False) HARDWARE.set_power_save(False)
manager.manager_init()
manager.manager_prepare() manager.manager_prepare()
for p in ALL_PROCESSES: for p in ALL_PROCESSES:
managed_processes[p].start() managed_processes[p].start()

@ -17,6 +17,7 @@ class Maneuver():
self.only_lead2 = kwargs.get("only_lead2", False) self.only_lead2 = kwargs.get("only_lead2", False)
self.only_radar = kwargs.get("only_radar", False) self.only_radar = kwargs.get("only_radar", False)
self.ensure_start = kwargs.get("ensure_start", False) self.ensure_start = kwargs.get("ensure_start", False)
self.enabled = kwargs.get("enabled", True)
self.duration = duration self.duration = duration
self.title = title self.title = title
@ -26,23 +27,24 @@ class Maneuver():
lead_relevancy=self.lead_relevancy, lead_relevancy=self.lead_relevancy,
speed=self.speed, speed=self.speed,
distance_lead=self.distance_lead, distance_lead=self.distance_lead,
enabled=self.enabled,
only_lead2=self.only_lead2, only_lead2=self.only_lead2,
only_radar=self.only_radar, only_radar=self.only_radar,
) )
valid = True valid = True
logs = [] logs = []
while plant.current_time() < self.duration: while plant.current_time < self.duration:
speed_lead = np.interp(plant.current_time(), self.breakpoints, self.speed_lead_values) speed_lead = np.interp(plant.current_time, self.breakpoints, self.speed_lead_values)
prob = np.interp(plant.current_time(), self.breakpoints, self.prob_lead_values) prob = np.interp(plant.current_time, self.breakpoints, self.prob_lead_values)
cruise = np.interp(plant.current_time(), self.breakpoints, self.cruise_values) cruise = np.interp(plant.current_time, self.breakpoints, self.cruise_values)
log = plant.step(speed_lead, prob, cruise) log = plant.step(speed_lead, prob, cruise)
d_rel = log['distance_lead'] - log['distance'] if self.lead_relevancy else 200. d_rel = log['distance_lead'] - log['distance'] if self.lead_relevancy else 200.
v_rel = speed_lead - log['speed'] if self.lead_relevancy else 0. v_rel = speed_lead - log['speed'] if self.lead_relevancy else 0.
log['d_rel'] = d_rel log['d_rel'] = d_rel
log['v_rel'] = v_rel log['v_rel'] = v_rel
logs.append(np.array([plant.current_time(), logs.append(np.array([plant.current_time,
log['distance'], log['distance'],
log['distance_lead'], log['distance_lead'],
log['speed'], log['speed'],

@ -10,11 +10,12 @@ from selfdrive.modeld.constants import T_IDXS
from selfdrive.controls.lib.longitudinal_planner import LongitudinalPlanner from selfdrive.controls.lib.longitudinal_planner import LongitudinalPlanner
from selfdrive.controls.lib.radar_helpers import _LEAD_ACCEL_TAU from selfdrive.controls.lib.radar_helpers import _LEAD_ACCEL_TAU
class Plant():
class Plant:
messaging_initialized = False messaging_initialized = False
def __init__(self, lead_relevancy=False, speed=0.0, distance_lead=2.0, def __init__(self, lead_relevancy=False, speed=0.0, distance_lead=2.0,
only_lead2=False, only_radar=False): enabled=True, only_lead2=False, only_radar=False):
self.rate = 1. / DT_MDL self.rate = 1. / DT_MDL
if not Plant.messaging_initialized: if not Plant.messaging_initialized:
@ -32,10 +33,11 @@ class Plant():
self.speeds = [] self.speeds = []
# lead car # lead car
self.distance_lead = distance_lead
self.lead_relevancy = lead_relevancy self.lead_relevancy = lead_relevancy
self.only_lead2=only_lead2 self.distance_lead = distance_lead
self.only_radar=only_radar self.enabled = enabled
self.only_lead2 = only_lead2
self.only_radar = only_radar
self.rk = Ratekeeper(self.rate, print_delay_threshold=100.0) self.rk = Ratekeeper(self.rate, print_delay_threshold=100.0)
self.ts = 1. / self.rate self.ts = 1. / self.rate
@ -47,6 +49,7 @@ class Plant():
self.planner = LongitudinalPlanner(CarInterface.get_params(CAR.CIVIC), init_v=self.speed) self.planner = LongitudinalPlanner(CarInterface.get_params(CAR.CIVIC), init_v=self.speed)
@property
def current_time(self): def current_time(self):
return float(self.rk.frame) / self.rate return float(self.rk.frame) / self.rate
@ -104,9 +107,7 @@ class Plant():
acceleration.x = [float(x) for x in np.zeros_like(T_IDXS)] acceleration.x = [float(x) for x in np.zeros_like(T_IDXS)]
model.modelV2.acceleration = acceleration model.modelV2.acceleration = acceleration
control.controlsState.longControlState = LongCtrlState.pid if self.enabled else LongCtrlState.off
control.controlsState.longControlState = LongCtrlState.pid
control.controlsState.vCruise = float(v_cruise * 3.6) control.controlsState.vCruise = float(v_cruise * 3.6)
car_state.carState.vEgo = float(self.speed) car_state.carState.vEgo = float(self.speed)
car_state.carState.standstill = self.speed < 0.01 car_state.carState.standstill = self.speed < 0.01
@ -141,7 +142,7 @@ class Plant():
# print at 5hz # print at 5hz
if (self.rk.frame % (self.rate // 5)) == 0: if (self.rk.frame % (self.rate // 5)) == 0:
print("%2.2f sec %6.2f m %6.2f m/s %6.2f m/s2 lead_rel: %6.2f m %6.2f m/s" print("%2.2f sec %6.2f m %6.2f m/s %6.2f m/s2 lead_rel: %6.2f m %6.2f m/s"
% (self.current_time(), self.distance, self.speed, self.acceleration, d_rel, v_rel)) % (self.current_time, self.distance, self.speed, self.acceleration, d_rel, v_rel))
# ******** update prevs ******** # ******** update prevs ********

@ -10,7 +10,7 @@ from selfdrive.test.longitudinal_maneuvers.maneuver import Maneuver
# TODO: make new FCW tests # TODO: make new FCW tests
maneuvers = [ maneuvers = [
Maneuver( Maneuver(
'approach stopped car at 20m/s, initial distance: 120m', 'approach stopped car at 25m/s, initial distance: 120m',
duration=20., duration=20.,
initial_speed=25., initial_speed=25.,
lead_relevancy=True, lead_relevancy=True,
@ -118,6 +118,13 @@ maneuvers = [
breakpoints=[1., 10., 15.], breakpoints=[1., 10., 15.],
ensure_start=True, ensure_start=True,
), ),
Maneuver(
'cruising at 25 m/s while disabled',
duration=20.,
initial_speed=25.,
lead_relevancy=False,
enabled=False,
),
] ]

@ -1 +1 @@
a36f7e2fd922fcadca6f8a3d777f4db787cba016 aa2d370836588fd80b648dbed8d156765ec804d5

@ -1004,11 +1004,11 @@ location set</source>
</message> </message>
<message> <message>
<source>Experimental Mode</source> <source>Experimental Mode</source>
<translation> </translation> <translation> </translation>
</message> </message>
<message> <message>
<source> openpilot defaults to driving in &lt;b&gt;chill mode&lt;/b&gt;. Experimental mode enables &lt;b&gt;alpha-level features&lt;/b&gt; that aren&apos;t ready for chill mode. Experimental features are listed below: &lt;br&gt; &lt;h4&gt;🌮 End-to-End Longitudinal Control 🌮&lt;/h4&gt; Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides which speed to drive, the set speed will only act as an upper bound.</source> <source> openpilot defaults to driving in &lt;b&gt;chill mode&lt;/b&gt;. Experimental mode enables &lt;b&gt;alpha-level features&lt;/b&gt; that aren&apos;t ready for chill mode. Experimental features are listed below: &lt;br&gt; &lt;h4&gt;🌮 End-to-End Longitudinal Control 🌮&lt;/h4&gt; Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides which speed to drive, the set speed will only act as an upper bound.</source>
<translation type="unfinished"></translation> <translation> openpilot은 &lt;b&gt; &lt;/b&gt; . &lt;b&gt; &lt;/b&gt; . &lt;br&gt; &lt;h4&gt;🌮 E2E 🌮&lt;/h4&gt; . openpilot . .</translation>
</message> </message>
<message> <message>
<source>openpilot defaults to the car&apos;s built-in ACC instead of openpilot&apos;s longitudinal control on this car. Enable this to switch to openpilot longitudinal control.</source> <source>openpilot defaults to the car&apos;s built-in ACC instead of openpilot&apos;s longitudinal control on this car. Enable this to switch to openpilot longitudinal control.</source>
@ -1016,7 +1016,7 @@ location set</source>
</message> </message>
<message> <message>
<source>WARNING: openpilot longitudinal control is experimental for this car and will disable Automatic Emergency Braking (AEB).</source> <source>WARNING: openpilot longitudinal control is experimental for this car and will disable Automatic Emergency Braking (AEB).</source>
<translation type="unfinished"></translation> <translation>경고: openpilot (AEB) .</translation>
</message> </message>
</context> </context>
<context> <context>

@ -1012,7 +1012,7 @@ trabalho definido</translation>
</message> </message>
<message> <message>
<source> openpilot defaults to driving in &lt;b&gt;chill mode&lt;/b&gt;. Experimental mode enables &lt;b&gt;alpha-level features&lt;/b&gt; that aren&apos;t ready for chill mode. Experimental features are listed below: &lt;br&gt; &lt;h4&gt;🌮 End-to-End Longitudinal Control 🌮&lt;/h4&gt; Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides which speed to drive, the set speed will only act as an upper bound.</source> <source> openpilot defaults to driving in &lt;b&gt;chill mode&lt;/b&gt;. Experimental mode enables &lt;b&gt;alpha-level features&lt;/b&gt; that aren&apos;t ready for chill mode. Experimental features are listed below: &lt;br&gt; &lt;h4&gt;🌮 End-to-End Longitudinal Control 🌮&lt;/h4&gt; Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides which speed to drive, the set speed will only act as an upper bound.</source>
<translation type="unfinished"></translation> <translation> openpilot por padrão funciona em &lt;b&gt;modo chill&lt;/b&gt;. modo Experimental ativa &lt;b&gt;recursos de nível-alfa&lt;/b&gt; que não estão prontos para o modo chill. Recursos experimentais estão listados abaixo: &lt;br&gt; &lt;h4&gt;🌮 Controle Longitudinal de Ponta a Ponta 🌮&lt;/h4&gt; Deixe o modelo de condução controlar o acelerador e os freios. Uma vez que o modelo de condução decide qual velocidade dirigir, a velocidade definida só funcionará como um limite superior.</translation>
</message> </message>
<message> <message>
<source>openpilot defaults to the car&apos;s built-in ACC instead of openpilot&apos;s longitudinal control on this car. Enable this to switch to openpilot longitudinal control.</source> <source>openpilot defaults to the car&apos;s built-in ACC instead of openpilot&apos;s longitudinal control on this car. Enable this to switch to openpilot longitudinal control.</source>
@ -1020,7 +1020,7 @@ trabalho definido</translation>
</message> </message>
<message> <message>
<source>WARNING: openpilot longitudinal control is experimental for this car and will disable Automatic Emergency Braking (AEB).</source> <source>WARNING: openpilot longitudinal control is experimental for this car and will disable Automatic Emergency Braking (AEB).</source>
<translation type="unfinished"></translation> <translation>ATENÇÃO: o controle longitudinal do openpilot é experimental para este carro e desativará a Frenagem Automática de Emergência (AEB).</translation>
</message> </message>
</context> </context>
<context> <context>

@ -60,11 +60,11 @@
</message> </message>
<message> <message>
<source>Cellular Metered</source> <source>Cellular Metered</source>
<translation type="unfinished"></translation> <translation></translation>
</message> </message>
<message> <message>
<source>Prevent large data uploads when on a metered connection</source> <source>Prevent large data uploads when on a metered connection</source>
<translation type="unfinished"></translation> <translation>使</translation>
</message> </message>
</context> </context>
<context> <context>
@ -240,11 +240,11 @@
</message> </message>
<message> <message>
<source>Reset</source> <source>Reset</source>
<translation type="unfinished"></translation> <translation></translation>
</message> </message>
<message> <message>
<source>Review</source> <source>Review</source>
<translation type="unfinished"></translation> <translation></translation>
</message> </message>
</context> </context>
<context> <context>
@ -864,7 +864,7 @@ location set</source>
</message> </message>
<message> <message>
<source>Uninstall</source> <source>Uninstall</source>
<translation type="unfinished"></translation> <translation></translation>
</message> </message>
</context> </context>
<context> <context>
@ -1004,19 +1004,19 @@ location set</source>
</message> </message>
<message> <message>
<source>Experimental Mode</source> <source>Experimental Mode</source>
<translation type="unfinished"></translation> <translation></translation>
</message> </message>
<message> <message>
<source> openpilot defaults to driving in &lt;b&gt;chill mode&lt;/b&gt;. Experimental mode enables &lt;b&gt;alpha-level features&lt;/b&gt; that aren&apos;t ready for chill mode. Experimental features are listed below: &lt;br&gt; &lt;h4&gt;🌮 End-to-End Longitudinal Control 🌮&lt;/h4&gt; Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides which speed to drive, the set speed will only act as an upper bound.</source> <source> openpilot defaults to driving in &lt;b&gt;chill mode&lt;/b&gt;. Experimental mode enables &lt;b&gt;alpha-level features&lt;/b&gt; that aren&apos;t ready for chill mode. Experimental features are listed below: &lt;br&gt; &lt;h4&gt;🌮 End-to-End Longitudinal Control 🌮&lt;/h4&gt; Let the driving model control the gas and brakes. openpilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides which speed to drive, the set speed will only act as an upper bound.</source>
<translation type="unfinished"></translation> <translation> openpilot &lt;b&gt;&lt;/b&gt; &lt;b&gt;alpha &lt;/b&gt; &lt;br&gt; &lt;h4&gt;🌮🌮&lt;/h4&gt; openpilot </translation>
</message> </message>
<message> <message>
<source>openpilot defaults to the car&apos;s built-in ACC instead of openpilot&apos;s longitudinal control on this car. Enable this to switch to openpilot longitudinal control.</source> <source>openpilot defaults to the car&apos;s built-in ACC instead of openpilot&apos;s longitudinal control on this car. Enable this to switch to openpilot longitudinal control.</source>
<translation type="unfinished"></translation> <translation>openpilot 使 (ACC)使 openpilot openpilot </translation>
</message> </message>
<message> <message>
<source>WARNING: openpilot longitudinal control is experimental for this car and will disable Automatic Emergency Braking (AEB).</source> <source>WARNING: openpilot longitudinal control is experimental for this car and will disable Automatic Emergency Braking (AEB).</source>
<translation type="unfinished"></translation> <translation>openpilot (AEB) </translation>
</message> </message>
</context> </context>
<context> <context>
@ -1074,7 +1074,7 @@ location set</source>
</message> </message>
<message> <message>
<source>Forget</source> <source>Forget</source>
<translation type="unfinished"></translation> <translation></translation>
</message> </message>
</context> </context>
</TS> </TS>

@ -1,9 +1,9 @@
[ [
{ {
"name": "boot", "name": "boot",
"url": "https://commadist.azureedge.net/agnosupdate/boot-57626d7737ab2fa1318e8707a202b1295b5da79ad2fa0a36377cc9481ad0d136.img.xz", "url": "https://commadist.azureedge.net/agnosupdate/boot-72662ec5d586c7a22659a1c8b140932d5472914176020fe76ba4204edbbb214a.img.xz",
"hash": "57626d7737ab2fa1318e8707a202b1295b5da79ad2fa0a36377cc9481ad0d136", "hash": "72662ec5d586c7a22659a1c8b140932d5472914176020fe76ba4204edbbb214a",
"hash_raw": "57626d7737ab2fa1318e8707a202b1295b5da79ad2fa0a36377cc9481ad0d136", "hash_raw": "72662ec5d586c7a22659a1c8b140932d5472914176020fe76ba4204edbbb214a",
"size": 14780416, "size": 14780416,
"sparse": false, "sparse": false,
"full_check": true, "full_check": true,
@ -41,9 +41,9 @@
}, },
{ {
"name": "system", "name": "system",
"url": "https://commadist.azureedge.net/agnosupdate/system-b40b08912576bb972907acba7c201c1399395cbc0cba06ce6e5e3f70ab565cb5.img.xz", "url": "https://commadist.azureedge.net/agnosupdate/system-9efdc9368f05e06008a7a1dbbee21b564e89988dc94d6ddee3a3a88e42268f0e.img.xz",
"hash": "6e8fbcc21a265f7f58062abce7675dc05540e2b60cee2df56992a151ba64936f", "hash": "48209ce7e8cc2fff4ec024f0cd82fc2e3e097b5c0629be2b292acf64e6701449",
"hash_raw": "b40b08912576bb972907acba7c201c1399395cbc0cba06ce6e5e3f70ab565cb5", "hash_raw": "9efdc9368f05e06008a7a1dbbee21b564e89988dc94d6ddee3a3a88e42268f0e",
"size": 10737418240, "size": 10737418240,
"sparse": true, "sparse": true,
"full_check": false, "full_check": false,

@ -431,9 +431,6 @@ class Tici(HardwareBase):
def initialize_hardware(self): def initialize_hardware(self):
self.amplifier.initialize_configuration() self.amplifier.initialize_configuration()
# TODO: this should go in AGNOS
os.system("sudo chmod 666 /dev/spidev0.0")
# Allow thermald to write engagement status to kmsg # Allow thermald to write engagement status to kmsg
os.system("sudo chmod a+w /dev/kmsg") os.system("sudo chmod a+w /dev/kmsg")

@ -19,7 +19,7 @@ prev_moc_path = cabana_env['QT_MOCHPREFIX']
cabana_env['QT_MOCHPREFIX'] = os.path.dirname(prev_moc_path) + '/cabana/moc_' cabana_env['QT_MOCHPREFIX'] = os.path.dirname(prev_moc_path) + '/cabana/moc_'
cabana_env.Execute('./generate_dbc_json.py --out car_fingerprint_to_dbc.json') cabana_env.Execute('./generate_dbc_json.py --out car_fingerprint_to_dbc.json')
cabana_lib = cabana_env.Library("cabana_lib", ['mainwin.cc', 'binaryview.cc', 'chartswidget.cc', 'historylog.cc', 'videowidget.cc', 'signaledit.cc', 'dbcmanager.cc', cabana_lib = cabana_env.Library("cabana_lib", ['mainwin.cc', 'binaryview.cc', 'chartswidget.cc', 'historylog.cc', 'videowidget.cc', 'signaledit.cc', 'dbcmanager.cc',
'canmessages.cc', 'messageswidget.cc', 'settings.cc', 'detailwidget.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) 'canmessages.cc', 'commands.cc', 'messageswidget.cc', 'settings.cc', 'detailwidget.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks)
cabana_env.Program('_cabana', ['cabana.cc', cabana_lib], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) cabana_env.Program('_cabana', ['cabana.cc', cabana_lib], LIBS=cabana_libs, FRAMEWORKS=base_frameworks)
if GetOption('test'): if GetOption('test'):

@ -165,14 +165,14 @@ void BinaryViewModel::setMessage(const QString &message_id) {
if ((dbc_msg = dbc()->msg(msg_id))) { if ((dbc_msg = dbc()->msg(msg_id))) {
row_count = dbc_msg->size; row_count = dbc_msg->size;
items.resize(row_count * column_count); items.resize(row_count * column_count);
for (int i = 0; i < dbc_msg->sigs.size(); ++i) { int i = 0;
const auto &sig = dbc_msg->sigs[i]; for (auto &[name, sig] : dbc_msg->sigs) {
auto [start, end] = getSignalRange(&sig); auto [start, end] = getSignalRange(&sig);
for (int j = start; j <= end; ++j) { for (int j = start; j <= end; ++j) {
int bit_index = sig.is_little_endian ? bigEndianBitIndex(j) : j; int bit_index = sig.is_little_endian ? bigEndianBitIndex(j) : j;
int idx = column_count * (bit_index / 8) + bit_index % 8; int idx = column_count * (bit_index / 8) + bit_index % 8;
if (idx >= items.size()) { if (idx >= items.size()) {
qWarning() << "signal " << sig.name.c_str() << "out of bounds.start_bit:" << sig.start_bit << "size:" << sig.size; qWarning() << "signal " << name << "out of bounds.start_bit:" << sig.start_bit << "size:" << sig.size;
break; break;
} }
if (j == start) sig.is_little_endian ? items[idx].is_lsb = true : items[idx].is_msb = true; if (j == start) sig.is_little_endian ? items[idx].is_lsb = true : items[idx].is_msb = true;
@ -180,6 +180,7 @@ void BinaryViewModel::setMessage(const QString &message_id) {
items[idx].bg_color = getColor(i); items[idx].bg_color = getColor(i);
items[idx].sigs.push_back(&sig); items[idx].sigs.push_back(&sig);
} }
++i;
} }
} else { } else {
row_count = can->lastMessage(msg_id).dat.size(); row_count = can->lastMessage(msg_id).dat.size();

@ -50,7 +50,7 @@ public:
private: private:
QString msg_id; QString msg_id;
const Msg *dbc_msg; const DBCMsg *dbc_msg;
int row_count = 0; int row_count = 0;
const int column_count = 9; const int column_count = 9;
}; };

@ -44,7 +44,7 @@ QList<QPointF> CANMessages::findSignalValues(const QString &id, const Signal *si
for (auto &evt : *evts) { for (auto &evt : *evts) {
if (evt->which != cereal::Event::Which::CAN) continue; if (evt->which != cereal::Event::Which::CAN) continue;
for (auto c : evt->event.getCan()) { for (const auto &c : evt->event.getCan()) {
if (bus == c.getSrc() && address == c.getAddress()) { if (bus == c.getSrc() && address == c.getAddress()) {
double val = get_raw_value((uint8_t *)c.getDat().begin(), c.getDat().size(), *signal); double val = get_raw_value((uint8_t *)c.getDat().begin(), c.getDat().size(), *signal);
if ((flag == EQ && val == value) || (flag == LT && val < value) || (flag == GT && val > value)) { if ((flag == EQ && val == value) || (flag == LT && val < value) || (flag == GT && val > value)) {
@ -65,6 +65,7 @@ void CANMessages::process(QHash<QString, CanData> *messages) {
emit updated(); emit updated();
emit msgsReceived(messages); emit msgsReceived(messages);
delete messages; delete messages;
processing = false;
} }
bool CANMessages::eventFilter(const Event *event) { bool CANMessages::eventFilter(const Event *event) {
@ -78,7 +79,7 @@ bool CANMessages::eventFilter(const Event *event) {
} }
double current_sec = replay->currentSeconds(); double current_sec = replay->currentSeconds();
if (counters_begin_sec == 0) { if (counters_begin_sec == 0 || counters_begin_sec >= current_sec) {
counters.clear(); counters.clear();
counters_begin_sec = current_sec; counters_begin_sec = current_sec;
} }
@ -94,7 +95,6 @@ bool CANMessages::eventFilter(const Event *event) {
} }
CanData &data = list.emplace_front(); CanData &data = list.emplace_front();
data.ts = current_sec; data.ts = current_sec;
data.bus_time = c.getBusTime();
data.dat.append((char *)c.getDat().begin(), c.getDat().size()); data.dat.append((char *)c.getDat().begin(), c.getDat().size());
data.count = ++counters[id]; data.count = ++counters[id];
@ -105,7 +105,9 @@ bool CANMessages::eventFilter(const Event *event) {
} }
double ts = millis_since_boot(); double ts = millis_since_boot();
if ((ts - prev_update_ts) > (1000.0 / settings.fps)) { if ((ts - prev_update_ts) > (1000.0 / settings.fps) && !processing) {
// delay posting CAN message if UI thread is busy
processing = true;
prev_update_ts = ts; prev_update_ts = ts;
// use pointer to avoid data copy in queued connection. // use pointer to avoid data copy in queued connection.
emit received(new_msgs.release()); emit received(new_msgs.release());
@ -120,7 +122,7 @@ const std::deque<CanData> CANMessages::messages(const QString &id) {
} }
void CANMessages::seekTo(double ts) { void CANMessages::seekTo(double ts) {
replay->seekTo(ts, false); replay->seekTo(std::max(double(0), ts), false);
counters_begin_sec = 0; counters_begin_sec = 0;
} }

@ -16,7 +16,6 @@ struct CanData {
double ts = 0.; double ts = 0.;
uint32_t count = 0; uint32_t count = 0;
uint32_t freq = 0; uint32_t freq = 0;
uint16_t bus_time = 0;
QByteArray dat; QByteArray dat;
}; };
@ -63,6 +62,7 @@ protected:
Replay *replay = nullptr; Replay *replay = nullptr;
std::mutex lock; std::mutex lock;
std::atomic<double> counters_begin_sec = 0; std::atomic<double> counters_begin_sec = 0;
std::atomic<bool> processing = false;
QHash<QString, uint32_t> counters; QHash<QString, uint32_t> counters;
QHash<QString, std::deque<CanData>> received_msgs; QHash<QString, std::deque<CanData>> received_msgs;
}; };

@ -192,6 +192,8 @@ ChartView::ChartView(const QString &id, const Signal *sig, QWidget *parent)
chart->createDefaultAxes(); chart->createDefaultAxes();
chart->legend()->hide(); chart->legend()->hide();
chart->layout()->setContentsMargins(0, 0, 0, 0); chart->layout()->setContentsMargins(0, 0, 0, 0);
// top margin for title
chart->setMargins({0, 11, 0, 0});
line_marker = new QGraphicsLineItem(chart); line_marker = new QGraphicsLineItem(chart);
line_marker->setZValue(chart->zValue() + 10); line_marker->setZValue(chart->zValue() + 10);
@ -205,7 +207,6 @@ ChartView::ChartView(const QString &id, const Signal *sig, QWidget *parent)
item_group->setZValue(chart->zValue() + 10); item_group->setZValue(chart->zValue() + 10);
// title // title
msg_title = new QGraphicsTextItem(chart);
QToolButton *remove_btn = new QToolButton(); QToolButton *remove_btn = new QToolButton();
remove_btn->setText("X"); remove_btn->setText("X");
remove_btn->setAutoRaise(true); remove_btn->setAutoRaise(true);
@ -234,13 +235,11 @@ ChartView::ChartView(const QString &id, const Signal *sig, QWidget *parent)
void ChartView::resizeEvent(QResizeEvent *event) { void ChartView::resizeEvent(QResizeEvent *event) {
QChartView::resizeEvent(event); QChartView::resizeEvent(event);
msg_title->setPos(11, 6);
close_btn_proxy->setPos(event->size().width() - close_btn_proxy->size().width() - 11, 8); close_btn_proxy->setPos(event->size().width() - close_btn_proxy->size().width() - 11, 8);
} }
void ChartView::updateTitle() { void ChartView::updateTitle() {
chart()->setTitle(signal->name.c_str()); chart()->setTitle(tr("<font color=\"gray\" text-align:left>%1 %2</font> <b>%3</b>").arg(dbc()->msg(id)->name).arg(id).arg(signal->name.c_str()));
msg_title->setHtml(tr("%1 <font color=\"gray\">%2</font>").arg(dbc()->msg(id)->name.c_str()).arg(id));
} }
void ChartView::updateFromSettings() { void ChartView::updateFromSettings() {
@ -248,7 +247,6 @@ void ChartView::updateFromSettings() {
chart()->setTheme(settings.chart_theme == 0 ? QChart::ChartThemeLight : QChart::QChart::ChartThemeDark); chart()->setTheme(settings.chart_theme == 0 ? QChart::ChartThemeLight : QChart::QChart::ChartThemeDark);
auto color = chart()->titleBrush().color(); auto color = chart()->titleBrush().color();
line_marker->setPen(QPen(color, 2)); line_marker->setPen(QPen(color, 2));
msg_title->setDefaultTextColor(color);
} }
void ChartView::setRange(double min, double max, bool force_update) { void ChartView::setRange(double min, double max, bool force_update) {
@ -265,6 +263,7 @@ void ChartView::adjustChartMargins() {
if (chart()->plotArea().left() != aligned_pos) { if (chart()->plotArea().left() != aligned_pos) {
const float left_margin = chart()->margins().left() + aligned_pos - chart()->plotArea().left(); const float left_margin = chart()->margins().left() + aligned_pos - chart()->plotArea().left();
chart()->setMargins(QMargins(left_margin, 11, 0, 0)); chart()->setMargins(QMargins(left_margin, 11, 0, 0));
updateLineMarker(can->currentSec());
} }
} }
@ -290,7 +289,7 @@ void ChartView::updateSeries(const std::pair<double, double> range) {
double end_ns = (route_start_time + range.second) * 1e9; double end_ns = (route_start_time + range.second) * 1e9;
for (auto it = begin; it != events->end() && (*it)->mono_time <= end_ns; ++it) { for (auto it = begin; it != events->end() && (*it)->mono_time <= end_ns; ++it) {
if ((*it)->which == cereal::Event::Which::CAN) { if ((*it)->which == cereal::Event::Which::CAN) {
for (auto c : (*it)->event.getCan()) { for (const auto &c : (*it)->event.getCan()) {
if (bus == c.getSrc() && address == c.getAddress()) { if (bus == c.getSrc() && address == c.getAddress()) {
auto dat = c.getDat(); auto dat = c.getDat();
double value = get_raw_value((uint8_t *)dat.begin(), dat.size(), *signal); double value = get_raw_value((uint8_t *)dat.begin(), dat.size(), *signal);

@ -44,7 +44,7 @@ private:
QGraphicsItemGroup *item_group; QGraphicsItemGroup *item_group;
QGraphicsLineItem *line_marker, *track_line; QGraphicsLineItem *line_marker, *track_line;
QGraphicsEllipseItem *track_ellipse; QGraphicsEllipseItem *track_ellipse;
QGraphicsTextItem *value_text, *msg_title; QGraphicsTextItem *value_text;
QGraphicsProxyWidget *close_btn_proxy; QGraphicsProxyWidget *close_btn_proxy;
QVector<QPointF> vals; QVector<QPointF> vals;
}; };

@ -0,0 +1,75 @@
#include "tools/cabana/commands.h"
// EditMsgCommand
EditMsgCommand::EditMsgCommand(const QString &id, const QString &title, int size, QUndoCommand *parent)
: id(id), new_title(title), new_size(size), QUndoCommand(parent) {
if (auto msg = dbc()->msg(id)) {
old_title = msg->name;
old_size = msg->size;
}
setText(QObject::tr("Edit message %1:%2").arg(DBCManager::parseId(id).second).arg(title));
}
void EditMsgCommand::undo() {
if (old_title.isEmpty())
dbc()->removeMsg(id);
else
dbc()->updateMsg(id, old_title, old_size);
}
void EditMsgCommand::redo() {
dbc()->updateMsg(id, new_title, new_size);
}
// RemoveMsgCommand
RemoveMsgCommand::RemoveMsgCommand(const QString &id, QUndoCommand *parent) : id(id), QUndoCommand(parent) {
if (auto msg = dbc()->msg(id)) {
message = *msg;
setText(QObject::tr("Remove message %1:%2").arg(DBCManager::parseId(id).second).arg(message.name));
}
}
void RemoveMsgCommand::undo() {
if (!message.name.isEmpty()) {
dbc()->updateMsg(id, message.name, message.size);
for (auto &[name, s] : message.sigs)
dbc()->addSignal(id, s);
}
}
void RemoveMsgCommand::redo() {
if (!message.name.isEmpty())
dbc()->removeMsg(id);
}
// AddSigCommand
AddSigCommand::AddSigCommand(const QString &id, const Signal &sig, QUndoCommand *parent)
: id(id), signal(sig), QUndoCommand(parent) {
setText(QObject::tr("Add signal %1 to %2").arg(sig.name.c_str()).arg(DBCManager::parseId(id).second));
}
void AddSigCommand::undo() { dbc()->removeSignal(id, signal.name.c_str()); }
void AddSigCommand::redo() { dbc()->addSignal(id, signal); }
// RemoveSigCommand
RemoveSigCommand::RemoveSigCommand(const QString &id, const Signal *sig, QUndoCommand *parent)
: id(id), signal(*sig), QUndoCommand(parent) {
setText(QObject::tr("Remove signal %1 from %2").arg(signal.name.c_str()).arg(DBCManager::parseId(id).second));
}
void RemoveSigCommand::undo() { dbc()->addSignal(id, signal); }
void RemoveSigCommand::redo() { dbc()->removeSignal(id, signal.name.c_str()); }
// EditSignalCommand
EditSignalCommand::EditSignalCommand(const QString &id, const Signal *sig, const Signal &new_sig, QUndoCommand *parent)
: id(id), old_signal(*sig), new_signal(new_sig), QUndoCommand(parent) {
setText(QObject::tr("Edit signal %1").arg(old_signal.name.c_str()));
}
void EditSignalCommand::undo() { dbc()->updateSignal(id, new_signal.name.c_str(), old_signal); }
void EditSignalCommand::redo() { dbc()->updateSignal(id, old_signal.name.c_str(), new_signal); }

@ -0,0 +1,63 @@
#pragma once
#include <QUndoCommand>
#include "tools/cabana/canmessages.h"
#include "tools/cabana/dbcmanager.h"
class EditMsgCommand : public QUndoCommand {
public:
EditMsgCommand(const QString &id, const QString &title, int size, QUndoCommand *parent = nullptr);
void undo() override;
void redo() override;
private:
const QString id;
QString old_title, new_title;
int old_size = 0, new_size = 0;
};
class RemoveMsgCommand : public QUndoCommand {
public:
RemoveMsgCommand(const QString &id, QUndoCommand *parent = nullptr);
void undo() override;
void redo() override;
private:
const QString id;
DBCMsg message;
};
class AddSigCommand : public QUndoCommand {
public:
AddSigCommand(const QString &id, const Signal &sig, QUndoCommand *parent = nullptr);
void undo() override;
void redo() override;
private:
const QString id;
Signal signal = {};
};
class RemoveSigCommand : public QUndoCommand {
public:
RemoveSigCommand(const QString &id, const Signal *sig, QUndoCommand *parent = nullptr);
void undo() override;
void redo() override;
private:
const QString id;
Signal signal = {};
};
class EditSignalCommand : public QUndoCommand {
public:
EditSignalCommand(const QString &id, const Signal *sig, const Signal &new_sig, QUndoCommand *parent = nullptr);
void undo() override;
void redo() override;
private:
const QString id;
Signal old_signal = {};
Signal new_signal = {};
};

@ -10,32 +10,36 @@ DBCManager::~DBCManager() {}
void DBCManager::open(const QString &dbc_file_name) { void DBCManager::open(const QString &dbc_file_name) {
dbc = const_cast<DBC *>(dbc_lookup(dbc_file_name.toStdString())); dbc = const_cast<DBC *>(dbc_lookup(dbc_file_name.toStdString()));
updateMsgMap(); initMsgMap();
emit DBCFileChanged();
} }
void DBCManager::open(const QString &name, const QString &content) { void DBCManager::open(const QString &name, const QString &content) {
std::istringstream stream(content.toStdString()); std::istringstream stream(content.toStdString());
dbc = const_cast<DBC *>(dbc_parse_from_stream(name.toStdString(), stream)); dbc = const_cast<DBC *>(dbc_parse_from_stream(name.toStdString(), stream));
updateMsgMap(); initMsgMap();
emit DBCFileChanged();
} }
void DBCManager::updateMsgMap() { void DBCManager::initMsgMap() {
msg_map.clear(); msgs.clear();
for (auto &msg : dbc->msgs) for (auto &msg : dbc->msgs) {
msg_map[msg.address] = &msg; auto &m = msgs[msg.address];
m.name = msg.name.c_str();
m.size = msg.size;
for (auto &s : msg.sigs)
m.sigs[QString::fromStdString(s.name)] = s;
}
emit DBCFileChanged();
} }
QString DBCManager::generateDBC() { QString DBCManager::generateDBC() {
if (!dbc) return {}; if (!dbc) return {};
QString dbc_string; QString dbc_string;
for (auto &m : dbc->msgs) { for (auto &[address, m] : msgs) {
dbc_string += QString("BO_ %1 %2: %3 XXX\n").arg(m.address).arg(m.name.c_str()).arg(m.size); dbc_string += QString("BO_ %1 %2: %3 XXX\n").arg(address).arg(m.name).arg(m.size);
for (auto &sig : m.sigs) { for (auto &[name, sig] : m.sigs) {
dbc_string += QString(" SG_ %1 : %2|%3@%4%5 (%6,%7) [0|0] \"\" XXX\n") dbc_string += QString(" SG_ %1 : %2|%3@%4%5 (%6,%7) [0|0] \"\" XXX\n")
.arg(sig.name.c_str()) .arg(name)
.arg(sig.start_bit) .arg(sig.start_bit)
.arg(sig.size) .arg(sig.size)
.arg(sig.is_little_endian ? '1' : '0') .arg(sig.is_little_endian ? '1' : '0')
@ -49,48 +53,45 @@ QString DBCManager::generateDBC() {
} }
void DBCManager::updateMsg(const QString &id, const QString &name, uint32_t size) { void DBCManager::updateMsg(const QString &id, const QString &name, uint32_t size) {
auto [bus, address] = parseId(id); auto [_, address] = parseId(id);
if (auto m = const_cast<Msg *>(msg(address))) { auto &m = msgs[address];
m->name = name.toStdString(); m.name = name;
m->size = size; m.size = size;
} else {
m = &dbc->msgs.emplace_back(Msg{.address = address, .name = name.toStdString(), .size = size});
msg_map[address] = m;
}
emit msgUpdated(address); emit msgUpdated(address);
} }
void DBCManager::removeMsg(const QString &id) { void DBCManager::removeMsg(const QString &id) {
uint32_t address = parseId(id).second; uint32_t address = parseId(id).second;
auto it = std::find_if(dbc->msgs.begin(), dbc->msgs.end(), [address](auto &m) { return m.address == address; }); msgs.erase(address);
if (it != dbc->msgs.end()) { emit msgRemoved(address);
dbc->msgs.erase(it);
updateMsgMap();
emit msgRemoved(address);
}
} }
void DBCManager::addSignal(const QString &id, const Signal &sig) { void DBCManager::addSignal(const QString &id, const Signal &sig) {
if (Msg *m = const_cast<Msg *>(msg(id))) { if (auto m = const_cast<DBCMsg *>(msg(id))) {
emit signalAdded(&m->sigs.emplace_back(sig)); auto &s = m->sigs[sig.name.c_str()];
s = sig;
emit signalAdded(&s);
} }
} }
void DBCManager::updateSignal(const QString &id, const QString &sig_name, const Signal &sig) { void DBCManager::updateSignal(const QString &id, const QString &sig_name, const Signal &sig) {
if (Msg *m = const_cast<Msg *>(msg(id))) { if (auto m = const_cast<DBCMsg *>(msg(id))) {
auto it = std::find_if(m->sigs.begin(), m->sigs.end(), [=](auto &sig) { return sig_name == sig.name.c_str(); }); // change key name
if (it != m->sigs.end()) { QString new_name = QString::fromStdString(sig.name);
*it = sig; auto node = m->sigs.extract(sig_name);
emit signalUpdated(&(*it)); node.key() = new_name;
} auto it = m->sigs.insert(std::move(node));
auto &s = m->sigs[new_name];
s = sig;
emit signalUpdated(&s);
} }
} }
void DBCManager::removeSignal(const QString &id, const QString &sig_name) { void DBCManager::removeSignal(const QString &id, const QString &sig_name) {
if (Msg *m = const_cast<Msg *>(msg(id))) { if (auto m = const_cast<DBCMsg *>(msg(id))) {
auto it = std::find_if(m->sigs.begin(), m->sigs.end(), [=](auto &sig) { return sig_name == sig.name.c_str(); }); auto it = m->sigs.find(sig_name);
if (it != m->sigs.end()) { if (it != m->sigs.end()) {
emit signalRemoved(&(*it)); emit signalRemoved(&(it->second));
m->sigs.erase(it); m->sigs.erase(it);
} }
} }
@ -164,3 +165,11 @@ std::pair<int, int> getSignalRange(const Signal *s) {
int to = from + s->size - 1; int to = from + s->size - 1;
return {from, to}; return {from, to};
} }
bool operator==(const Signal &l, const Signal &r) {
return l.name == r.name && l.size == r.size &&
l.start_bit == r.start_bit &&
l.msb == r.msb && l.lsb == r.lsb &&
l.is_signed == r.is_signed && l.is_little_endian == r.is_little_endian &&
l.factor == r.factor && l.offset == r.offset;
}

@ -1,9 +1,16 @@
#pragma once #pragma once
#include <map>
#include <QObject> #include <QObject>
#include <QString>
#include "opendbc/can/common_dbc.h" #include "opendbc/can/common_dbc.h"
struct DBCMsg {
QString name;
uint32_t size;
std::map<QString, Signal> sigs;
};
class DBCManager : public QObject { class DBCManager : public QObject {
Q_OBJECT Q_OBJECT
@ -24,11 +31,11 @@ public:
void updateMsg(const QString &id, const QString &name, uint32_t size); void updateMsg(const QString &id, const QString &name, uint32_t size);
void removeMsg(const QString &id); void removeMsg(const QString &id);
inline const DBC *getDBC() const { return dbc; } inline const std::map<uint32_t, DBCMsg> &messages() const { return msgs; }
inline const Msg *msg(const QString &id) const { return msg(parseId(id).second); } inline const DBCMsg *msg(const QString &id) const { return msg(parseId(id).second); }
inline const Msg *msg(uint32_t address) const { inline const DBCMsg *msg(uint32_t address) const {
auto it = msg_map.find(address); auto it = msgs.find(address);
return it != msg_map.end() ? it->second : nullptr; return it != msgs.end() ? &it->second : nullptr;
} }
signals: signals:
@ -40,13 +47,15 @@ signals:
void DBCFileChanged(); void DBCFileChanged();
private: private:
void updateMsgMap(); void initMsgMap();
DBC *dbc = nullptr; DBC *dbc = nullptr;
std::unordered_map<uint32_t, const Msg *> msg_map; std::map<uint32_t, DBCMsg> msgs;
}; };
// TODO: Add helper function in dbc.h // TODO: Add helper function in dbc.h
double get_raw_value(uint8_t *data, size_t data_size, const Signal &sig); double get_raw_value(uint8_t *data, size_t data_size, const Signal &sig);
bool operator==(const Signal &l, const Signal &r);
inline bool operator!=(const Signal &l, const Signal &r) { return !(l == r); }
int bigEndianStartBitsIndex(int start_bit); int bigEndianStartBitsIndex(int start_bit);
int bigEndianBitIndex(int index); int bigEndianBitIndex(int index);
void updateSigSizeParamsFromRange(Signal &s, int start_bit, int size); void updateSigSizeParamsFromRange(Signal &s, int start_bit, int size);
@ -54,5 +63,5 @@ std::pair<int, int> getSignalRange(const Signal *s);
DBCManager *dbc(); DBCManager *dbc();
inline QString msgName(const QString &id, const char *def = "untitled") { inline QString msgName(const QString &id, const char *def = "untitled") {
auto msg = dbc()->msg(id); auto msg = dbc()->msg(id);
return msg ? msg->name.c_str() : def; return msg ? msg->name : def;
} }

@ -6,20 +6,22 @@
#include <QMessageBox> #include <QMessageBox>
#include <QScrollBar> #include <QScrollBar>
#include <QTimer> #include <QTimer>
#include <QVBoxLayout>
#include "selfdrive/ui/qt/util.h" #include "selfdrive/ui/qt/util.h"
#include "tools/cabana/canmessages.h" #include "tools/cabana/canmessages.h"
#include "tools/cabana/commands.h"
#include "tools/cabana/dbcmanager.h" #include "tools/cabana/dbcmanager.h"
// DetailWidget // DetailWidget
DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(charts), QWidget(parent) { DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(charts), QWidget(parent) {
undo_stack = new QUndoStack(this);
QVBoxLayout *main_layout = new QVBoxLayout(this); QVBoxLayout *main_layout = new QVBoxLayout(this);
main_layout->setContentsMargins(0, 0, 0, 0); main_layout->setContentsMargins(0, 0, 0, 0);
main_layout->setSpacing(0); main_layout->setSpacing(0);
// tabbar // tabbar
tabbar = new QTabBar(this); tabbar = new QTabBar(this);
tabbar->setTabsClosable(true); tabbar->setTabsClosable(true);
tabbar->setDrawBase(false); tabbar->setDrawBase(false);
@ -78,9 +80,8 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart
container_layout->addWidget(binary_view); container_layout->addWidget(binary_view);
// signals // signals
signals_container = new QWidget(this); signals_layout = new QVBoxLayout();
signals_container->setLayout(new QVBoxLayout); container_layout->addLayout(signals_layout);
container_layout->addWidget(signals_container);
// history log // history log
history_log = new HistoryLog(this); history_log = new HistoryLog(this);
@ -88,7 +89,7 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart
QObject::connect(binary_view, &BinaryView::resizeSignal, this, &DetailWidget::resizeSignal); QObject::connect(binary_view, &BinaryView::resizeSignal, this, &DetailWidget::resizeSignal);
QObject::connect(binary_view, &BinaryView::addSignal, this, &DetailWidget::addSignal); QObject::connect(binary_view, &BinaryView::addSignal, this, &DetailWidget::addSignal);
QObject::connect(can, &CANMessages::updated, this, &DetailWidget::updateState); QObject::connect(can, &CANMessages::msgsReceived, this, &DetailWidget::updateState);
QObject::connect(dbc(), &DBCManager::DBCFileChanged, [this]() { dbcMsgChanged(); }); QObject::connect(dbc(), &DBCManager::DBCFileChanged, [this]() { dbcMsgChanged(); });
QObject::connect(tabbar, &QTabBar::customContextMenuRequested, this, &DetailWidget::showTabBarContextMenu); QObject::connect(tabbar, &QTabBar::customContextMenuRequested, this, &DetailWidget::showTabBarContextMenu);
QObject::connect(tabbar, &QTabBar::currentChanged, [this](int index) { QObject::connect(tabbar, &QTabBar::currentChanged, [this](int index) {
@ -99,6 +100,10 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart
QObject::connect(tabbar, &QTabBar::tabCloseRequested, tabbar, &QTabBar::removeTab); QObject::connect(tabbar, &QTabBar::tabCloseRequested, tabbar, &QTabBar::removeTab);
QObject::connect(charts, &ChartsWidget::chartOpened, [this](const QString &id, const Signal *sig) { updateChartState(id, sig, true); }); QObject::connect(charts, &ChartsWidget::chartOpened, [this](const QString &id, const Signal *sig) { updateChartState(id, sig, true); });
QObject::connect(charts, &ChartsWidget::chartClosed, [this](const QString &id, const Signal *sig) { updateChartState(id, sig, false); }); QObject::connect(charts, &ChartsWidget::chartClosed, [this](const QString &id, const Signal *sig) { updateChartState(id, sig, false); });
QObject::connect(undo_stack, &QUndoStack::indexChanged, [this]() {
if (undo_stack->count() > 0)
dbcMsgChanged();
});
} }
void DetailWidget::showTabBarContextMenu(const QPoint &pt) { void DetailWidget::showTabBarContextMenu(const QPoint &pt) {
@ -107,103 +112,82 @@ void DetailWidget::showTabBarContextMenu(const QPoint &pt) {
QMenu menu(this); QMenu menu(this);
menu.addAction(tr("Close Other Tabs")); menu.addAction(tr("Close Other Tabs"));
if (menu.exec(tabbar->mapToGlobal(pt))) { if (menu.exec(tabbar->mapToGlobal(pt))) {
tabbar->setCurrentIndex(index); tabbar->moveTab(index, 0);
// remove all tabs before the one to keep tabbar->setCurrentIndex(0);
for (int i = 0; i < index; ++i) { while (tabbar->count() > 1)
tabbar->removeTab(0);
}
// remove all tabs after the one to keep
while (tabbar->count() > 1) {
tabbar->removeTab(1); tabbar->removeTab(1);
}
} }
} }
} }
void DetailWidget::setMessage(const QString &message_id) { void DetailWidget::setMessage(const QString &message_id) {
if (message_id.isEmpty()) return;
int index = -1;
for (int i = 0; i < tabbar->count(); ++i) {
if (tabbar->tabText(i) == message_id) {
index = i;
break;
}
}
msg_id = message_id; msg_id = message_id;
int index = tabbar->count() - 1;
for (/**/; index >= 0 && tabbar->tabText(index) != msg_id; --index) { /**/ }
if (index == -1) { if (index == -1) {
index = tabbar->addTab(message_id); index = tabbar->addTab(message_id);
tabbar->setTabToolTip(index, msgName(message_id)); tabbar->setTabToolTip(index, msgName(message_id));
} }
tabbar->setCurrentIndex(index); tabbar->setCurrentIndex(index);
dbcMsgChanged(); dbcMsgChanged();
scroll->verticalScrollBar()->setValue(0);
} }
void DetailWidget::dbcMsgChanged(int show_form_idx) { void DetailWidget::dbcMsgChanged(int show_form_idx) {
if (msg_id.isEmpty()) return; if (msg_id.isEmpty()) return;
setUpdatesEnabled(false); setUpdatesEnabled(false);
QStringList warnings;
for (auto f : signal_list) f->hide();
const Msg *msg = dbc()->msg(msg_id); binary_view->setMessage(msg_id);
history_log->setMessage(msg_id);
int i = 0;
QStringList warnings;
const DBCMsg *msg = dbc()->msg(msg_id);
if (msg) { if (msg) {
for (int i = 0; i < msg->sigs.size(); ++i) { for (auto &[name, sig] : msg->sigs) {
SignalEdit *form = i < signal_list.size() ? signal_list[i] : nullptr; SignalEdit *form = i < signal_list.size() ? signal_list[i] : nullptr;
if (!form) { if (!form) {
form = new SignalEdit(i); form = new SignalEdit(i);
QObject::connect(form, &SignalEdit::showFormClicked, this, &DetailWidget::showForm);
QObject::connect(form, &SignalEdit::remove, this, &DetailWidget::removeSignal); QObject::connect(form, &SignalEdit::remove, this, &DetailWidget::removeSignal);
QObject::connect(form, &SignalEdit::save, this, &DetailWidget::saveSignal); QObject::connect(form, &SignalEdit::save, this, &DetailWidget::saveSignal);
QObject::connect(form, &SignalEdit::highlight, binary_view, &BinaryView::highlight); QObject::connect(form, &SignalEdit::highlight, binary_view, &BinaryView::highlight);
QObject::connect(binary_view, &BinaryView::signalHovered, form, &SignalEdit::signalHovered); QObject::connect(binary_view, &BinaryView::signalHovered, form, &SignalEdit::signalHovered);
QObject::connect(form, &SignalEdit::showChart, charts, &ChartsWidget::showChart); QObject::connect(form, &SignalEdit::showChart, charts, &ChartsWidget::showChart);
signals_container->layout()->addWidget(form); signals_layout->addWidget(form);
signal_list.push_back(form); signal_list.push_back(form);
} }
form->setSignal(msg_id, &(msg->sigs[i]), i == show_form_idx); form->setSignal(msg_id, &sig);
form->setChartOpened(charts->isChartOpened(msg_id, &(msg->sigs[i]))); form->setChartOpened(charts->isChartOpened(msg_id, &sig));
form->show(); ++i;
} }
if (msg->size != can->lastMessage(msg_id).dat.size()) if (msg->size != can->lastMessage(msg_id).dat.size())
warnings.push_back(tr("Message size (%1) is incorrect.").arg(msg->size)); warnings.push_back(tr("Message size (%1) is incorrect.").arg(msg->size));
} }
for (/**/; i < signal_list.size(); ++i)
signal_list[i]->hide();
toolbar->setVisible(!msg_id.isEmpty()); toolbar->setVisible(!msg_id.isEmpty());
remove_msg_act->setEnabled(msg != nullptr); remove_msg_act->setEnabled(msg != nullptr);
name_label->setText(msgName(msg_id)); name_label->setText(msgName(msg_id));
binary_view->setMessage(msg_id); for (auto s : binary_view->getOverlappingSignals())
history_log->setMessage(msg_id); warnings.push_back(tr("%1 has overlapping bits.").arg(s->name.c_str()));
// Check overlapping bits
if (auto overlapping = binary_view->getOverlappingSignals(); !overlapping.isEmpty()) {
for (auto s : overlapping)
warnings.push_back(tr("%1 has overlapping bits.").arg(s->name.c_str()));
}
warning_label->setText(warnings.join('\n')); warning_label->setText(warnings.join('\n'));
warning_widget->setVisible(!warnings.isEmpty()); warning_widget->setVisible(!warnings.isEmpty());
setUpdatesEnabled(true); QTimer::singleShot(1, [this]() { setUpdatesEnabled(true); });
scroll->verticalScrollBar()->setValue(0);
} }
void DetailWidget::updateState() { void DetailWidget::updateState(const QHash<QString, CanData> * msgs) {
time_label->setText(QString::number(can->currentSec(), 'f', 3)); time_label->setText(QString::number(can->currentSec(), 'f', 3));
if (msg_id.isEmpty()) return; if (!msgs->contains(msg_id))
return;
binary_view->updateState(); binary_view->updateState();
history_log->updateState(); history_log->updateState();
} }
void DetailWidget::showForm() {
SignalEdit *sender = qobject_cast<SignalEdit *>(QObject::sender());
setUpdatesEnabled(false);
for (auto f : signal_list)
f->setFormVisible(f == sender && !f->isFormVisible());
QTimer::singleShot(1, [this]() { setUpdatesEnabled(true); });
}
void DetailWidget::updateChartState(const QString &id, const Signal *sig, bool opened) { void DetailWidget::updateChartState(const QString &id, const Signal *sig, bool opened) {
for (auto f : signal_list) for (auto f : signal_list)
if (f->msg_id == id && f->sig == sig) f->setChartOpened(opened); if (f->msg_id == id && f->sig == sig) f->setChartOpened(opened);
@ -215,45 +199,34 @@ void DetailWidget::editMsg() {
int size = msg ? msg->size : can->lastMessage(id).dat.size(); int size = msg ? msg->size : can->lastMessage(id).dat.size();
EditMessageDialog dlg(id, msgName(id), size, this); EditMessageDialog dlg(id, msgName(id), size, this);
if (dlg.exec()) { if (dlg.exec()) {
dbc()->updateMsg(id, dlg.name_edit->text(), dlg.size_spin->value()); undo_stack->push(new EditMsgCommand(msg_id, dlg.name_edit->text(), dlg.size_spin->value()));
dbcMsgChanged();
} }
} }
void DetailWidget::removeMsg() { void DetailWidget::removeMsg() {
QString id = msg_id; undo_stack->push(new RemoveMsgCommand(msg_id));
if (auto msg = dbc()->msg(id)) {
QString text = tr("Are you sure you want to remove '%1'").arg(msg->name.c_str());
if (QMessageBox::Yes == QMessageBox::question(this, tr("Remove Message"), text)) {
dbc()->removeMsg(id);
dbcMsgChanged();
}
}
} }
void DetailWidget::addSignal(int start_bit, int size, bool little_endian) { void DetailWidget::addSignal(int start_bit, int size, bool little_endian) {
auto msg = dbc()->msg(msg_id); auto msg = dbc()->msg(msg_id);
if (!msg) { if (!msg) {
for (int i = 1; /**/; ++i) { for (int i = 1; /**/; ++i) {
std::string name = "NEW_MSG_" + std::to_string(i); QString name = QString("NEW_MSG_%1").arg(i);
auto it = std::find_if(dbc()->getDBC()->msgs.begin(), dbc()->getDBC()->msgs.end(), [&](auto &m) { return m.name == name; }); auto it = std::find_if(dbc()->messages().begin(), dbc()->messages().end(), [&](auto &m) { return m.second.name == name; });
if (it == dbc()->getDBC()->msgs.end()) { if (it == dbc()->messages().end()) {
dbc()->updateMsg(msg_id, name.c_str(), can->lastMessage(msg_id).dat.size()); undo_stack->push(new EditMsgCommand(msg_id, name, can->lastMessage(msg_id).dat.size()));
msg = dbc()->msg(msg_id); msg = dbc()->msg(msg_id);
break; break;
} }
} }
} }
Signal sig = {}; Signal sig = {.is_little_endian = little_endian};
for (int i = 1; /**/; ++i) { for (int i = 1; /**/; ++i) {
sig.name = "NEW_SIGNAL_" + std::to_string(i); sig.name = "NEW_SIGNAL_" + std::to_string(i);
auto it = std::find_if(msg->sigs.begin(), msg->sigs.end(), [&](auto &s) { return sig.name == s.name; }); if (msg->sigs.count(sig.name.c_str()) == 0) break;
if (it == msg->sigs.end()) break;
} }
sig.is_little_endian = little_endian;
updateSigSizeParamsFromRange(sig, start_bit, size); updateSigSizeParamsFromRange(sig, start_bit, size);
dbc()->addSignal(msg_id, sig); undo_stack->push(new AddSigCommand(msg_id, sig));
dbcMsgChanged(msg->sigs.size() - 1);
} }
void DetailWidget::resizeSignal(const Signal *sig, int start_bit, int size) { void DetailWidget::resizeSignal(const Signal *sig, int start_bit, int size) {
@ -265,14 +238,13 @@ void DetailWidget::resizeSignal(const Signal *sig, int start_bit, int size) {
void DetailWidget::saveSignal(const Signal *sig, const Signal &new_sig) { void DetailWidget::saveSignal(const Signal *sig, const Signal &new_sig) {
auto msg = dbc()->msg(msg_id); auto msg = dbc()->msg(msg_id);
if (new_sig.name != sig->name) { if (new_sig.name != sig->name) {
auto it = std::find_if(msg->sigs.begin(), msg->sigs.end(), [&](auto &s) { return s.name == new_sig.name; }); auto it = msg->sigs.find(new_sig.name.c_str());
if (it != msg->sigs.end()) { if (it != msg->sigs.end()) {
QString warning_str = tr("There is already a signal with the same name '%1'").arg(new_sig.name.c_str()); QString warning_str = tr("There is already a signal with the same name '%1'").arg(new_sig.name.c_str());
QMessageBox::warning(this, tr("Failed to save signal"), warning_str); QMessageBox::warning(this, tr("Failed to save signal"), warning_str);
return; return;
} }
} }
auto [start, end] = getSignalRange(&new_sig); auto [start, end] = getSignalRange(&new_sig);
if (start < 0 || end >= msg->size * 8) { if (start < 0 || end >= msg->size * 8) {
QString warning_str = tr("Signal size [%1] exceed limit").arg(new_sig.size); QString warning_str = tr("Signal size [%1] exceed limit").arg(new_sig.size);
@ -280,16 +252,11 @@ void DetailWidget::saveSignal(const Signal *sig, const Signal &new_sig) {
return; return;
} }
dbc()->updateSignal(msg_id, sig->name.c_str(), new_sig); undo_stack->push(new EditSignalCommand(msg_id, sig, new_sig));
dbcMsgChanged();
} }
void DetailWidget::removeSignal(const Signal *sig) { void DetailWidget::removeSignal(const Signal *sig) {
QString text = tr("Are you sure you want to remove signal '%1'").arg(sig->name.c_str()); undo_stack->push(new RemoveSigCommand(msg_id, sig));
if (QMessageBox::Yes == QMessageBox::question(this, tr("Remove signal"), text)) {
dbc()->removeSignal(msg_id, sig->name.c_str());
dbcMsgChanged();
}
} }
// EditMessageDialog // EditMessageDialog
@ -300,6 +267,7 @@ EditMessageDialog::EditMessageDialog(const QString &msg_id, const QString &title
form_layout->addRow("ID", new QLabel(msg_id)); form_layout->addRow("ID", new QLabel(msg_id));
name_edit = new QLineEdit(title, this); name_edit = new QLineEdit(title, this);
name_edit->setValidator(new QRegExpValidator(QRegExp("^(\\w+)"), name_edit));
form_layout->addRow(tr("Name"), name_edit); form_layout->addRow(tr("Name"), name_edit);
size_spin = new QSpinBox(this); size_spin = new QSpinBox(this);

@ -3,6 +3,7 @@
#include <QScrollArea> #include <QScrollArea>
#include <QTabBar> #include <QTabBar>
#include <QToolBar> #include <QToolBar>
#include <QUndoStack>
#include "tools/cabana/binaryview.h" #include "tools/cabana/binaryview.h"
#include "tools/cabana/chartswidget.h" #include "tools/cabana/chartswidget.h"
@ -10,8 +11,6 @@
#include "tools/cabana/signaledit.h" #include "tools/cabana/signaledit.h"
class EditMessageDialog : public QDialog { class EditMessageDialog : public QDialog {
Q_OBJECT
public: public:
EditMessageDialog(const QString &msg_id, const QString &title, int size, QWidget *parent); EditMessageDialog(const QString &msg_id, const QString &title, int size, QWidget *parent);
@ -26,6 +25,7 @@ public:
DetailWidget(ChartsWidget *charts, QWidget *parent); DetailWidget(ChartsWidget *charts, QWidget *parent);
void setMessage(const QString &message_id); void setMessage(const QString &message_id);
void dbcMsgChanged(int show_form_idx = -1); void dbcMsgChanged(int show_form_idx = -1);
QUndoStack *undo_stack = nullptr;
private: private:
void updateChartState(const QString &id, const Signal *sig, bool opened); void updateChartState(const QString &id, const Signal *sig, bool opened);
@ -36,13 +36,12 @@ private:
void removeSignal(const Signal *sig); void removeSignal(const Signal *sig);
void editMsg(); void editMsg();
void removeMsg(); void removeMsg();
void showForm(); void updateState(const QHash<QString, CanData> * msgs);
void updateState();
QString msg_id; QString msg_id;
QLabel *name_label, *time_label, *warning_label; QLabel *name_label, *time_label, *warning_label;
QWidget *warning_widget; QWidget *warning_widget;
QWidget *signals_container; QVBoxLayout *signals_layout;
QTabBar *tabbar; QTabBar *tabbar;
QToolBar *toolbar; QToolBar *toolbar;
QAction *remove_msg_act; QAction *remove_msg_act;

@ -4,6 +4,10 @@
// HistoryLogModel // HistoryLogModel
inline const Signal &get_signal(const DBCMsg *m, int index) {
return std::next(m->sigs.begin(), index)->second;
}
QVariant HistoryLogModel::data(const QModelIndex &index, int role) const { QVariant HistoryLogModel::data(const QModelIndex &index, int role) const {
bool has_signal = dbc_msg && !dbc_msg->sigs.empty(); bool has_signal = dbc_msg && !dbc_msg->sigs.empty();
if (role == Qt::DisplayRole) { if (role == Qt::DisplayRole) {
@ -11,7 +15,7 @@ QVariant HistoryLogModel::data(const QModelIndex &index, int role) const {
if (index.column() == 0) { if (index.column() == 0) {
return QString::number(m.ts, 'f', 2); return QString::number(m.ts, 'f', 2);
} }
return has_signal ? QString::number(get_raw_value((uint8_t *)m.dat.begin(), m.dat.size(), dbc_msg->sigs[index.column() - 1])) return has_signal ? QString::number(get_raw_value((uint8_t *)m.dat.begin(), m.dat.size(), get_signal(dbc_msg, index.column() - 1)))
: toHex(m.dat); : toHex(m.dat);
} else if (role == Qt::FontRole && index.column() == 1 && !has_signal) { } else if (role == Qt::FontRole && index.column() == 1 && !has_signal) {
return QFontDatabase::systemFont(QFontDatabase::FixedFont); return QFontDatabase::systemFont(QFontDatabase::FixedFont);
@ -37,7 +41,7 @@ QVariant HistoryLogModel::headerData(int section, Qt::Orientation orientation, i
if (section == 0) { if (section == 0) {
return "Time"; return "Time";
} }
return has_signal ? QString::fromStdString(dbc_msg->sigs[section - 1].name).replace('_', ' ') : "Data"; return has_signal ? QString::fromStdString(get_signal(dbc_msg, section - 1).name).replace('_', ' ') : "Data";
} else if (role == Qt::BackgroundRole && section > 0 && has_signal) { } else if (role == Qt::BackgroundRole && section > 0 && has_signal) {
return QBrush(QColor(getColor(section - 1))); return QBrush(QColor(getColor(section - 1)));
} }

@ -28,7 +28,7 @@ private:
QString msg_id; QString msg_id;
int row_count = 0; int row_count = 0;
int column_count = 2; int column_count = 2;
const Msg *dbc_msg = nullptr; const DBCMsg *dbc_msg = nullptr;
std::deque<CanData> messages; std::deque<CanData> messages;
}; };

@ -1,5 +1,6 @@
#include "tools/cabana/mainwin.h" #include "tools/cabana/mainwin.h"
#include <iostream>
#include <QApplication> #include <QApplication>
#include <QClipboard> #include <QClipboard>
#include <QCompleter> #include <QCompleter>
@ -12,12 +13,15 @@
#include <QMessageBox> #include <QMessageBox>
#include <QScreen> #include <QScreen>
#include <QToolBar> #include <QToolBar>
#include <QUndoView>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QWidgetAction>
#include "tools/replay/util.h" #include "tools/replay/util.h"
static MainWindow *main_win = nullptr; static MainWindow *main_win = nullptr;
void qLogMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) { void qLogMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) {
if (type == QtDebugMsg) std::cout << msg.toStdString() << std::endl;
if (main_win) emit main_win->showMessage(msg, 0); if (main_win) emit main_win->showMessage(msg, 0);
} }
@ -41,9 +45,7 @@ MainWindow::MainWindow() : QMainWindow() {
dbc_combo->addItem(QString::fromStdString(name)); dbc_combo->addItem(QString::fromStdString(name));
} }
dbc_combo->model()->sort(0); dbc_combo->model()->sort(0);
dbc_combo->setEditable(true);
dbc_combo->setInsertPolicy(QComboBox::NoInsert); dbc_combo->setInsertPolicy(QComboBox::NoInsert);
dbc_combo->completer()->setCompletionMode(QCompleter::PopupCompletion);
messages_layout->addWidget(dbc_combo); messages_layout->addWidget(dbc_combo);
messages_widget = new MessagesWidget(this); messages_widget = new MessagesWidget(this);
@ -102,9 +104,13 @@ MainWindow::MainWindow() : QMainWindow() {
QObject::connect(charts_widget, &ChartsWidget::rangeChanged, video_widget, &VideoWidget::rangeChanged); QObject::connect(charts_widget, &ChartsWidget::rangeChanged, video_widget, &VideoWidget::rangeChanged);
QObject::connect(can, &CANMessages::streamStarted, this, &MainWindow::loadDBCFromFingerprint); QObject::connect(can, &CANMessages::streamStarted, this, &MainWindow::loadDBCFromFingerprint);
QObject::connect(dbc(), &DBCManager::DBCFileChanged, [this]() { QObject::connect(dbc(), &DBCManager::DBCFileChanged, [this]() {
detail_widget->undo_stack->clear();
dbc_combo->setCurrentText(QFileInfo(dbc()->name()).baseName()); dbc_combo->setCurrentText(QFileInfo(dbc()->name()).baseName());
setWindowTitle(tr("%1 - Cabana").arg(dbc()->name())); setWindowTitle(tr("%1 - Cabana").arg(dbc()->name()));
}); });
QObject::connect(detail_widget->undo_stack, &QUndoStack::indexChanged, [this](int index) {
setWindowTitle(tr("%1%2 - Cabana").arg(index > 0 ? "* " : "").arg(dbc()->name()));
});
} }
void MainWindow::createActions() { void MainWindow::createActions() {
@ -116,6 +122,23 @@ void MainWindow::createActions() {
file_menu->addAction(tr("Copy DBC To Clipboard"), this, &MainWindow::saveDBCToClipboard); file_menu->addAction(tr("Copy DBC To Clipboard"), this, &MainWindow::saveDBCToClipboard);
file_menu->addSeparator(); file_menu->addSeparator();
file_menu->addAction(tr("Settings..."), this, &MainWindow::setOption); file_menu->addAction(tr("Settings..."), this, &MainWindow::setOption);
QMenu *edit_menu = menuBar()->addMenu(tr("&Edit"));
auto undo_act = detail_widget->undo_stack->createUndoAction(this, tr("&Undo"));
undo_act->setShortcuts(QKeySequence::Undo);
edit_menu->addAction(undo_act);
auto redo_act = detail_widget->undo_stack->createRedoAction(this, tr("&Rndo"));
redo_act->setShortcuts(QKeySequence::Redo);
edit_menu->addAction(redo_act);
edit_menu->addSeparator();
QMenu *commands_menu = edit_menu->addMenu(tr("Command &List"));
auto undo_view = new QUndoView(detail_widget->undo_stack);
undo_view->setWindowTitle(tr("Command List"));
QWidgetAction *commands_act = new QWidgetAction(this);
commands_act->setDefaultWidget(undo_view);
commands_menu->addAction(commands_act);
QMenu *help_menu = menuBar()->addMenu(tr("&Help")); QMenu *help_menu = menuBar()->addMenu(tr("&Help"));
help_menu->addAction(tr("About &Qt"), qApp, &QApplication::aboutQt); help_menu->addAction(tr("About &Qt"), qApp, &QApplication::aboutQt);
} }
@ -135,8 +158,9 @@ void MainWindow::loadDBCFromName(const QString &name) {
} }
void MainWindow::loadDBCFromFile() { void MainWindow::loadDBCFromFile() {
QString file_name = QFileDialog::getOpenFileName(this, tr("Open File"), QDir::homePath(), "DBC (*.dbc)"); QString file_name = QFileDialog::getOpenFileName(this, tr("Open File"), settings.last_dir, "DBC (*.dbc)");
if (!file_name.isEmpty()) { if (!file_name.isEmpty()) {
settings.last_dir = QFileInfo(file_name).absolutePath();
QFile file(file_name); QFile file(file_name);
if (file.open(QIODevice::ReadOnly)) { if (file.open(QIODevice::ReadOnly)) {
auto dbc_name = QFileInfo(file_name).baseName(); auto dbc_name = QFileInfo(file_name).baseName();
@ -164,11 +188,13 @@ void MainWindow::loadDBCFromFingerprint() {
void MainWindow::saveDBCToFile() { void MainWindow::saveDBCToFile() {
QString file_name = QFileDialog::getSaveFileName(this, tr("Save File"), QString file_name = QFileDialog::getSaveFileName(this, tr("Save File"),
QDir::homePath() + "/untitled.dbc", tr("DBC (*.dbc)")); QDir::cleanPath(settings.last_dir + "/untitled.dbc"), tr("DBC (*.dbc)"));
if (!file_name.isEmpty()) { if (!file_name.isEmpty()) {
settings.last_dir = QFileInfo(file_name).absolutePath();
QFile file(file_name); QFile file(file_name);
if (file.open(QIODevice::WriteOnly)) if (file.open(QIODevice::WriteOnly))
file.write(dbc()->generateDBC().toUtf8()); file.write(dbc()->generateDBC().toUtf8());
detail_widget->undo_stack->clear();
} }
} }
@ -204,9 +230,21 @@ void MainWindow::dockCharts(bool dock) {
} }
void MainWindow::closeEvent(QCloseEvent *event) { void MainWindow::closeEvent(QCloseEvent *event) {
if (detail_widget->undo_stack->index() > 0) {
auto ret = QMessageBox::question(this, tr("Unsaved Changes"),
tr("Are you sure you want to exit without saving?\nAny unsaved changes will be lost."),
QMessageBox::Yes | QMessageBox::No);
if (ret == QMessageBox::No) {
event->ignore();
return;
}
}
main_win = nullptr; main_win = nullptr;
if (floating_window) if (floating_window)
floating_window->deleteLater(); floating_window->deleteLater();
settings.save();
QWidget::closeEvent(event); QWidget::closeEvent(event);
} }

@ -1,6 +1,7 @@
#include "tools/cabana/settings.h" #include "tools/cabana/settings.h"
#include <QDialogButtonBox> #include <QDialogButtonBox>
#include <QDir>
#include <QFormLayout> #include <QFormLayout>
#include <QSettings> #include <QSettings>
@ -19,7 +20,7 @@ void Settings::save() {
s.setValue("chart_height", chart_height); s.setValue("chart_height", chart_height);
s.setValue("chart_theme", chart_theme); s.setValue("chart_theme", chart_theme);
s.setValue("max_chart_x_range", max_chart_x_range); s.setValue("max_chart_x_range", max_chart_x_range);
emit changed(); s.setValue("last_dir", last_dir);
} }
void Settings::load() { void Settings::load() {
@ -30,6 +31,7 @@ void Settings::load() {
chart_height = s.value("chart_height", 200).toInt(); chart_height = s.value("chart_height", 200).toInt();
chart_theme = s.value("chart_theme", 0).toInt(); chart_theme = s.value("chart_theme", 0).toInt();
max_chart_x_range = s.value("max_chart_x_range", 3 * 60).toInt(); max_chart_x_range = s.value("max_chart_x_range", 3 * 60).toInt();
last_dir = s.value("last_dir", QDir::homePath()).toString();
} }
// SettingsDlg // SettingsDlg
@ -90,4 +92,5 @@ void SettingsDlg::save() {
settings.max_chart_x_range = max_chart_x_range->value() * 60; settings.max_chart_x_range = max_chart_x_range->value() * 60;
settings.save(); settings.save();
accept(); accept();
emit settings.changed();
} }

@ -18,6 +18,7 @@ public:
int chart_height = 200; int chart_height = 200;
int chart_theme = 0; int chart_theme = 0;
int max_chart_x_range = 3 * 60; // 3 minutes int max_chart_x_range = 3 * 60; // 3 minutes
QString last_dir;
signals: signals:
void changed(); void changed();

@ -1,12 +1,11 @@
#include "tools/cabana/signaledit.h" #include "tools/cabana/signaledit.h"
#include <QDialogButtonBox>
#include <QDoubleValidator> #include <QDoubleValidator>
#include <QFormLayout> #include <QFormLayout>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QMessageBox>
#include <QRadioButton>
#include <QScrollArea> #include <QScrollArea>
#include <QTimer>
#include <QToolBar>
#include <QVBoxLayout> #include <QVBoxLayout>
#include "selfdrive/ui/qt/util.h" #include "selfdrive/ui/qt/util.h"
@ -15,9 +14,9 @@
SignalForm::SignalForm(QWidget *parent) : QWidget(parent) { SignalForm::SignalForm(QWidget *parent) : QWidget(parent) {
QFormLayout *form_layout = new QFormLayout(this); QFormLayout *form_layout = new QFormLayout(this);
form_layout->setContentsMargins(0, 0, 0, 0);
name = new QLineEdit(); name = new QLineEdit();
name->setValidator(new QRegExpValidator(QRegExp("^(\\w+)"), name));
form_layout->addRow(tr("Name"), name); form_layout->addRow(tr("Name"), name);
size = new QSpinBox(); size = new QSpinBox();
@ -58,6 +57,13 @@ SignalForm::SignalForm(QWidget *parent) : QWidget(parent) {
form_layout->addRow(tr("Maximum value"), max_val); form_layout->addRow(tr("Maximum value"), max_val);
val_desc = new QLineEdit(); val_desc = new QLineEdit();
form_layout->addRow(tr("Value descriptions"), val_desc); form_layout->addRow(tr("Value descriptions"), val_desc);
QObject::connect(name, &QLineEdit::textEdited, this, &SignalForm::changed);
QObject::connect(factor, &QLineEdit::textEdited, this, &SignalForm::changed);
QObject::connect(offset, &QLineEdit::textEdited, this, &SignalForm::changed);
QObject::connect(sign, SIGNAL(activated(int)), SIGNAL(changed()));
QObject::connect(endianness, SIGNAL(activated(int)), SIGNAL(changed()));
QObject::connect(size, SIGNAL(valueChanged(int)), SIGNAL(changed()));
} }
// SignalEdit // SignalEdit
@ -65,38 +71,45 @@ SignalForm::SignalForm(QWidget *parent) : QWidget(parent) {
SignalEdit::SignalEdit(int index, QWidget *parent) : form_idx(index), QWidget(parent) { SignalEdit::SignalEdit(int index, QWidget *parent) : form_idx(index), QWidget(parent) {
QVBoxLayout *main_layout = new QVBoxLayout(this); QVBoxLayout *main_layout = new QVBoxLayout(this);
main_layout->setContentsMargins(0, 0, 0, 0); main_layout->setContentsMargins(0, 0, 0, 0);
main_layout->setSpacing(0);
// title bar // title bar
QHBoxLayout *title_layout = new QHBoxLayout(); auto title_bar = new QWidget(this);
icon = new QLabel(); title_bar->setFixedHeight(32);
QHBoxLayout *title_layout = new QHBoxLayout(title_bar);
title_layout->setContentsMargins(0, 0, 0, 0);
title_bar->setStyleSheet("QToolButton {width:15px;height:15px;font-size:15px}");
color_label = new QLabel(this);
color_label->setFixedWidth(25);
color_label->setContentsMargins(5, 0, 0, 0);
title_layout->addWidget(color_label);
icon = new QLabel(this);
title_layout->addWidget(icon); title_layout->addWidget(icon);
title = new ElidedLabel(this); title = new ElidedLabel(this);
title->setStyleSheet(QString("font-weight:bold; color:%1").arg(getColor(index))); title->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
title_layout->addWidget(title, 1); title_layout->addWidget(title);
QPushButton *seek_btn = new QPushButton(""); plot_btn = new QToolButton(this);
seek_btn->setStyleSheet("QPushButton{font-weight:bold;font-size:18px}"); plot_btn->setText("📈");
plot_btn->setCheckable(true);
plot_btn->setAutoRaise(true);
title_layout->addWidget(plot_btn);
auto seek_btn = new QToolButton(this);
seek_btn->setIcon(QIcon::fromTheme("edit-find"));
seek_btn->setAutoRaise(true);
seek_btn->setToolTip(tr("Find signal values")); seek_btn->setToolTip(tr("Find signal values"));
seek_btn->setFixedSize(25, 25);
title_layout->addWidget(seek_btn); title_layout->addWidget(seek_btn);
auto remove_btn = new QToolButton(this);
plot_btn = new QPushButton(this); remove_btn->setAutoRaise(true);
plot_btn->setStyleSheet("QPushButton {font-size:18px}"); remove_btn->setText("x");
plot_btn->setFixedSize(25, 25); remove_btn->setToolTip(tr("Remove signal"));
title_layout->addWidget(plot_btn); title_layout->addWidget(remove_btn);
main_layout->addLayout(title_layout); main_layout->addWidget(title_bar);
// signal form // signal form
form_container = new QWidget(this); form = new SignalForm(this);
QVBoxLayout *v_layout = new QVBoxLayout(form_container); form->setVisible(false);
QHBoxLayout *h = new QHBoxLayout(); main_layout->addWidget(form);
QPushButton *remove_btn = new QPushButton(tr("Remove Signal"));
h->addWidget(remove_btn);
h->addStretch();
QPushButton *save_btn = new QPushButton(tr("Save"));
h->addWidget(save_btn);
v_layout->addLayout(h);
main_layout->addWidget(form_container);
// bottom line // bottom line
QFrame *hline = new QFrame(); QFrame *hline = new QFrame();
@ -104,25 +117,27 @@ SignalEdit::SignalEdit(int index, QWidget *parent) : form_idx(index), QWidget(pa
hline->setFrameShadow(QFrame::Sunken); hline->setFrameShadow(QFrame::Sunken);
main_layout->addWidget(hline); main_layout->addWidget(hline);
QObject::connect(remove_btn, &QPushButton::clicked, [this]() { emit remove(this->sig); });
QObject::connect(title, &ElidedLabel::clicked, this, &SignalEdit::showFormClicked); QObject::connect(title, &ElidedLabel::clicked, this, &SignalEdit::showFormClicked);
QObject::connect(save_btn, &QPushButton::clicked, this, &SignalEdit::saveSignal); QObject::connect(plot_btn, &QToolButton::clicked, [this](bool checked) { emit showChart(msg_id, sig, checked); });
QObject::connect(plot_btn, &QPushButton::clicked, [this]() { emit showChart(msg_id, sig, !chart_opened); }); QObject::connect(seek_btn, &QToolButton::clicked, [this]() { SignalFindDlg(msg_id, sig, this).exec(); });
QObject::connect(seek_btn, &QPushButton::clicked, [this]() { QObject::connect(remove_btn, &QToolButton::clicked, [this]() { emit remove(sig); });
SignalFindDlg dlg(msg_id, sig, this); QObject::connect(form, &SignalForm::changed, this, &SignalEdit::saveSignal);
dlg.exec();
});
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
} }
void SignalEdit::setSignal(const QString &message_id, const Signal *signal, bool show_form) { void SignalEdit::setSignal(const QString &message_id, const Signal *signal) {
msg_id = message_id;
sig = signal; sig = signal;
title->setText(QString("%1. %2").arg(form_idx + 1).arg(sig->name.c_str())); updateForm(msg_id == message_id && form->isVisible());
setFormVisible(show_form); msg_id = message_id;
color_label->setText(QString::number(form_idx + 1));
color_label->setStyleSheet(QString("background-color:%1").arg(getColor(form_idx)));
title->setText(sig->name.c_str());
show();
} }
void SignalEdit::saveSignal() { void SignalEdit::saveSignal() {
if (!sig || !form->changed_by_user) return;
Signal s = *sig; Signal s = *sig;
s.name = form->name->text().toStdString(); s.name = form->name->text().toStdString();
s.size = form->size->text().toInt(); s.size = form->size->text().toInt();
@ -148,38 +163,50 @@ void SignalEdit::saveSignal() {
s.lsb = bigEndianStartBitsIndex(bigEndianBitIndex(s.start_bit) + s.size - 1); s.lsb = bigEndianStartBitsIndex(bigEndianBitIndex(s.start_bit) + s.size - 1);
s.msb = s.start_bit; s.msb = s.start_bit;
} }
title->setText(QString("%1. %2").arg(form_idx + 1).arg(form->name->text())); if (s != *sig)
emit save(this->sig, s); emit save(this->sig, s);
} }
void SignalEdit::setChartOpened(bool opened) { void SignalEdit::setChartOpened(bool opened) {
plot_btn->setText(opened ? "" : "📈"); plot_btn->setToolTip(opened ? tr("Close Plot") : tr("Show Plot"));
plot_btn->setToolTip(opened ? tr("Close Plot") :tr("Show Plot")); plot_btn->setChecked(opened);
chart_opened = opened;
} }
void SignalEdit::setFormVisible(bool visible) { void SignalEdit::updateForm(bool visible) {
if (visible) { if (visible && sig) {
if (!form) { form->changed_by_user = false;
form = new SignalForm(this);
((QVBoxLayout *)form_container->layout())->insertWidget(0, form);
}
form->name->setText(sig->name.c_str()); form->name->setText(sig->name.c_str());
form->size->setValue(sig->size);
form->endianness->setCurrentIndex(sig->is_little_endian ? 0 : 1); form->endianness->setCurrentIndex(sig->is_little_endian ? 0 : 1);
form->sign->setCurrentIndex(sig->is_signed ? 0 : 1); form->sign->setCurrentIndex(sig->is_signed ? 0 : 1);
form->factor->setText(QString::number(sig->factor)); form->factor->setText(QString::number(sig->factor));
form->offset->setText(QString::number(sig->offset)); form->offset->setText(QString::number(sig->offset));
form->msb->setText(QString::number(sig->msb)); form->msb->setText(QString::number(sig->msb));
form->lsb->setText(QString::number(sig->lsb)); form->lsb->setText(QString::number(sig->lsb));
form->size->setValue(sig->size);
form->changed_by_user = true;
} }
form_container->setVisible(visible); form->setVisible(visible);
icon->setText(visible ? "" : ">"); icon->setText(visible ? "" : "> ");
}
void SignalEdit::showFormClicked() {
parentWidget()->setUpdatesEnabled(false);
for (auto &edit : parentWidget()->findChildren<SignalEdit*>())
edit->updateForm(edit == this && !form->isVisible());
QTimer::singleShot(1, [this]() { parentWidget()->setUpdatesEnabled(true); });
} }
void SignalEdit::signalHovered(const Signal *s) { void SignalEdit::signalHovered(const Signal *s) {
auto color = sig == s ? hoverColor(getColor(form_idx)) : QColor(getColor(form_idx)); auto bg_color = sig == s ? hoverColor(getColor(form_idx)) : QColor(getColor(form_idx));
title->setStyleSheet(QString("font-weight:bold; color:%1").arg(color.name())); auto color = sig == s ? "white" : "black";
color_label->setStyleSheet(QString("color:%1; background-color:%2").arg(color).arg(bg_color.name()));
}
void SignalEdit::hideEvent(QHideEvent *event) {
msg_id = "";
sig = nullptr;
updateForm(false);
QWidget::hideEvent(event);
} }
void SignalEdit::enterEvent(QEvent *event) { void SignalEdit::enterEvent(QEvent *event) {
@ -204,7 +231,7 @@ SignalFindDlg::SignalFindDlg(const QString &id, const Signal *signal, QWidget *p
comp_box->addItems({">", "=", "<"}); comp_box->addItems({">", "=", "<"});
h->addWidget(comp_box); h->addWidget(comp_box);
QLineEdit *value_edit = new QLineEdit("0", this); QLineEdit *value_edit = new QLineEdit("0", this);
value_edit->setValidator( new QDoubleValidator(-500000, 500000, 6, this) ); value_edit->setValidator(new QDoubleValidator(-500000, 500000, 6, this));
h->addWidget(value_edit, 1); h->addWidget(value_edit, 1);
QPushButton *search_btn = new QPushButton(tr("Find"), this); QPushButton *search_btn = new QPushButton(tr("Find"), this);
h->addWidget(search_btn); h->addWidget(search_btn);

@ -4,8 +4,8 @@
#include <QDialog> #include <QDialog>
#include <QLabel> #include <QLabel>
#include <QLineEdit> #include <QLineEdit>
#include <QPushButton>
#include <QSpinBox> #include <QSpinBox>
#include <QToolButton>
#include "selfdrive/ui/qt/widgets/controls.h" #include "selfdrive/ui/qt/widgets/controls.h"
@ -13,13 +13,17 @@
#include "tools/cabana/dbcmanager.h" #include "tools/cabana/dbcmanager.h"
class SignalForm : public QWidget { class SignalForm : public QWidget {
Q_OBJECT
public: public:
SignalForm(QWidget *parent); SignalForm(QWidget *parent);
QLineEdit *name, *unit, *comment, *val_desc, *offset, *factor, *min_val, *max_val; QLineEdit *name, *unit, *comment, *val_desc, *offset, *factor, *min_val, *max_val;
QLabel *lsb, *msb; QLabel *lsb, *msb;
QSpinBox *size; QSpinBox *size;
QComboBox *sign, *endianness; QComboBox *sign, *endianness;
bool changed_by_user = false;
signals:
void changed();
}; };
class SignalEdit : public QWidget { class SignalEdit : public QWidget {
@ -27,38 +31,35 @@ class SignalEdit : public QWidget {
public: public:
SignalEdit(int index, QWidget *parent = nullptr); SignalEdit(int index, QWidget *parent = nullptr);
void setSignal(const QString &msg_id, const Signal *sig, bool show_form); void setSignal(const QString &msg_id, const Signal *sig);
void setChartOpened(bool opened); void setChartOpened(bool opened);
void setFormVisible(bool show);
void signalHovered(const Signal *sig); void signalHovered(const Signal *sig);
inline bool isFormVisible() const { return form_container->isVisible(); }
const Signal *sig = nullptr; const Signal *sig = nullptr;
QString msg_id; QString msg_id;
signals: signals:
void highlight(const Signal *sig); void highlight(const Signal *sig);
void showChart(const QString &name, const Signal *sig, bool show); void showChart(const QString &name, const Signal *sig, bool show);
void showFormClicked();
void remove(const Signal *sig); void remove(const Signal *sig);
void save(const Signal *sig, const Signal &new_sig); void save(const Signal *sig, const Signal &new_sig);
protected: protected:
void hideEvent(QHideEvent *event) override;
void enterEvent(QEvent *event) override; void enterEvent(QEvent *event) override;
void leaveEvent(QEvent *event) override; void leaveEvent(QEvent *event) override;
void saveSignal(); void saveSignal();
void updateForm(bool show);
void showFormClicked();
SignalForm *form = nullptr; SignalForm *form = nullptr;
ElidedLabel *title; ElidedLabel *title;
QWidget *form_container; QLabel *color_label;
QLabel *icon; QLabel *icon;
int form_idx = 0; int form_idx = 0;
bool chart_opened = false; QToolButton *plot_btn;
QPushButton *plot_btn;
}; };
class SignalFindDlg : public QDialog { class SignalFindDlg : public QDialog {
Q_OBJECT
public: public:
SignalFindDlg(const QString &id, const Signal *signal, QWidget *parent); SignalFindDlg(const QString &id, const Signal *signal, QWidget *parent);
}; };

@ -1,35 +1,67 @@
#include "opendbc/can/common.h"
#undef INFO
#include "catch2/catch.hpp" #include "catch2/catch.hpp"
#include "tools/cabana/dbcmanager.h" #include "tools/cabana/dbcmanager.h"
#include "tools/replay/logreader.h"
// demo route, first segment
const std::string TEST_RLOG_URL = "https://commadata2.blob.core.windows.net/commadata2/4cf7a6ad03080c90/2021-09-29--13-46-36/0/rlog.bz2";
TEST_CASE("DBCManager::generateDBC") { TEST_CASE("DBCManager::generateDBC") {
DBCManager dbc_origin(nullptr); DBCManager dbc_origin(nullptr);
dbc_origin.open("toyota_new_mc_pt_generated"); dbc_origin.open("toyota_new_mc_pt_generated");
QString dbc_string = dbc_origin.generateDBC();
DBCManager dbc_from_generated(nullptr); DBCManager dbc_from_generated(nullptr);
dbc_from_generated.open("", dbc_string); dbc_from_generated.open("", dbc_origin.generateDBC());
auto &msgs = dbc_origin.messages();
auto &new_msgs = dbc_from_generated.messages();
REQUIRE(msgs.size() == new_msgs.size());
for (auto &[address, m] : msgs) {
auto new_m = new_msgs.at(address);
REQUIRE(m.name == new_m.name);
REQUIRE(m.size == new_m.size);
REQUIRE(m.sigs.size() == new_m.sigs.size());
for (auto &[name, sig] : m.sigs)
REQUIRE(sig == new_m.sigs[name]);
}
}
TEST_CASE("Parse can messages") {
DBCManager dbc(nullptr);
dbc.open("toyota_new_mc_pt_generated");
CANParser can_parser(0, "toyota_new_mc_pt_generated", {}, {});
LogReader log;
REQUIRE(log.load(TEST_RLOG_URL, nullptr, {}, true));
REQUIRE(log.events.size() > 0);
for (auto e : log.events) {
if (e->which == cereal::Event::Which::CAN) {
std::map<std::pair<uint32_t, std::string>, std::vector<double>> values_1;
for (const auto &c : e->event.getCan()) {
const auto msg = dbc.msg(c.getAddress());
if (c.getSrc() == 0 && msg) {
for (auto &[name, sig] : msg->sigs) {
double val = get_raw_value((uint8_t *)c.getDat().begin(), c.getDat().size(), sig);
values_1[{c.getAddress(), name.toStdString()}].push_back(val);
}
}
}
auto dbc = dbc_origin.getDBC(); can_parser.UpdateCans(e->mono_time, e->event.getCan());
auto new_dbc = dbc_from_generated.getDBC(); auto values_2 = can_parser.query_latest();
REQUIRE(dbc->msgs.size() == new_dbc->msgs.size()); for (auto &[key, v1] : values_1) {
for (int i = 0; i < dbc->msgs.size(); ++i) { bool found = false;
REQUIRE(dbc->msgs[i].name == new_dbc->msgs[i].name); for (auto &v2 : values_2) {
REQUIRE(dbc->msgs[i].address == new_dbc->msgs[i].address); if (v2.address == key.first && v2.name == key.second) {
REQUIRE(dbc->msgs[i].size == new_dbc->msgs[i].size); REQUIRE(v2.all_values.size() == v1.size());
REQUIRE(dbc->msgs[i].sigs.size() == new_dbc->msgs[i].sigs.size()); REQUIRE(v2.all_values == v1);
auto &sig = dbc->msgs[i].sigs; found = true;
auto &new_sig = new_dbc->msgs[i].sigs; break;
for (int j = 0; j < sig.size(); ++j) { }
REQUIRE(sig[j].name == new_sig[j].name); }
REQUIRE(sig[j].start_bit == new_sig[j].start_bit); REQUIRE(found);
REQUIRE(sig[j].msb == new_sig[j].msb); }
REQUIRE(sig[j].lsb == new_sig[j].lsb);
REQUIRE(sig[j].size == new_sig[j].size);
REQUIRE(sig[j].is_signed == new_sig[j].is_signed);
REQUIRE(sig[j].factor == new_sig[j].factor);
REQUIRE(sig[j].offset == new_sig[j].offset);
REQUIRE(sig[j].is_little_endian == new_sig[j].is_little_endian);
} }
} }
} }

@ -37,7 +37,7 @@ VideoWidget::VideoWidget(QWidget *parent) : QWidget(parent) {
// btn controls // btn controls
QHBoxLayout *control_layout = new QHBoxLayout(); QHBoxLayout *control_layout = new QHBoxLayout();
play_btn = new QPushButton(""); play_btn = new QPushButton("");
play_btn->setStyleSheet("font-weight:bold"); play_btn->setStyleSheet("font-weight:bold; height:16px");
control_layout->addWidget(play_btn); control_layout->addWidget(play_btn);
QButtonGroup *group = new QButtonGroup(this); QButtonGroup *group = new QButtonGroup(this);

@ -91,9 +91,14 @@ def main(addr, cams, nvidia=False):
vipc_server.create_buffers(vst, 4, False, W, H) vipc_server.create_buffers(vst, 4, False, W, H)
vipc_server.start_listener() vipc_server.start_listener()
procs = []
for k, v in cams.items(): for k, v in cams.items():
multiprocessing.Process(target=decoder, args=(addr, k, vipc_server, v, nvidia)).start() p = multiprocessing.Process(target=decoder, args=(addr, k, vipc_server, v, nvidia))
p.start()
procs.append(p)
for p in procs:
p.join()
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Decode video streams and broadcast on VisionIPC") parser = argparse.ArgumentParser(description="Decode video streams and broadcast on VisionIPC")

Loading…
Cancel
Save