├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── component_proposal.md │ └── feature_request.md ├── dependabot.yml └── workflows │ └── python-publish.yml ├── .gitignore ├── .gitmodules ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── arduino ├── leads_vec_power │ └── leads_vec_power.ino └── leads_vec_wsc │ ├── Adafruit_BNO08x_RVC.cpp │ ├── Adafruit_BNO08x_RVC.h │ ├── BNO08x.cpp │ ├── BNO08x.h │ └── leads_vec_wsc.ino ├── design ├── Pin Config.pptx ├── main_controller.c4d └── main_controller.stl ├── docs ├── Beginner Tutorial.md ├── LEADS.pptx └── assets │ ├── comm-flowchart.png │ ├── demo-1.gif │ ├── demo-2.gif │ ├── demo-manual.png │ ├── ecology.png │ ├── lap-analysis.png │ ├── leads-framework.png │ ├── leads-vec-rc.jpg │ ├── leads-vec-rc.png │ └── pv.png ├── home ├── assets │ ├── background-1.png │ ├── background-10.png │ ├── background-11.png │ ├── background-2.png │ ├── background-3.png │ ├── background-4.png │ ├── background-6.png │ ├── background-9.png │ ├── script.js │ └── style.css └── index.html ├── leads ├── __init__.py ├── _ltm │ └── core ├── callback.py ├── comm │ ├── __init__.py │ ├── client │ │ ├── __init__.py │ │ └── client.py │ ├── prototype.py │ └── server │ │ ├── __init__.py │ │ └── server.py ├── config │ ├── __init__.py │ ├── registry.py │ └── template.py ├── constant.py ├── context.py ├── data.py ├── data_persistence │ ├── __init__.py │ ├── _computational │ │ └── __init__.py │ ├── analyzer │ │ ├── __init__.py │ │ ├── inference.py │ │ ├── jarvis.py │ │ ├── preprocess.py │ │ ├── processor.py │ │ └── utils.py │ └── core.py ├── dt │ ├── __init__.py │ ├── controller.py │ ├── device.py │ ├── odometer.py │ ├── predefined_tags.py │ └── registry.py ├── event.py ├── leads.py ├── logger.py ├── ltm.py ├── os.py ├── plugin │ ├── __init__.py │ ├── abs.py │ ├── atbs.py │ ├── dtcs.py │ ├── ebi.py │ └── plugin.py ├── registry.py ├── sft.py └── types.py ├── leads_arduino ├── __init__.py ├── accelerometer.py ├── arduino_micro.py ├── arduino_nano.py ├── arduino_proto.py ├── pedal.py ├── voltage_sensor.py └── wheel_speed_sensor.py ├── leads_audio ├── __init__.py ├── assets │ ├── confirm.mp3 │ ├── direction-indicator-off.mp3 │ ├── direction-indicator-on.mp3 │ └── warning.mp3 ├── prototype.py └── system.py ├── leads_can ├── __init__.py ├── obd.py └── prototype.py ├── leads_comm_serial ├── __init__.py ├── connection.py ├── identity.py └── sobd │ ├── __init__.py │ └── sobd.py ├── leads_emulation ├── __init__.py └── replay.py ├── leads_gpio ├── __init__.py ├── button.py ├── cpu_monitor.py ├── gps_receiver.py ├── led.py ├── led_group.py └── types.py ├── leads_gui ├── __init__.py ├── accelerometer.py ├── assets │ ├── icons │ │ ├── battery-black.png │ │ ├── battery-red.png │ │ ├── battery-white.png │ │ ├── brake-black.png │ │ ├── brake-red.png │ │ ├── brake-white.png │ │ ├── car-black.png │ │ ├── car-red.png │ │ ├── car-white.png │ │ ├── engine-black.png │ │ ├── engine-red.png │ │ ├── engine-white.png │ │ ├── esc-black.png │ │ ├── esc-red.png │ │ ├── esc-white.png │ │ ├── hazard-black.png │ │ ├── hazard-red.png │ │ ├── hazard-white.png │ │ ├── high-beam-black.png │ │ ├── high-beam-red.png │ │ ├── high-beam-white.png │ │ ├── left-black.png │ │ ├── left-red.png │ │ ├── left-white.png │ │ ├── light-black.png │ │ ├── light-red.png │ │ ├── light-white.png │ │ ├── motor-black.png │ │ ├── motor-red.png │ │ ├── motor-white.png │ │ ├── right-black.png │ │ ├── right-red.png │ │ ├── right-white.png │ │ ├── satellite-black.png │ │ ├── satellite-red.png │ │ ├── satellite-white.png │ │ ├── speed-black.png │ │ ├── speed-red.png │ │ ├── speed-white.png │ │ ├── stopwatch-black.png │ │ ├── stopwatch-red.png │ │ └── stopwatch-white.png │ ├── leads-theme.json │ └── logo.png ├── config.py ├── icons.py ├── performance_checker.py ├── photo.py ├── prototype.py ├── proxy.py ├── speedometer.py ├── system.py ├── types.py └── typography.py ├── leads_vec ├── __entry__.py ├── __init__.py ├── __main__.py ├── __version__.py ├── _bootloader │ ├── __init__.py │ ├── bgrt-fallback.png │ ├── frp.py │ ├── leads-vec.service.sh │ ├── splash.py │ ├── systemd.py │ └── watermark.png ├── benchmark.py ├── cli.py ├── config.py ├── devices.py ├── devices_visual.py ├── replay.py ├── run.py └── utils.py ├── leads_vec_dp ├── __entry__.py ├── __init__.py ├── __main__.py └── run.py ├── leads_vec_rc ├── __entry__.py ├── __init__.py ├── __main__.py ├── cli.py └── config.py ├── leads_video ├── __init__.py ├── camera.py └── utils.py ├── pyproject.toml └── scripts ├── frp-config.sh ├── frp-install.sh ├── python-install.sh ├── setup.sh └── uninstall.sh /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ProjectNeura 2 | patreon: ProjectNeura 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: [ bug, todo ] 6 | assignees: ATATC 7 | issue_type: Bug 8 | 9 | --- 10 | 11 | **Describe the bug** 12 | A clear and concise description of what the bug is. 13 | 14 | **To Reproduce** 15 | Steps to reproduce the behavior: 16 | 1. Go to '...' 17 | 2. Click on '....' 18 | 3. Scroll down to '....' 19 | 4. See error 20 | 21 | **Expected behavior** 22 | A clear and concise description of what you expected to happen. 23 | 24 | **Screenshots** 25 | If applicable, add screenshots to help explain your problem. 26 | 27 | **Desktop (please complete the following information):** 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Smartphone (please complete the following information):** 33 | - Device: [e.g. iPhone6] 34 | - OS: [e.g. iOS8.1] 35 | - Browser [e.g. stock browser, safari] 36 | - Version [e.g. 22] 37 | 38 | **Additional context** 39 | Add any other context about the problem here. 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/component_proposal.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Component proposal 3 | about: Request for a component purchase 4 | title: "Component Proposal: REQUESTED COMPONENT" 5 | labels: [ enhancement, question ] 6 | assignees: qmascarenhas 7 | issue_type: Feature 8 | 9 | --- 10 | 11 | **Describe the use of the component** 12 | A clear and concise description of what the component is and what it is for. 13 | 14 | **List at least one possible purchasing link** 15 | Include at least one way to purchase the component. It may or may not be adopted. 16 | 17 | **Additional context** 18 | Add any other context about the proposal here. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: [ enhancement, question ] 6 | assignees: [ ATATC, qmascarenhas ] 7 | issue_type: Feature 8 | 9 | --- 10 | 11 | **Is your feature request related to a problem? Please describe.** 12 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 13 | 14 | **Describe the solution you'd like** 15 | A clear and concise description of what you want to happen. 16 | 17 | **Describe alternatives you've considered** 18 | A clear and concise description of any alternative solutions or features you've considered. 19 | 20 | **Additional context** 21 | Add any other context or screenshots about the feature request here. 22 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | build-and-publish: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | id-token: write 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | - name: Set up Python 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: "3.12" 23 | - name: Install dependencies 24 | run: python -m pip install -U setuptools wheel build 25 | - name: Build 26 | run: python -m build . 27 | - name: Publish 28 | uses: pypa/gh-action-pypi-publish@release/v1 29 | with: 30 | skip-existing: true 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | __pycache__ 3 | main.py 4 | build 5 | *.egg-info 6 | dist 7 | data 8 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "readthedocs"] 2 | path = readthedocs 3 | url = https://github.com/ProjectNeura/leads-docs.git 4 | [submodule "arduino/leads"] 5 | path = arduino/leads 6 | url = https://github.com/ProjectNeura/LEADS-Arduino.git 7 | [submodule "jarvis"] 8 | path = jarvis 9 | url = https://github.com/ProjectNeura/LEADS-Jarvis.git 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Please fork the project into your repository. Before your pull request, ensure you have tested all possible impacts on 4 | other parts of the project. If no certainty is assured, please contact our core team members for official support. 5 | 6 | ## Releases 7 | 8 | Any new release of formal versions must come after at least 2 alpha versions and 2 beta versions. Each beta version must 9 | be deployed and tested on a new testing environment following the 10 | [User Manual](https://leads-docs.projectneura.org/en/latest/vec/user-manual.html). 11 | 12 | ## Change to APIs 13 | 14 | Any effect on the signatures of APIs must go through a deprecation process where the original interfaces must be 15 | marked deprecated and remain for 3 formal versions. 16 | -------------------------------------------------------------------------------- /arduino/leads_vec_power/leads_vec_power.ino: -------------------------------------------------------------------------------- 1 | #include "LEADS.h" 2 | #include "Pedal.h" 3 | 4 | const int PIN_VOT[] = {A0}; 5 | 6 | Peer P{POWER_CONTROLLER}; 7 | VoltageSensor VOT{ArrayList(PIN_VOT, 1), 30000.0, 7500.0}; 8 | 9 | void setup() { 10 | P.initializeAsRoot(); 11 | VOT.initializeAsRoot(); 12 | } 13 | 14 | void loop() { 15 | P.refresh(); 16 | returnFloat(P, VOLTAGE_SENSOR, VOT.read()); 17 | delay(100); 18 | } 19 | -------------------------------------------------------------------------------- /arduino/leads_vec_wsc/Adafruit_BNO08x_RVC.cpp: -------------------------------------------------------------------------------- 1 | /*! 2 | * @file Adafruit_BNO08x_RVC.cpp 3 | * 4 | * @mainpage Adafruit BNO08x RVC A simple library to use the UART-RVC mode of 5 | * the BNO08x sensors from Hillcrest Laboratories 6 | * 7 | * @section intro_sec Introduction 8 | * 9 | * I2C Driver for the Library for the BNO08x_RVC A simple library to use 10 | * the UART-RVC mode of the BNO08x sensors from Hillcrest Laboratories 11 | * 12 | * This is a library for the Adafruit BNO08x_RVC breakout: 13 | * https://www.adafruit.com/product/4754 14 | * 15 | * Adafruit invests time and resources providing this open source code, 16 | * please support Adafruit and open-source hardware by purchasing products from 17 | * Adafruit! 18 | * 19 | * @section dependencies Dependencies 20 | * This library depends on the Adafruit BusIO library 21 | * 22 | * This library depends on the Adafruit Unified Sensor library 23 | * 24 | * @section author Author 25 | * 26 | * Bryan Siepert for Adafruit Industries 27 | * 28 | * @section license License 29 | * 30 | * BSD (see license.txt) 31 | * 32 | * @section HISTORY 33 | * 34 | * v1.0 - First release 35 | */ 36 | 37 | #include "Arduino.h" 38 | #include 39 | 40 | #include "Adafruit_BNO08x_RVC.h" 41 | 42 | /** 43 | * @brief Construct a new Adafruit_BNO08x_RVC::Adafruit_BNO08x_RVC object 44 | * 45 | */ 46 | Adafruit_BNO08x_RVC::Adafruit_BNO08x_RVC(void) {} 47 | 48 | /** 49 | * @brief Destroy the Adafruit_BNO08x_RVC::Adafruit_BNO08x_RVC object 50 | * 51 | */ 52 | Adafruit_BNO08x_RVC::~Adafruit_BNO08x_RVC(void) {} 53 | 54 | /*! 55 | * @brief Setups the hardware 56 | * @param theSerial 57 | * Pointer to Stream (HardwareSerial/SoftwareSerial) interface 58 | * @return True 59 | */ 60 | bool Adafruit_BNO08x_RVC::begin(Stream *theSerial) { 61 | serial_dev = theSerial; 62 | return true; 63 | } 64 | 65 | /** 66 | * @brief Get the next available heading and acceleration data from the sensor 67 | * 68 | * @param heading pointer to a BNO08x_RVC_Data struct to hold the measurements 69 | * @return true: success false: failure 70 | */ 71 | bool Adafruit_BNO08x_RVC::read(BNO08x_RVC_Data *heading) { 72 | if (!heading) { 73 | return false; 74 | } 75 | 76 | if (!serial_dev->available()) { 77 | return false; 78 | } 79 | if (serial_dev->peek() != 0xAA) { 80 | serial_dev->read(); 81 | return false; 82 | } 83 | // Now read all 19 bytes 84 | 85 | if (serial_dev->available() < 19) { 86 | return false; 87 | } 88 | // at this point we know there's at least 19 bytes available and the first 89 | if (serial_dev->read() != 0xAA) { 90 | // shouldn't happen baecause peek said it was 0xAA 91 | return false; 92 | } 93 | // make sure the next byte is the second 0xAA 94 | if (serial_dev->read() != 0xAA) { 95 | return false; 96 | } 97 | uint8_t buffer[19]; 98 | // ok, we've got our header, read the actual data+crc 99 | if (!serial_dev->readBytes(buffer, 17)) { 100 | return false; 101 | }; 102 | 103 | uint8_t sum = 0; 104 | // get checksum ready 105 | for (uint8_t i = 0; i < 16; i++) { 106 | sum += buffer[i]; 107 | } 108 | if (sum != buffer[16]) { 109 | return false; 110 | } 111 | 112 | // The data comes in endian'd, this solves it so it works on all platforms 113 | int16_t buffer_16[6]; 114 | 115 | for (uint8_t i = 0; i < 6; i++) { 116 | 117 | buffer_16[i] = (buffer[1 + (i * 2)]); 118 | buffer_16[i] += (buffer[1 + (i * 2) + 1] << 8); 119 | } 120 | heading->yaw = (float)buffer_16[0] * DEGREE_SCALE; 121 | heading->pitch = (float)buffer_16[1] * DEGREE_SCALE; 122 | heading->roll = (float)buffer_16[2] * DEGREE_SCALE; 123 | 124 | heading->x_accel = (float)buffer_16[3] * MILLI_G_TO_MS2; 125 | heading->y_accel = (float)buffer_16[4] * MILLI_G_TO_MS2; 126 | heading->z_accel = (float)buffer_16[5] * MILLI_G_TO_MS2; 127 | 128 | return true; 129 | } -------------------------------------------------------------------------------- /arduino/leads_vec_wsc/Adafruit_BNO08x_RVC.h: -------------------------------------------------------------------------------- 1 | /*! 2 | * @file Adafruit_BNO08x_RVC.h 3 | * 4 | * I2C Driver for the Adafruit BNO08x RVC A simple library to use the 5 | *UART-RVC mode of the BNO08x sensors from Hillcrest Laboratories 6 | * 7 | * This is a library for use with thethe Adafruit BNO08x breakout: 8 | * https://www.adafruit.com/products/4754 9 | * 10 | * Adafruit invests time and resources providing this open source code, 11 | * please support Adafruit and open-source hardware by purchasing products from 12 | * Adafruit! 13 | * 14 | * 15 | * BSD license (see license.txt) 16 | */ 17 | 18 | #ifndef _ADAFRUIT_BNO08x_RVC_H 19 | #define _ADAFRUIT_BNO08x_RVC_H 20 | 21 | #define MILLI_G_TO_MS2 0.0098067 ///< Scalar to convert milli-gs to m/s^2 22 | #define DEGREE_SCALE 0.01 ///< To convert the degree values 23 | 24 | #include "Arduino.h" 25 | 26 | /*! 27 | * @brief Class that stores state and functions for interacting with 28 | * the BNO08x_RVC A simple library to use the UART-RVC mode of the 29 | * BNO08x sensors from Hillcrest Laboratories 30 | */ 31 | /**! Struct to hold a UART-RVC packet **/ 32 | typedef struct BNO08xRVCData { 33 | float yaw, ///< Yaw in Degrees 34 | pitch, ///< Pitch in Degrees 35 | roll; ///< Roll in Degrees 36 | float x_accel, ///< The X acceleration value in m/s^2 37 | y_accel, ///< The Y acceleration value in m/s^2 38 | z_accel; ///< The Z acceleration value in m/s^2 39 | 40 | } BNO08x_RVC_Data; 41 | 42 | /** 43 | * @brief A class to interact with the BNO08x sensors from Hillcrest 44 | * Laboritories using the UART-RVC mode 45 | * 46 | */ 47 | class Adafruit_BNO08x_RVC { 48 | public: 49 | Adafruit_BNO08x_RVC(); 50 | ~Adafruit_BNO08x_RVC(); 51 | 52 | bool begin(Stream *theStream); 53 | bool read(BNO08x_RVC_Data *heading); 54 | 55 | private: 56 | Stream *serial_dev; 57 | }; 58 | 59 | #endif -------------------------------------------------------------------------------- /arduino/leads_vec_wsc/BNO08x.cpp: -------------------------------------------------------------------------------- 1 | #include "BNO08x.h" 2 | 3 | BNO08x::BNO08x(OnAccelerometerUpdate onUpdate) : Accelerometer(onUpdate) {} 4 | void BNO08x::initialize(const ArrayList &parentTags) { 5 | Accelerometer::initialize(parentTags); 6 | Serial1.begin(115200); 7 | while (!Serial1) delay(10); 8 | if (!_rvc.begin(&Serial1)) delay(10); 9 | } 10 | Acceleration BNO08x::read() { 11 | BNO08x_RVC_Data heading; 12 | Acceleration r = Acceleration(); 13 | if (!_rvc.read(&heading)) return r; 14 | r.yaw = heading.yaw; 15 | r.pitch = heading.pitch; 16 | r.roll = heading.roll; 17 | r.forwardAcceleration = heading.y_accel; 18 | r.lateralAcceleration = heading.x_accel; 19 | r.verticalAcceleration = heading.z_accel; 20 | _onUpdate(r); 21 | return r; 22 | } -------------------------------------------------------------------------------- /arduino/leads_vec_wsc/BNO08x.h: -------------------------------------------------------------------------------- 1 | #ifndef BNO08X_H 2 | #define BNO08X_H 3 | 4 | 5 | #include "Adafruit_BNO08x_RVC.h" 6 | #include "LEADS.h" 7 | 8 | class BNO08x : public Accelerometer { 9 | protected: 10 | Adafruit_BNO08x_RVC _rvc = Adafruit_BNO08x_RVC(); 11 | public: 12 | explicit BNO08x(OnAccelerometerUpdate onUpdate); 13 | void initialize(const ArrayList &parentTags) override; 14 | Acceleration read() override; 15 | }; 16 | 17 | 18 | #endif // BNO08X_H 19 | -------------------------------------------------------------------------------- /arduino/leads_vec_wsc/leads_vec_wsc.ino: -------------------------------------------------------------------------------- 1 | #include "BNO08x.h" 2 | 3 | const int PIN_LFWSS[] = {2}; 4 | const int PIN_RFWSS[] = {3}; 5 | const int PIN_LRWSS[] = {4}; 6 | const int PIN_RRWSS[] = {5}; 7 | const int PIN_CRWSS[] = {6}; 8 | 9 | Peer P{WHEEL_SPEED_CONTROLLER}; 10 | WheelSpeedSensor LFWSS{ArrayList(PIN_LFWSS, 1), [](float ws) {returnFloat(P, LEFT_FRONT_WHEEL_SPEED_SENSOR, ws);}}; 11 | WheelSpeedSensor RFWSS{ArrayList(PIN_RFWSS, 1), [](float ws) {returnFloat(P, RIGHT_FRONT_WHEEL_SPEED_SENSOR, ws);}}; 12 | WheelSpeedSensor LRWSS{ArrayList(PIN_LRWSS, 1), [](float ws) {returnFloat(P, LEFT_REAR_WHEEL_SPEED_SENSOR, ws);}}; 13 | WheelSpeedSensor RRWSS{ArrayList(PIN_RRWSS, 1), [](float ws) {returnFloat(P, RIGHT_REAR_WHEEL_SPEED_SENSOR, ws);}}; 14 | WheelSpeedSensor CRWSS{ArrayList(PIN_CRWSS, 1), [](float ws) {returnFloat(P, CENTER_REAR_WHEEL_SPEED_SENSOR, ws);}}; 15 | BNO08x ACCL{[](Acceleration acceleration) {returnString(P, ACCELEROMETER, acceleration.toString());}}; 16 | 17 | void setup() { 18 | P.initializeAsRoot(); 19 | LFWSS.initializeAsRoot(); 20 | RFWSS.initializeAsRoot(); 21 | LRWSS.initializeAsRoot(); 22 | RRWSS.initializeAsRoot(); 23 | CRWSS.initializeAsRoot(); 24 | ACCL.initializeAsRoot(); 25 | } 26 | 27 | void loop() { 28 | P.refresh(); 29 | LFWSS.read(); 30 | RFWSS.read(); 31 | LRWSS.read(); 32 | RRWSS.read(); 33 | CRWSS.read(); 34 | ACCL.read(); 35 | } 36 | -------------------------------------------------------------------------------- /design/Pin Config.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/design/Pin Config.pptx -------------------------------------------------------------------------------- /design/main_controller.c4d: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/design/main_controller.c4d -------------------------------------------------------------------------------- /design/main_controller.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/design/main_controller.stl -------------------------------------------------------------------------------- /docs/Beginner Tutorial.md: -------------------------------------------------------------------------------- 1 | # Beginner Tutorial 2 | 3 | ## Everyone 4 | 5 | [GitHub Quickstart](https://docs.github.com/en/get-started/quickstart) is quite helpful for the kickoff. 6 | 7 | ## Programmers 8 | 9 | Required Environment: [Python](https://python.org) >= 3.12 10 | 11 | Recommended Package Management: [Anaconda](https://www.anaconda.com) 12 | 13 | Recommended IDE: [PyCharm](https://www.jetbrains.com/pycharm) -------------------------------------------------------------------------------- /docs/LEADS.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/docs/LEADS.pptx -------------------------------------------------------------------------------- /docs/assets/comm-flowchart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/docs/assets/comm-flowchart.png -------------------------------------------------------------------------------- /docs/assets/demo-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/docs/assets/demo-1.gif -------------------------------------------------------------------------------- /docs/assets/demo-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/docs/assets/demo-2.gif -------------------------------------------------------------------------------- /docs/assets/demo-manual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/docs/assets/demo-manual.png -------------------------------------------------------------------------------- /docs/assets/ecology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/docs/assets/ecology.png -------------------------------------------------------------------------------- /docs/assets/lap-analysis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/docs/assets/lap-analysis.png -------------------------------------------------------------------------------- /docs/assets/leads-framework.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/docs/assets/leads-framework.png -------------------------------------------------------------------------------- /docs/assets/leads-vec-rc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/docs/assets/leads-vec-rc.jpg -------------------------------------------------------------------------------- /docs/assets/leads-vec-rc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/docs/assets/leads-vec-rc.png -------------------------------------------------------------------------------- /docs/assets/pv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/docs/assets/pv.png -------------------------------------------------------------------------------- /home/assets/background-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/home/assets/background-1.png -------------------------------------------------------------------------------- /home/assets/background-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/home/assets/background-10.png -------------------------------------------------------------------------------- /home/assets/background-11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/home/assets/background-11.png -------------------------------------------------------------------------------- /home/assets/background-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/home/assets/background-2.png -------------------------------------------------------------------------------- /home/assets/background-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/home/assets/background-3.png -------------------------------------------------------------------------------- /home/assets/background-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/home/assets/background-4.png -------------------------------------------------------------------------------- /home/assets/background-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/home/assets/background-6.png -------------------------------------------------------------------------------- /home/assets/background-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/home/assets/background-9.png -------------------------------------------------------------------------------- /home/assets/script.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", () => { 2 | const lines = document.querySelectorAll(".line"); 3 | const background = document.querySelector(".background"); 4 | let currentIndex = 0; 5 | 6 | function showLine(index) { 7 | if (index === 0 || index === 5 || index === 8) background.style.background = "#343751"; 8 | else if (index === 7) background.style.background = "#000000"; 9 | else background.style.background = "url(\"assets/background-" + index + ".png\") no-repeat center center/cover"; 10 | lines.forEach((line, i) => { 11 | line.classList.remove("visible", "exit"); 12 | if (i === index) line.classList.add("visible"); 13 | else if (i === index - 1 || (index === 0 && i === lines.length - 1)) line.classList.add("exit"); 14 | }); 15 | } 16 | 17 | window.addEventListener("scroll", () => { 18 | const scrollPosition = window.scrollY; 19 | const blurValue = Math.min(scrollPosition / 100, 10); 20 | background.style.filter = "blur(" + blurValue + "px)"; 21 | 22 | const newIndex = Math.floor(scrollPosition / window.innerHeight); 23 | if (newIndex !== currentIndex) { 24 | currentIndex = newIndex; 25 | showLine(currentIndex); 26 | } 27 | }); 28 | 29 | showLine(currentIndex); 30 | document.body.style.height = (lines.length * window.innerHeight) + "px"; 31 | }); 32 | -------------------------------------------------------------------------------- /home/assets/style.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | z-index: -1; 3 | height: 100%; 4 | margin: 0; 5 | padding: 0; 6 | font-family: Arial, sans-serif; 7 | overflow-x: hidden; 8 | } 9 | 10 | .background { 11 | position: fixed; 12 | width: 100%; 13 | height: 100%; 14 | top: 0; 15 | left: 0; 16 | transition: background 1s ease, filter 1s ease; 17 | } 18 | 19 | .content { 20 | position: fixed; 21 | top: 45%; 22 | left: 50%; 23 | transform: translate(-50%, -50%); 24 | text-align: center; 25 | width: 80%; 26 | } 27 | 28 | .line { 29 | position: absolute; 30 | top: 50%; 31 | left: 50%; 32 | transform: translate(-50%, -50%) translateY(100%); 33 | width: 100%; 34 | opacity: 0; 35 | transition: opacity 0.5s, transform 0.5s; 36 | color: white; 37 | box-sizing: border-box; 38 | } 39 | 40 | .line p { 41 | padding-left: 10%; 42 | padding-right: 10%; 43 | } 44 | 45 | .line.visible { 46 | opacity: 1; 47 | transform: translate(-50%, -50%) translateY(0); 48 | } 49 | 50 | .line.exit { 51 | opacity: 0; 52 | transform: translate(-50%, -50%) translateY(-100%); 53 | } 54 | 55 | h1 { 56 | font-size: 4rem; 57 | } 58 | 59 | h2 { 60 | font-size: 2rem; 61 | } 62 | 63 | p { 64 | font-size: 1.5rem; 65 | } 66 | 67 | a { 68 | color: #FAD12B 69 | } 70 | 71 | .sticky-footer { 72 | position: fixed; 73 | right: 48px; 74 | bottom: 0; 75 | width: 100%; 76 | height: 48px; 77 | display: flex; 78 | justify-content: center; 79 | background-color: #FAD12B; 80 | } 81 | 82 | .sticky-footer b { 83 | height: 48px; 84 | line-height: 48px; 85 | color: white; 86 | } 87 | 88 | .sticky-footer img { 89 | position: fixed; 90 | right: 0; 91 | width: 48px; 92 | height: 48px; 93 | background: #90ACC7; 94 | } 95 | -------------------------------------------------------------------------------- /home/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | LEADS: Lightweight Embedded Assisted Driving System 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 |

LEADS: 15 | Lightweight 16 | Embedded 17 | Assisted 18 | Driving 19 | System 20 |

21 |

Enable your racing car with powerful, data-driven instrumentation, control, and analysis systems, all wrapped 22 | up in a gorgeous look.

23 |
24 |

Why LEADS?

25 |

Aesthetics

26 |

Full Control through a Touch Screen

27 |

Immersive Cockpit

28 |

Data Analysis

29 |

LEADS VeC Remote Analyst

30 |

LEADS VeC Data Processor

31 |

Rich Ecology

32 |
33 |

LEADS VeC

34 |

The VeC Project is the main intended 35 | application of LEADS. LEADS VeC initially served for this target. Yet throughout the development, it has 36 | become an indivisible part of LEADS. It works as the bootloader of LEADS, providing a template of how to 37 | make use of the framework.

38 |
39 |
40 |

LEADS Framework

41 |

The LEADS framework is an excellent foundation that enables developers even with zero experience to quickly 42 | use Python to interact with popular hardware platforms and devices. Although it is not directly runnable, it 43 | sets the path for everything from embedded systems to GUI, networking, and even AI.

44 |
45 |
46 |

Yours to Discover

47 |

Write your journey with LEADS.

48 |
49 |
50 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /leads/__init__.py: -------------------------------------------------------------------------------- 1 | from leads.config import * 2 | from leads.context import * 3 | from leads.data import * 4 | from leads.dt import * 5 | from leads.event import * 6 | from leads.leads import * 7 | from leads.logger import Level, L 8 | from leads.ltm import * 9 | from leads.plugin import * 10 | from leads.registry import * 11 | from leads.sft import SFT, mark_device, read_device_marker 12 | -------------------------------------------------------------------------------- /leads/_ltm/core: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /leads/callback.py: -------------------------------------------------------------------------------- 1 | from typing import Self as _Self 2 | 3 | from leads.os import _currentframe 4 | 5 | 6 | class CallbackChain(object): 7 | def __init__(self, chain: _Self | None = None) -> None: 8 | self._chain: CallbackChain | None = chain 9 | 10 | def bind_chain(self, chain: _Self | None) -> None: 11 | self._chain = chain 12 | 13 | def super(self, *args, **kwargs) -> None: 14 | """ 15 | Call the superior method if there is one. 16 | This must be called directly in the corresponding successor method. 17 | """ 18 | if not self._chain: 19 | return 20 | cf = _currentframe().f_back 21 | while (cn := cf.f_code.co_name) == "super": 22 | cf = cf.f_back 23 | getattr(self._chain, cn)(*args, **kwargs) 24 | -------------------------------------------------------------------------------- /leads/comm/__init__.py: -------------------------------------------------------------------------------- 1 | from leads.comm.client import * 2 | from leads.comm.prototype import * 3 | from leads.comm.server import * 4 | -------------------------------------------------------------------------------- /leads/comm/client/__init__.py: -------------------------------------------------------------------------------- 1 | from leads.comm.client.client import Client 2 | from leads.comm.prototype import Callback 3 | 4 | 5 | def create_client(port: int = 16900, callback: Callback = Callback(), separator: bytes = b";") -> Client: 6 | """ 7 | Create a client service. 8 | :param port: the port to which the client connects 9 | :param callback: the callback methods 10 | :param separator: the separator that splits messages into sentences 11 | :return: the client service 12 | """ 13 | return Client(port, callback, separator) 14 | 15 | 16 | def start_client(server_address: str, target: Client = create_client(), parallel: bool = False) -> Client: 17 | """ 18 | Starts the client service. 19 | :param server_address: the server address to which the client connects 20 | :param target: the client service to start 21 | :param parallel: True: run in a separate thread; False: run in the caller thread 22 | :return: the client service 23 | """ 24 | return target.start(parallel, server_address=server_address) 25 | -------------------------------------------------------------------------------- /leads/comm/client/client.py: -------------------------------------------------------------------------------- 1 | from typing import override as _override 2 | 3 | from leads.comm.prototype import Entity, Connection, Callback 4 | 5 | 6 | class Client(Entity): 7 | """ 8 | You should use `create_client()` and `start_client()` instead of directly calling any method. 9 | """ 10 | 11 | def __init__(self, port: int, callback: Callback, separator: bytes) -> None: 12 | """ 13 | :param port: the port to which the client connects 14 | :param callback: the callback interface 15 | :param separator: the symbol that splits the stream into messages 16 | """ 17 | super().__init__(port, callback) 18 | self._connection: Connection | None = None 19 | self._separator: bytes = separator 20 | 21 | @_override 22 | def run(self, server_address: str) -> None: 23 | """ 24 | Establish a connection and stage it. 25 | :param server_address: the server address to which the client connects 26 | """ 27 | self._callback.on_initialize(self) 28 | self._socket.connect((server_address, self._port)) 29 | self._callback.on_connect(self, connection := Connection(self._socket, (server_address, self._port), 30 | separator=self._separator)) 31 | self._connection = connection 32 | self._stage(connection) 33 | 34 | def send(self, msg: bytes) -> None: 35 | """ 36 | Send the message to the server. 37 | :param msg: the message to send 38 | :exception IOError: no connection 39 | """ 40 | if not self._connection: 41 | raise IOError("Client must be running to perform this operation") 42 | self._connection.send(msg) 43 | 44 | @_override 45 | def close(self) -> None: 46 | """ 47 | Close the connection. 48 | """ 49 | if self._connection: 50 | self._connection.close() 51 | -------------------------------------------------------------------------------- /leads/comm/server/__init__.py: -------------------------------------------------------------------------------- 1 | from leads.comm.prototype import Callback 2 | from leads.comm.server.server import Server 3 | 4 | 5 | def create_server(port: int = 16900, callback: Callback = Callback(), separator: bytes = b";") -> Server: 6 | """ 7 | Create a server service. 8 | :param port: the port on which the server listens 9 | :param callback: the callback methods 10 | :param separator: the separator that splits messages into sentences 11 | :return: the server service 12 | """ 13 | return Server(port, callback, separator) 14 | 15 | 16 | def start_server(target: Server = create_server(), parallel: bool = False) -> Server: 17 | """ 18 | Starts the server service. 19 | :param target: the server service to start 20 | :param parallel: True: run in a separate thread; False: run in the caller thread 21 | :return: the server service 22 | """ 23 | return target.start(parallel) 24 | -------------------------------------------------------------------------------- /leads/comm/server/server.py: -------------------------------------------------------------------------------- 1 | from threading import Thread as _Thread 2 | from typing import override as _override 3 | 4 | from leads.comm.prototype import Entity, Connection, Callback 5 | from leads.os import _thread_flags 6 | 7 | 8 | class Server(Entity): 9 | """ 10 | You should use `create_server()` and `start_server()` instead of directly calling any method. 11 | """ 12 | 13 | def __init__(self, port: int, callback: Callback, separator: bytes) -> None: 14 | """ 15 | :param port: the port on which the server listens 16 | :param callback: the callback interface 17 | :param separator: the symbol that splits the stream into messages 18 | """ 19 | super().__init__(port, callback) 20 | self._connections: list[Connection] = [] 21 | self._separator: bytes = separator 22 | 23 | def num_connections(self) -> int: 24 | """ 25 | Get the number of active connections. 26 | :return: the number of connections 27 | """ 28 | return len(self._connections) 29 | 30 | def remove_connection(self, connection: Connection) -> None: 31 | """ 32 | Remove the connection from the list. 33 | :param connection: the connection to remove 34 | """ 35 | try: 36 | self._connections.remove(connection) 37 | except ValueError: 38 | pass 39 | 40 | @_override 41 | def run(self, max_connection: int = 1) -> None: 42 | """ 43 | Start listening for the connections and stage each connection in a new thread. 44 | :param max_connection: the maximum number of connections allowed at the same time 45 | """ 46 | self._socket.bind(("0.0.0.0", self._port)) 47 | self._socket.listen(max_connection) 48 | self._callback.on_initialize(self) 49 | while _thread_flags.active: 50 | socket, address = self._socket.accept() 51 | self._callback.on_connect(self, connection := Connection(socket, address, separator=self._separator, 52 | on_close=lambda c: self.remove_connection(c))) 53 | self._connections.append(connection) 54 | _Thread(target=self._stage, args=(connection,), daemon=True).start() 55 | 56 | def broadcast(self, msg: bytes) -> None: 57 | """ 58 | Send the message to all connected clients. 59 | :param msg: the message to send 60 | """ 61 | for c in self._connections: 62 | try: 63 | c.send(msg) 64 | except IOError: 65 | self.remove_connection(c) 66 | 67 | @_override 68 | def close(self) -> None: 69 | """ 70 | Close all active connections. 71 | """ 72 | self._socket.close() 73 | for connection in self._connections: 74 | connection.close() 75 | -------------------------------------------------------------------------------- /leads/config/__init__.py: -------------------------------------------------------------------------------- 1 | from leads.config.template import * 2 | from leads.config.registry import * 3 | -------------------------------------------------------------------------------- /leads/config/registry.py: -------------------------------------------------------------------------------- 1 | from json import load as _load 2 | from typing import TypeVar as _TypeVar, TextIO as _TextIO, Callable as _Callable 3 | 4 | from leads.config.template import ConfigTemplate 5 | from leads.types import OnRegister as _OnRegister, OnRegisterChain as _OnRegisterChain, \ 6 | SupportedConfig as _SupportedConfig 7 | 8 | T = _TypeVar("T", bound=ConfigTemplate) 9 | 10 | _config_instance: T | None = None 11 | 12 | _on_register_config: _OnRegister[T] = lambda _: None 13 | 14 | 15 | def set_on_register_config(callback: _OnRegisterChain[T]) -> None: 16 | """ 17 | Set the root node of the callback chain that is triggered when a configuration is registered. 18 | :param callback: the callback interface 19 | """ 20 | global _on_register_config 21 | _on_register_config = callback(_on_register_config) 22 | 23 | 24 | def load_config(file: str | _TextIO, constructor: _Callable[[dict[str, _SupportedConfig]], T]) -> T: 25 | """ 26 | Load a configuration from a file. 27 | :param file: the file to load from 28 | :param constructor: the constructor or an equivalent function 29 | :return: the configuration 30 | """ 31 | if isinstance(file, str): 32 | with open(file) as f: 33 | return constructor(_load(f)) 34 | return constructor(_load(file)) 35 | 36 | 37 | def register_config(config: T) -> None: 38 | """ 39 | Register a configuration. 40 | :param config: the configuration 41 | :exception RuntimeError: duplicated registration 42 | """ 43 | global _config_instance 44 | if _config_instance: 45 | raise RuntimeError("Another config is already registered") 46 | _on_register_config(config) 47 | _config_instance = config 48 | 49 | 50 | def get_config() -> T | None: 51 | """ 52 | Get the registered configuration. 53 | :return: the configuration if registered or else None 54 | """ 55 | return _config_instance 56 | 57 | 58 | def require_config() -> T: 59 | """ 60 | Require the registered configuration. 61 | :return: the configuration 62 | :exception RuntimeError: no configuration is registered 63 | """ 64 | if _config_instance: 65 | return _config_instance 66 | raise RuntimeError("No config registered") 67 | -------------------------------------------------------------------------------- /leads/config/template.py: -------------------------------------------------------------------------------- 1 | from json import dumps as _dumps 2 | from typing import override as _override, Literal as _Literal, Any as _Any 3 | 4 | from leads.data import Serializable 5 | from leads.types import SupportedConfig as _SupportedConfig 6 | 7 | 8 | class ConfigTemplate(Serializable): 9 | def __init__(self, base: dict[str, _SupportedConfig]) -> None: 10 | """ 11 | All custom attributes should be public (not named after "_"). 12 | Writable attributes should start with "w_" such as "w_debug_level". 13 | :param base: the base dictionary 14 | """ 15 | self._d: dict[str, _SupportedConfig] = self.fix_dict(base) 16 | self._frozen: bool = False 17 | self.w_debug_level: _Literal["DEBUG", "INFO", "WARN", "ERROR"] = "DEBUG" 18 | self.data_seq_size: int = 100 19 | self.num_laps_timed: int = 3 20 | self.refresh() 21 | 22 | def fix_dict(self, d: dict[str, _Any]) -> dict[str, _SupportedConfig]: 23 | """ 24 | Fix the types of every element in the dictionary. 25 | :param d: the input dictionary 26 | :return: the fixed dictionary 27 | """ 28 | return {k: self.fix_type(v) for k, v in d.items()} 29 | 30 | def fix_type(self, value: _Any) -> _SupportedConfig: 31 | """ 32 | Replace lists with tuples and check for illegal types. 33 | :param value: the input value 34 | :return: the fixed value where 35 | :exception TypeError: the type of the value is illegal 36 | """ 37 | if isinstance(value, (tuple, list)): 38 | return tuple(self.fix_type(i) for i in value) 39 | if not isinstance(value, (bool, int, float, str, type(None))): 40 | raise TypeError(f"Unsupported value type: {value}") 41 | return value 42 | 43 | def __getitem__(self, name: str) -> _SupportedConfig | None: 44 | return self.get(name) 45 | 46 | def __setitem__(self, name: str, value: _SupportedConfig) -> None: 47 | self.set(name, value) 48 | 49 | @_override 50 | def __setattr__(self, name: str, value: _SupportedConfig) -> None: 51 | if self._writable(name): 52 | super().__setattr__(name, value if name.startswith("_") else self.fix_type(value)) 53 | 54 | @_override 55 | def __str__(self) -> str: 56 | """ 57 | Convert to a JSON string. 58 | :return: the JSON string 59 | """ 60 | return _dumps(self.to_dict()) 61 | 62 | @_override 63 | def to_dict(self) -> dict[str, _SupportedConfig]: 64 | return self._d.copy() 65 | 66 | def _writable(self, name: str) -> bool: 67 | """ 68 | :param name: the configuration name 69 | :return: True: writable; False: readonly 70 | """ 71 | return not hasattr(self, "_frozen") or not self._frozen or name.startswith("w_") 72 | 73 | def set(self, name: str, value: _SupportedConfig) -> None: 74 | """ 75 | Set the value with the given name in the dictionary. 76 | :param name: the dictionary key 77 | :param value: the value to set 78 | """ 79 | if self._writable(name): 80 | self._d[name] = self.fix_type(value) 81 | self.refresh() 82 | 83 | def get(self, name: str, default: _SupportedConfig | None = None) -> _SupportedConfig | None: 84 | """ 85 | Get the value of a given name from the dictionary. 86 | :param name: the dictionary key 87 | :param default: the default value if the value does not exist 88 | :return: the value if it exists or else the default value 89 | """ 90 | return self._d.get(name, default) 91 | 92 | def refresh(self) -> None: 93 | """ 94 | Write the dictionary into the instance attributes. 95 | """ 96 | for name in dir(self): 97 | if not name.startswith("_") and (v := self._d.get(name)) is not None: 98 | if self._writable(name): 99 | setattr(self, name, v) 100 | self._frozen = True 101 | -------------------------------------------------------------------------------- /leads/constant.py: -------------------------------------------------------------------------------- 1 | from enum import StrEnum as _StrEnum, IntEnum as _IntEnum 2 | 3 | 4 | class SystemLiteral(_StrEnum): 5 | DTCS: str = "DTCS" 6 | ABS: str = "ABS" 7 | EBI: str = "EBI" 8 | ATBS: str = "ATBS" 9 | 10 | 11 | class ESCMode(_IntEnum): 12 | STANDARD: int = 0 13 | AGGRESSIVE: int = 1 14 | SPORT: int = 2 15 | OFF: int = 3 16 | -------------------------------------------------------------------------------- /leads/context.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta as _ABCMeta, abstractmethod as _abstractmethod 2 | from collections import deque as _deque 3 | from time import time as _time 4 | from typing import TypeVar as _TypeVar, Generic as _Generic 5 | 6 | from leads.constant import ESCMode 7 | from leads.data import DataContainer 8 | 9 | T = _TypeVar("T", bound=DataContainer) 10 | 11 | 12 | def _check_data_type(data: T, superclass: type[DataContainer] = DataContainer) -> None: 13 | if not isinstance(data, superclass): 14 | raise TypeError(f"New data must inherit from `{superclass}`") 15 | 16 | 17 | class Context(_Generic[T], metaclass=_ABCMeta): 18 | def __init__(self, initial_data: T | None, data_seq_size: int, num_laps_timed: int) -> None: 19 | """ 20 | :param initial_data: initial data 21 | :param data_seq_size: buffer size of history data 22 | :param num_laps_timed: number of timed laps retained 23 | """ 24 | if initial_data: 25 | _check_data_type(initial_data) 26 | else: 27 | initial_data = DataContainer() 28 | self._initial_data_type: type[DataContainer] = type(initial_data) 29 | if data_seq_size < 1: 30 | raise ValueError("`data_seq_size` must be greater or equal to 1") 31 | self._data_seq: _deque[DataContainer] = _deque((initial_data,), maxlen=data_seq_size) 32 | self._speed_seq: _deque[float] = _deque(maxlen=data_seq_size) 33 | self._lap_time_seq: _deque[int] = _deque((int(_time() * 1000),), maxlen=num_laps_timed + 1) 34 | self._esc_mode: ESCMode = ESCMode.STANDARD 35 | self._brake_indicator: bool = False 36 | self._left_indicator: bool = False 37 | self._right_indicator: bool = False 38 | self._hazard: bool = False 39 | 40 | def data(self) -> T: 41 | """ 42 | :return: a copy of the current data container 43 | """ 44 | return self._data_seq[-1] 45 | 46 | def push(self, data: T) -> None: 47 | """ 48 | Push new data into the sequence. 49 | :param data: the new data 50 | """ 51 | _check_data_type(data, self._initial_data_type) 52 | self._data_seq.append(data) 53 | self._speed_seq.append(data.speed) 54 | 55 | def esc_mode(self, esc_mode: ESCMode | None = None) -> ESCMode | None: 56 | """ 57 | Set or get the ESC mode. 58 | :param esc_mode: the ESC mode or None if getter mode 59 | :return: the ESC mode or None if setter mode 60 | """ 61 | if esc_mode is None: 62 | return self._esc_mode 63 | self._esc_mode = esc_mode 64 | 65 | @_abstractmethod 66 | def update(self) -> None: 67 | raise NotImplementedError 68 | 69 | @_abstractmethod 70 | def intervene(self, *args, **kwargs) -> None: # real signature unknown 71 | raise NotImplementedError 72 | 73 | @_abstractmethod 74 | def suspend(self, *args, **kwargs) -> None: # real signature unknown 75 | raise NotImplementedError 76 | 77 | def time_lap(self) -> None: 78 | self._lap_time_seq.append(int(_time() * 1000)) 79 | 80 | def lap_times(self) -> list[int]: 81 | return [self._lap_time_seq[i] - self._lap_time_seq[i - 1] for i in range(1, len(self._lap_time_seq))] 82 | 83 | def speed_trend(self) -> float: 84 | return (self._speed_seq[-1] - self._speed_seq[0]) / len(self._speed_seq) if len(self._speed_seq) > 1 else 0 85 | 86 | def brake_indicator(self, brake_indicator: bool | None = None) -> bool | None: 87 | if brake_indicator is None: 88 | return self._brake_indicator 89 | self._brake_indicator = brake_indicator 90 | 91 | def left_indicator(self, left_indicator: bool | None = None, override: bool = False) -> bool | None: 92 | if not override: 93 | if self._hazard: 94 | return True 95 | if left_indicator: 96 | self.right_indicator(False, True) 97 | if left_indicator is None: 98 | return self._left_indicator 99 | self._left_indicator = left_indicator 100 | 101 | def right_indicator(self, right_indicator: bool | None = None, override: bool = False) -> bool | None: 102 | if not override: 103 | if self._hazard: 104 | return True 105 | if right_indicator: 106 | self.left_indicator(False, True) 107 | if right_indicator is None: 108 | return self._right_indicator 109 | self._right_indicator = right_indicator 110 | 111 | def hazard(self, hazard: bool | None = None) -> bool | None: 112 | """ 113 | Set or get the hazard light status. 114 | :param hazard: True: hazard light on; False: hazard light off; None: getter mode 115 | :return: the hazard light status or None if setter mode 116 | """ 117 | if hazard is None: 118 | return self._hazard 119 | self.left_indicator(False, True) 120 | self.right_indicator(False, True) 121 | self.left_indicator(hazard, True) 122 | self.right_indicator(hazard, True) 123 | self._hazard = hazard 124 | -------------------------------------------------------------------------------- /leads/data_persistence/__init__.py: -------------------------------------------------------------------------------- 1 | from leads.data_persistence.core import * 2 | -------------------------------------------------------------------------------- /leads/data_persistence/_computational/__init__.py: -------------------------------------------------------------------------------- 1 | try: 2 | import cupy as _np 3 | except ImportError: 4 | import numpy as _np 5 | import pandas as _pandas 6 | 7 | mean: type[_np.mean] = _np.mean 8 | array: type[_np.array] = _np.array 9 | ndarray: type[_np.array] = _np.ndarray 10 | norm: type[_np.linalg.norm] = _np.linalg.norm 11 | sqrt: type[_np.sqrt] = _np.sqrt 12 | minimum: type[_np.min] = _np.min 13 | maximum: type[_np.max] = _np.max 14 | diff: type[_np.diff] = _np.diff 15 | 16 | read_csv: type[_pandas.read_csv] = _pandas.read_csv 17 | DataFrame: type[_pandas.DataFrame] = _pandas.DataFrame 18 | TextFileReader: type[_pandas.io.parsers.TextFileReader] = _pandas.io.parsers.TextFileReader 19 | -------------------------------------------------------------------------------- /leads/data_persistence/analyzer/__init__.py: -------------------------------------------------------------------------------- 1 | from leads.data_persistence.analyzer.inference import * 2 | from leads.data_persistence.analyzer.jarvis import * 3 | from leads.data_persistence.analyzer.preprocess import * 4 | from leads.data_persistence.analyzer.utils import * 5 | -------------------------------------------------------------------------------- /leads/data_persistence/analyzer/jarvis.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta as _ABCMeta, abstractmethod as _abstractmethod 2 | 3 | from leads.logger import L 4 | from .._computational import ndarray as _ndarray 5 | 6 | 7 | class JarvisBackend(object, metaclass=_ABCMeta): 8 | @_abstractmethod 9 | def predict(self, x: _ndarray, visual: _ndarray) -> tuple[float, float, float]: 10 | """ 11 | :param x: the input data 12 | :param visual: the visual data 13 | :return: [throttle, brake, steer] 14 | """ 15 | raise NotImplementedError 16 | 17 | 18 | class Jarvis(object): 19 | def __init__(self, backend: JarvisBackend) -> None: 20 | self.backend: JarvisBackend = backend 21 | L.info("Jarvis is enabled\n" 22 | "Before proceeding, you must read the following Terms and Conditions carefully:\n" 23 | "Jarvis is a deep learning model that aims to provide the optimal operation based on limited inputs.\n" 24 | "The user must be aware of the limitations of Jarvis and always drive with caution.\n" 25 | "The user must pay attention to the road at all times and follow the actual situation.\n" 26 | "The user must not blindly trust the instructions given by Jarvis.\n" 27 | "Local laws and regulations unconditionally take precedence over instructions given by Jarvis.\n" 28 | "Any personal injury caused by erroneous instructions shall be borne by the user.") 29 | -------------------------------------------------------------------------------- /leads/data_persistence/analyzer/preprocess.py: -------------------------------------------------------------------------------- 1 | from typing import Sequence as _Sequence, Any as _Any, SupportsFloat as _SupportsFloat 2 | 3 | from leads.data_persistence.analyzer import utils as _utils 4 | from .._computational import array as _array, ndarray as _ndarray 5 | 6 | 7 | class Preprocessor(object): 8 | def __init__(self, data_seq: _Sequence[dict[str, _Any]]) -> None: 9 | self._data_seq: _Sequence[dict[str, _Any]] = data_seq 10 | 11 | def to_tensor(self, channels: tuple[str, ...] = ("time", "speed", "latitude", "longitude")) -> _ndarray: 12 | r = [] 13 | for row in self._data_seq: 14 | r_row = [] 15 | for channel in channels: 16 | d = row[channel] 17 | if not isinstance(d, _SupportsFloat): 18 | raise TypeError(f"{d} ({channel}) is not a float and cannot be converted to a float") 19 | if getattr(_utils, f"{channel}_invalid", lambda _: False)(d): 20 | raise ValueError(f"Invalid value for {channel} ({d}) at row {len(r)}") 21 | r_row.append(d) 22 | r.append(r_row) 23 | return _array(r) 24 | -------------------------------------------------------------------------------- /leads/data_persistence/analyzer/utils.py: -------------------------------------------------------------------------------- 1 | from typing import Any as _Any 2 | 3 | 4 | def time_invalid(o: _Any) -> bool: 5 | return not isinstance(o, int) 6 | 7 | 8 | def speed_invalid(o: _Any) -> bool: 9 | return not isinstance(o, int | float) or o < 0 10 | 11 | 12 | def acceleration_invalid(o: _Any) -> bool: 13 | return not isinstance(o, int | float) 14 | 15 | 16 | def mileage_invalid(o: _Any) -> bool: 17 | return not isinstance(o, int | float) 18 | 19 | 20 | def latitude_invalid(o: _Any) -> bool: 21 | return not isinstance(o, int | float) or not -90 < o < 90 22 | 23 | 24 | def longitude_invalid(o: _Any) -> bool: 25 | return not isinstance(o, int | float) or not -180 < o < 180 26 | 27 | 28 | def latency_invalid(o: _Any) -> bool: 29 | return not isinstance(o, int | float) 30 | -------------------------------------------------------------------------------- /leads/dt/__init__.py: -------------------------------------------------------------------------------- 1 | from leads.dt.controller import * 2 | from leads.dt.device import * 3 | from leads.dt.odometer import * 4 | from leads.dt.predefined_tags import * 5 | from leads.dt.registry import * 6 | -------------------------------------------------------------------------------- /leads/dt/controller.py: -------------------------------------------------------------------------------- 1 | from typing import override as _override, overload as _overload 2 | 3 | from leads.dt.device import Device 4 | 5 | 6 | class Controller(Device): 7 | @_overload 8 | def __init__(self, *args, **kwargs) -> None: # real signature unknown 9 | raise NotImplementedError 10 | 11 | def __init__(self) -> None: 12 | super().__init__() 13 | self._devices: dict[str, Device] = {} 14 | 15 | def _attach_device(self, tag: str, device: Device) -> None: 16 | self._devices[tag] = device 17 | device.tag(tag) 18 | device.lock_tag() 19 | 20 | def devices(self) -> list[Device]: 21 | """ 22 | :return: the device list 23 | """ 24 | return list(self._devices.values()) 25 | 26 | def device(self, tag: str, device: Device | None = None) -> Device | None: 27 | """ 28 | Set or get a device by tag. The device's tag will be overwritten. 29 | :param tag: tag of the device (it shares the global namespace) 30 | :param device: the device or None if getter mode 31 | :return: the device or None if setter mode 32 | """ 33 | if device is None: 34 | return self._devices[tag] 35 | self._attach_device(tag, device) 36 | 37 | @_override 38 | def initialize(self, *parent_tags: str) -> None: 39 | super().initialize(*parent_tags) 40 | for device in self._devices.values(): 41 | device.initialize(*self._parent_tags, self._tag) 42 | -------------------------------------------------------------------------------- /leads/dt/device.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod as _abstractmethod, ABCMeta as _ABCMeta 2 | from threading import Thread as _Thread 3 | from typing import Any as _Any, override as _override, overload as _overload 4 | 5 | from leads.os import _thread_flags 6 | 7 | 8 | class Device(object): 9 | @_overload 10 | def __init__(self, *args, **kwargs) -> None: # real signature unknown 11 | raise NotImplementedError 12 | 13 | def __init__(self, *pins: int | str) -> None: 14 | self._tag: str = "" 15 | self._tag_locked: bool = False 16 | self._parent_tags: tuple[str, ...] = () 17 | self._pins: tuple[int | str, ...] = pins 18 | 19 | @_override 20 | def __str__(self) -> str: 21 | return f"{len(self._parent_tags)}.{self._tag}" 22 | 23 | def level(self) -> int: 24 | """ 25 | Get the level of the device in the device tree. 26 | :return: the number of parents 27 | """ 28 | return len(self._parent_tags) 29 | 30 | def tag(self, tag: str | None = None) -> str | None: 31 | """ 32 | Set or get the tag of the device. The tag will not be set if it is locked. 33 | :param tag: the tag or None if getter mode 34 | :return: the tag or None if setter mode 35 | """ 36 | if tag is None: 37 | return self._tag 38 | if not self._tag_locked: 39 | self._tag = tag 40 | 41 | def lock_tag(self) -> None: 42 | """ 43 | Lock the tag of the device. 44 | """ 45 | self._tag_locked = True 46 | 47 | def parent_tags(self) -> tuple[str, ...]: 48 | """ 49 | Get the parent tags of the device. 50 | :return: the parent tags 51 | """ 52 | return self._parent_tags[:] 53 | 54 | def initialize(self, *parent_tags: str) -> None: 55 | self._parent_tags = parent_tags 56 | 57 | def read(self) -> _Any: 58 | raise NotImplementedError 59 | 60 | def write(self, payload: _Any) -> None: 61 | raise NotImplementedError 62 | 63 | def update(self, data: _Any) -> None: 64 | raise NotImplementedError 65 | 66 | def close(self) -> None: 67 | ... 68 | 69 | 70 | class ShadowDevice(Device, metaclass=_ABCMeta): 71 | def __init__(self, *pins: int | str) -> None: 72 | super().__init__(*pins) 73 | self._shadow_thread: _Thread | None = None 74 | 75 | @_abstractmethod 76 | def loop(self) -> None: 77 | raise NotImplementedError 78 | 79 | def run(self) -> None: 80 | while _thread_flags.active: 81 | self.loop() 82 | 83 | @_override 84 | def initialize(self, *parent_tags: str) -> None: 85 | super().initialize(*parent_tags) 86 | self._shadow_thread = _Thread(name=f"{id(self)} shadow", target=self.run, daemon=True) 87 | self._shadow_thread.start() 88 | -------------------------------------------------------------------------------- /leads/dt/odometer.py: -------------------------------------------------------------------------------- 1 | from threading import Lock as _Lock 2 | from typing import override as _override 3 | 4 | from leads.dt.device import Device 5 | 6 | 7 | class Odometer(Device): 8 | def __init__(self) -> None: 9 | super().__init__() 10 | self._mileage: float = 0 11 | 12 | @_override 13 | def write(self, payload: float) -> None: 14 | self._mileage += payload 15 | 16 | @_override 17 | def read(self) -> float: 18 | return self._mileage 19 | 20 | 21 | class ConcurrentOdometer(Odometer): 22 | def __init__(self) -> None: 23 | super().__init__() 24 | self._lock: _Lock = _Lock() 25 | 26 | @_override 27 | def write(self, payload: float) -> None: 28 | self._lock.acquire() 29 | try: 30 | super().write(payload) 31 | finally: 32 | self._lock.release() 33 | -------------------------------------------------------------------------------- /leads/dt/predefined_tags.py: -------------------------------------------------------------------------------- 1 | MAIN_CONTROLLER: str = "__main__" 2 | 3 | LEFT_FRONT_WHEEL_SPEED_SENSOR: str = "wss_lf" 4 | RIGHT_FRONT_WHEEL_SPEED_SENSOR: str = "wss_rf" 5 | CENTER_REAR_WHEEL_SPEED_SENSOR: str = "wss_cr" 6 | LEFT_REAR_WHEEL_SPEED_SENSOR: str = "wss_lr" 7 | RIGHT_REAR_WHEEL_SPEED_SENSOR: str = "wss_rr" 8 | ACCELEROMETER: str = "accl" 9 | ODOMETER: str = "odm" 10 | VOLTAGE_SENSOR: str = "vot" 11 | THROTTLE_PEDAL: str = "tpd" 12 | BRAKE_PEDAL: str = "bpd" 13 | MOTOR_CONTROLLER: str = "mc" 14 | BRAKE_CONTROLLER: str = "bc" 15 | GPS_RECEIVER: str = "gps" 16 | BRAKE_INDICATOR: str = "bindi" 17 | LEFT_INDICATOR: str = "lindi" 18 | RIGHT_INDICATOR: str = "rindi" 19 | 20 | FRONT_VIEW_CAMERA: str = "frvc" 21 | LEFT_VIEW_CAMERA: str = "lfvc" 22 | RIGHT_VIEW_CAMERA: str = "rtvc" 23 | REAR_VIEW_CAMERA: str = "revc" 24 | 25 | POWER_CONTROLLER: str = "pc" 26 | WHEEL_SPEED_CONTROLLER: str = "wsc" 27 | -------------------------------------------------------------------------------- /leads/dt/registry.py: -------------------------------------------------------------------------------- 1 | from atexit import register as _register 2 | from typing import Any as _Any, Callable as _Callable, Sequence as _Sequence 3 | 4 | from leads.dt.controller import Controller 5 | from leads.dt.device import Device 6 | from leads.dt.predefined_tags import MAIN_CONTROLLER 7 | from leads.logger import L 8 | 9 | _controllers: dict[str, Controller] = {} 10 | _devices: dict[str, Device] = {} 11 | 12 | 13 | def controller(tag: str, 14 | parent: str | None = None, 15 | args: tuple[_Any, ...] = (), 16 | kwargs: dict[str, _Any] | None = None) -> _Callable[[type[Controller]], None]: 17 | if kwargs is None: 18 | kwargs = {} 19 | 20 | def _(target: type[Controller]) -> None: 21 | if not issubclass(target, Controller): 22 | raise TypeError("Controllers must inherit from `Controller`") 23 | register_controller(tag, target(*args, **kwargs), parent) 24 | 25 | return _ 26 | 27 | 28 | def device(tag: str | _Sequence[str], 29 | parent: str | _Sequence[str], 30 | args: tuple[_Any, ...] | list[tuple[_Any, ...]] = (), 31 | kwargs: dict[str, _Any] | list[dict[str, _Any]] | None = None) -> _Callable[[type[Device]], None]: 32 | if isinstance(tag, str): 33 | tag = [tag] 34 | n = len(tag) 35 | if isinstance(parent, str): 36 | parent = [parent] * n 37 | if isinstance(args, tuple): 38 | args = [args] * n 39 | if kwargs is None: 40 | kwargs = [{}] * n 41 | elif isinstance(kwargs, dict): 42 | kwargs = [kwargs] * n 43 | 44 | def _(target: type[Device]) -> None: 45 | if not issubclass(target, Device): 46 | raise TypeError("Devices must inherit from `Device`") 47 | for i in range(len(tag)): 48 | _register_device(target, tag[i], _controllers[parent[i]], args[i], kwargs[i]) 49 | 50 | return _ 51 | 52 | 53 | def register_controller(tag: str, c: Controller, parent: str | None = None) -> None: 54 | if tag in _controllers: 55 | raise RuntimeError(f"Cannot register: tag \"{tag}\" is already used") 56 | if parent: 57 | _controllers[parent].device(tag, c) 58 | else: 59 | c.tag(tag) 60 | c.lock_tag() 61 | _controllers[tag] = c 62 | 63 | 64 | def has_controller(tag: str) -> bool: 65 | return tag in _controllers 66 | 67 | 68 | def get_controller(tag: str) -> Controller: 69 | return _controllers[tag] 70 | 71 | 72 | def _register_device(prototype: type[Device], 73 | tag: str, 74 | parent: Controller, 75 | args: tuple[_Any, ...], 76 | kwargs: dict[str, _Any]) -> None: 77 | instance = prototype(*args, **kwargs) 78 | parent.device(tag, instance) 79 | _devices[tag] = instance 80 | 81 | 82 | def has_device(tag: str) -> bool: 83 | return tag in _devices 84 | 85 | 86 | def get_device(tag: str) -> Device: 87 | return _devices[tag] 88 | 89 | 90 | @_register 91 | def release() -> None: 92 | for d in _devices.values(): 93 | try: 94 | d.close() 95 | except Exception as e: 96 | L.debug(f"Error when closing device {d}: {repr(e)}") 97 | _devices.clear() 98 | for c in _controllers.values(): 99 | try: 100 | c.close() 101 | except Exception as e: 102 | L.debug(f"Error when closing controller {c}: {repr(e)}") 103 | _controllers.clear() 104 | 105 | 106 | def initialize_main() -> None: 107 | get_controller(MAIN_CONTROLLER).initialize() 108 | -------------------------------------------------------------------------------- /leads/event.py: -------------------------------------------------------------------------------- 1 | from typing import Any as _Any, override as _override, overload as _overload 2 | 3 | from leads.callback import CallbackChain 4 | from leads.context import Context 5 | from leads.data import DataContainer 6 | 7 | 8 | class Event(object): 9 | @_overload 10 | def __init__(self, *args, **kwargs) -> None: # real signature unknown 11 | raise NotImplementedError 12 | 13 | def __init__(self, t: str, context: Context) -> None: 14 | self.t: str = t 15 | self.context: Context = context 16 | 17 | @_override 18 | def __str__(self) -> str: 19 | return f"[{self.t}] {self.context.data()}" 20 | 21 | 22 | class DataPushedEvent(Event): 23 | def __init__(self, context: Context, data: DataContainer) -> None: 24 | super().__init__("DATA PUSHED", context) 25 | self.data: DataContainer = data 26 | 27 | 28 | class UpdateEvent(Event): 29 | def __init__(self, context: Context) -> None: 30 | super().__init__("UPDATE", context) 31 | 32 | 33 | class SystemEvent(Event): 34 | def __init__(self, t: str, context: Context, system: str) -> None: 35 | super().__init__(t, context) 36 | self.system: str = system 37 | 38 | 39 | class InterventionEvent(SystemEvent): 40 | def __init__(self, context: Context, system: str, *data: _Any) -> None: 41 | super().__init__("INTERVENTION", context, system) 42 | self.data: tuple[_Any, ...] = data 43 | 44 | 45 | class InterventionExitEvent(InterventionEvent): 46 | pass 47 | 48 | 49 | class SuspensionEvent(SystemEvent): 50 | def __init__(self, context: Context, system: str, cause: str, fatal: bool = False) -> None: 51 | super().__init__("SUSPENSION", context, system) 52 | self.cause: str = cause 53 | self.fatal: bool = fatal 54 | 55 | 56 | class SuspensionExitEvent(SuspensionEvent): 57 | pass 58 | 59 | 60 | class EventListener(CallbackChain): 61 | @_override 62 | def super(self, e: Event) -> None: 63 | super().super(e) 64 | 65 | def pre_push(self, event: DataPushedEvent) -> None: ... 66 | 67 | def post_push(self, event: DataPushedEvent) -> None: ... 68 | 69 | def on_update(self, event: UpdateEvent) -> None: ... 70 | 71 | def pre_intervene(self, event: InterventionEvent) -> None: ... 72 | 73 | def post_intervene(self, event: InterventionExitEvent) -> None: ... 74 | 75 | def pre_suspend(self, event: SuspensionEvent) -> None: ... 76 | 77 | def post_suspend(self, event: SuspensionExitEvent) -> None: ... 78 | 79 | def brake_indicator(self, event: Event, state: bool) -> None: ... 80 | 81 | def left_indicator(self, event: Event, state: bool) -> None: ... 82 | 83 | def right_indicator(self, event: Event, state: bool) -> None: ... 84 | 85 | def hazard(self, event: Event, state: bool) -> None: ... 86 | -------------------------------------------------------------------------------- /leads/leads.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar as _TypeVar, Any as _Any, override as _override, Literal as _Literal 2 | 3 | from leads.context import Context 4 | from leads.data import DataContainer 5 | from leads.dt import has_device 6 | from leads.event import EventListener, Event, DataPushedEvent, UpdateEvent, SuspensionEvent, InterventionEvent, \ 7 | InterventionExitEvent, SuspensionExitEvent 8 | from leads.plugin import Plugin 9 | from leads.sft import SFT 10 | 11 | T = _TypeVar("T", bound=DataContainer) 12 | 13 | 14 | class _SuspensionException(Exception): 15 | def __init__(self, event: SuspensionEvent) -> None: 16 | super().__init__() 17 | self.event = event 18 | 19 | 20 | class LEADS(Context[T]): 21 | def __init__(self, initial_data: T | None = None, data_seq_size: int = 100, num_laps_timed: int = 3) -> None: 22 | super().__init__(initial_data, data_seq_size, num_laps_timed) 23 | self._plugins: dict[str, Plugin] = {} 24 | self._event_listener: EventListener = EventListener() 25 | 26 | def plugin(self, key: str, plugin: Plugin | None = None) -> Plugin | None: 27 | if plugin is None: 28 | return self._plugins[key] 29 | self._plugins[key] = plugin 30 | plugin.on_load(self) 31 | 32 | def set_event_listener(self, event_listener: EventListener) -> None: 33 | event_listener.bind_chain(self._event_listener) 34 | self._event_listener = event_listener 35 | 36 | @_override 37 | def suspend(self, event: SuspensionEvent) -> None: 38 | if isinstance(event, SuspensionExitEvent): 39 | self._event_listener.post_suspend(event) 40 | else: 41 | self._event_listener.pre_suspend(event) 42 | 43 | def _acquire_data(self, name: str, key: str, mandatory: bool = True) -> _Any | None: 44 | try: 45 | return getattr(self.data(), name) 46 | except AttributeError: 47 | if mandatory: 48 | raise _SuspensionException(SuspensionEvent(self, key, f"No data for `{name}`")) 49 | 50 | def _do_plugin_callback(self, method: _Literal["pre_push", "post_push", "pre_update", "post_update"]) -> None: 51 | for key, plugin in self._plugins.items(): 52 | if plugin.enabled(): 53 | try: 54 | for tag in plugin.required_devices(): 55 | if not has_device(tag) or not SFT.device_ok(tag): 56 | raise _SuspensionException(SuspensionEvent(self, key, f"Device {tag} not ok")) 57 | getattr(plugin, method)(self, {d: self._acquire_data(d, key) for d in plugin.required_data()}) 58 | except _SuspensionException as e: 59 | self.suspend(e.event) 60 | 61 | @_override 62 | def push(self, data: T) -> None: 63 | self._event_listener.pre_push(DataPushedEvent(self, data)) 64 | self._do_plugin_callback("pre_push") 65 | super().push(data) 66 | self._do_plugin_callback("post_push") 67 | self._event_listener.post_push(DataPushedEvent(self, data)) 68 | 69 | @_override 70 | def intervene(self, event: InterventionEvent) -> None: 71 | if isinstance(event, InterventionExitEvent): 72 | self._event_listener.post_intervene(event) 73 | else: 74 | self._event_listener.pre_intervene(event) 75 | 76 | @_override 77 | def update(self) -> None: 78 | self._do_plugin_callback("pre_update") 79 | self._event_listener.on_update(UpdateEvent(self)) 80 | self._do_plugin_callback("post_update") 81 | 82 | @_override 83 | def brake_indicator(self, brake_indicator: bool | None = None) -> bool | None: 84 | initial_state = self._brake_indicator 85 | try: 86 | return super().brake_indicator(brake_indicator) 87 | finally: 88 | if self._brake_indicator != initial_state: 89 | self._event_listener.brake_indicator(Event("BRAKE_INDICATOR", self), brake_indicator) 90 | 91 | @_override 92 | def left_indicator(self, left_indicator: bool | None = None, override: bool = False) -> bool | None: 93 | initial_state = self._left_indicator 94 | try: 95 | return super().left_indicator(left_indicator, override) 96 | finally: 97 | if self._left_indicator != initial_state: 98 | self._event_listener.left_indicator(Event("LEFT_INDICATOR", self), left_indicator) 99 | 100 | @_override 101 | def right_indicator(self, right_indicator: bool | None = None, override: bool = False) -> bool | None: 102 | initial_state = self._right_indicator 103 | try: 104 | return super().right_indicator(right_indicator, override) 105 | finally: 106 | if self._right_indicator != initial_state: 107 | self._event_listener.right_indicator(Event("RIGHT_INDICATOR", self), right_indicator) 108 | 109 | @_override 110 | def hazard(self, hazard: bool | None = None) -> bool | None: 111 | if (r := super().hazard(hazard)) is None: 112 | self._event_listener.hazard(Event("HAZARD", self), hazard) 113 | return r 114 | -------------------------------------------------------------------------------- /leads/logger.py: -------------------------------------------------------------------------------- 1 | from collections import deque as _deque 2 | from datetime import datetime as _datetime 3 | from enum import IntEnum as _IntEnum 4 | from threading import Lock as _Lock 5 | 6 | from leads.config import set_on_register_config, ConfigTemplate 7 | from leads.os import _currentframe 8 | from leads.types import OnRegister as _OnRegister 9 | 10 | 11 | class Level(_IntEnum): 12 | DEBUG: int = 0 13 | INFO: int = 1 14 | WARN: int = 2 15 | ERROR: int = 3 16 | 17 | 18 | class Logger(object): 19 | REGULAR: int = 0 20 | BOLD: int = 1 21 | ITALIC: int = 3 22 | UNDERLINED: int = 4 23 | INVERSE: int = 7 24 | 25 | BLACK: int = 30 26 | RED: int = 31 27 | GREEN: int = 32 28 | YELLOW: int = 33 29 | BLUE: int = 34 30 | PURPLE: int = 35 31 | CYAN: int = 36 32 | WHITE: int = 37 33 | 34 | def __init__(self) -> None: 35 | self._history_messages: _deque[str] = _deque(maxlen=10) 36 | self._debug_level: Level = Level.DEBUG 37 | self._lock: _Lock = _Lock() 38 | 39 | def history_messages(self) -> tuple[str, ...]: 40 | return tuple(self._history_messages) 41 | 42 | def debug_level(self, debug_level: Level | None = None) -> Level | None: 43 | """ 44 | Set or get the debug level. 45 | :param debug_level: the debug level or None if getter mode 46 | :return: the debug level or None if setter mode 47 | """ 48 | if debug_level is None: 49 | return self._debug_level 50 | self._debug_level = debug_level 51 | 52 | @staticmethod 53 | def mark(msg: str, level: Level) -> str: 54 | return f"[{repr(level)[1:-1]}] [{_currentframe().f_back.f_back.f_code.co_name}] [{_datetime.now()}] {msg}" 55 | 56 | def format(self, msg: str, font: int, color: int | None, background: int | None) -> str: 57 | self._history_messages.append(msg) 58 | return f"\033[{font}{f";{color}" if color else ""}{f";{background + 10}" if background else ""}m{msg}\033[0m" 59 | 60 | def print(self, msg: str, level: int) -> None: 61 | self._lock.acquire() 62 | try: 63 | if self._debug_level <= level: 64 | print(msg) 65 | finally: 66 | self._lock.release() 67 | 68 | def info(self, *msg: str, sep: str = " ", end: str = "\n", 69 | f: tuple[int, int | None, int | None] = (REGULAR, None, None)) -> None: 70 | self.print(self.format(Logger.mark(sep.join(msg) + end, level=Level.INFO), *f), Level.INFO) 71 | 72 | def debug(self, *msg: str, sep: str = " ", end: str = "\n", 73 | f: tuple[int, int | None, int | None] = (REGULAR, YELLOW, None)) -> None: 74 | self.print(self.format(Logger.mark(sep.join(msg) + end, level=Level.DEBUG), *f), Level.DEBUG) 75 | 76 | def warn(self, *msg: str, sep: str = " ", end: str = "\n", 77 | f: tuple[int, int | None, int | None] = (REGULAR, RED, None)) -> None: 78 | self.print(self.format(Logger.mark(sep.join(msg) + end, level=Level.WARN), *f), Level.WARN) 79 | 80 | def error(self, *msg: str, sep: str = " ", end: str = "\n", 81 | f: tuple[int, int | None, int | None] = (REGULAR, RED, None)) -> None: 82 | self.print(self.format(Logger.mark(sep.join(msg) + end, Level.ERROR), *f), Level.ERROR) 83 | 84 | 85 | L: Logger = Logger() 86 | 87 | 88 | def _on_register_config(chain: _OnRegister[ConfigTemplate]) -> _OnRegister[ConfigTemplate]: 89 | def _(config: ConfigTemplate) -> None: 90 | chain(config) 91 | L.debug_level(Level[config.w_debug_level]) 92 | 93 | return _ 94 | 95 | 96 | set_on_register_config(_on_register_config) 97 | -------------------------------------------------------------------------------- /leads/ltm.py: -------------------------------------------------------------------------------- 1 | from json import loads as _loads, dumps as _dumps 2 | from os import chmod as _chmod, access as _access, R_OK as _R_OK, W_OK as _W_OK 3 | from os.path import abspath as _abspath, exists as _exists 4 | 5 | from leads.logger import L 6 | from leads.types import SupportedConfigValue as _SupportedConfigValue 7 | 8 | _PATH: str = f"{_abspath(__file__)[:-6]}_ltm/core" 9 | 10 | _ltm: dict[str, _SupportedConfigValue] = {} 11 | 12 | 13 | def _acquire_permission() -> bool: 14 | if _access(_PATH, _R_OK) and _access(_PATH, _W_OK): 15 | return True 16 | L.debug(f"Attempting to acquire permission for {_PATH}") 17 | try: 18 | _chmod(_PATH, 0o666) 19 | return True 20 | except Exception as e: 21 | L.debug(f"Failed to acquire permission: {repr(e)}") 22 | L.debug(f"For Linux users, try executing `sudo chmod 666 {_PATH}` manually or run LEADS as the root user") 23 | return False 24 | 25 | 26 | def _load_ltm() -> None: 27 | if not _permission_ok: 28 | return 29 | global _ltm 30 | try: 31 | if not _exists(_PATH): 32 | with open(_PATH, "w") as f: 33 | f.write("{}") 34 | return 35 | with open(_PATH) as f: 36 | ltm_content = f.read() 37 | if not (ltm_content.startswith("{") and ltm_content.endswith("}")): 38 | ltm_content = "{}" 39 | _ltm = _loads(ltm_content) 40 | except Exception as e: 41 | L.warn(f"Attempted but failed to load LTM: {repr(e)}") 42 | 43 | 44 | def _sync_ltm() -> None: 45 | if not _permission_ok: 46 | return 47 | try: 48 | with open(_PATH, "w") as f: 49 | f.write(_dumps(_ltm)) 50 | except Exception as e: 51 | L.warn(f"Attempted but failed to sync LTM: {repr(e)}") 52 | 53 | 54 | def ltm_get(key: str) -> _SupportedConfigValue: 55 | return _ltm[key] 56 | 57 | 58 | def ltm_set(key: str, value: _SupportedConfigValue) -> None: 59 | _ltm[key] = value 60 | _sync_ltm() 61 | 62 | 63 | _permission_ok: bool = _acquire_permission() 64 | L.debug(f"LTM permission {"OK" if _permission_ok else "NOT OK"}: {_PATH}") 65 | _load_ltm() 66 | -------------------------------------------------------------------------------- /leads/os.py: -------------------------------------------------------------------------------- 1 | from atexit import register as _register 2 | from threading import Lock as _Lock 3 | from types import FrameType as _FrameType 4 | 5 | 6 | def _currentframe() -> _FrameType: 7 | try: 8 | raise Exception 9 | except Exception as exc: 10 | return exc.__traceback__.tb_frame.f_back 11 | 12 | 13 | class _ThreadFlags(object): 14 | def __init__(self) -> None: 15 | self._lock: _Lock = _Lock() 16 | self._active: bool = True 17 | 18 | @property 19 | def active(self) -> bool: 20 | return self._active 21 | 22 | @active.setter 23 | def active(self, active: bool) -> None: 24 | self._lock.acquire() 25 | try: 26 | self._active = active 27 | finally: 28 | self._lock.release() 29 | 30 | 31 | _thread_flags: _ThreadFlags = _ThreadFlags() 32 | 33 | 34 | @_register 35 | def _request_threads_stop() -> None: 36 | _thread_flags.active = False 37 | -------------------------------------------------------------------------------- /leads/plugin/__init__.py: -------------------------------------------------------------------------------- 1 | from leads.plugin.abs import * 2 | from leads.plugin.atbs import * 3 | from leads.plugin.dtcs import * 4 | from leads.plugin.ebi import * 5 | from leads.plugin.plugin import * 6 | -------------------------------------------------------------------------------- /leads/plugin/abs.py: -------------------------------------------------------------------------------- 1 | from typing import Any as _Any, override as _override 2 | 3 | from leads.constant import SystemLiteral, ESCMode 4 | from leads.context import Context 5 | from leads.dt import WHEEL_SPEED_CONTROLLER, THROTTLE_PEDAL, BRAKE_PEDAL 6 | from leads.event import InterventionEvent, InterventionExitEvent 7 | from leads.plugin.plugin import ESCPlugin 8 | 9 | # (absolute, percentage) 10 | _CALIBRATIONS: dict[ESCMode, tuple[float | None, float | None]] = { 11 | ESCMode.STANDARD: (.01, .001), 12 | ESCMode.AGGRESSIVE: (1, .01), 13 | ESCMode.SPORT: (2, None), 14 | ESCMode.OFF: (None, None) 15 | } 16 | 17 | 18 | def do_abs(context: Context, 19 | front_wheel_speed: float, 20 | rear_wheel_speed: float) -> InterventionEvent: 21 | if ESCPlugin.adjudicate(front_wheel_speed - rear_wheel_speed, rear_wheel_speed, *_CALIBRATIONS[context.esc_mode()]): 22 | d = context.data() 23 | d.brake = 0 24 | return InterventionEvent(context, SystemLiteral.ABS, front_wheel_speed, rear_wheel_speed) 25 | return InterventionExitEvent(context, SystemLiteral.ABS, front_wheel_speed, rear_wheel_speed) 26 | 27 | 28 | class ABS(ESCPlugin): 29 | def __init__(self) -> None: 30 | super().__init__(("front_wheel_speed", "rear_wheel_speed"), 31 | (WHEEL_SPEED_CONTROLLER, THROTTLE_PEDAL, BRAKE_PEDAL)) 32 | 33 | @_override 34 | def pre_update(self, context: Context, kwargs: dict[str, _Any]) -> None: 35 | context.intervene(do_abs(context, kwargs["front_wheel_speed"], kwargs["rear_wheel_speed"])) 36 | -------------------------------------------------------------------------------- /leads/plugin/atbs.py: -------------------------------------------------------------------------------- 1 | from typing import override as _override, Any as _Any 2 | 3 | from leads.constant import SystemLiteral 4 | from leads.context import Context 5 | from leads.dt import THROTTLE_PEDAL, BRAKE_PEDAL 6 | from leads.event import InterventionEvent, InterventionExitEvent 7 | from leads.plugin.plugin import ESCPlugin 8 | 9 | 10 | def do_atbs(context: Context) -> InterventionEvent: 11 | return InterventionExitEvent(context, SystemLiteral.ATBS) 12 | 13 | 14 | class ATBS(ESCPlugin): 15 | def __init__(self) -> None: 16 | super().__init__((), (THROTTLE_PEDAL, BRAKE_PEDAL)) 17 | 18 | @_override 19 | def pre_update(self, context: Context, kwargs: dict[str, _Any]) -> None: 20 | context.intervene(do_atbs(context)) 21 | -------------------------------------------------------------------------------- /leads/plugin/dtcs.py: -------------------------------------------------------------------------------- 1 | from typing import Any as _Any, override as _override 2 | 3 | from leads.constant import SystemLiteral, ESCMode 4 | from leads.context import Context 5 | from leads.dt import WHEEL_SPEED_CONTROLLER, THROTTLE_PEDAL, BRAKE_PEDAL 6 | from leads.event import InterventionEvent, InterventionExitEvent 7 | from leads.plugin.plugin import ESCPlugin 8 | 9 | # (absolute, percentage) 10 | _CALIBRATIONS: dict[ESCMode, tuple[float | None, float | None]] = { 11 | ESCMode.STANDARD: (1, .05), 12 | ESCMode.AGGRESSIVE: (4, .15), 13 | ESCMode.SPORT: (8, None), 14 | ESCMode.OFF: (None, None) 15 | } 16 | 17 | 18 | def do_dtcs(context: Context, 19 | front_wheel_speed: float, 20 | rear_wheel_speed: float) -> InterventionEvent: 21 | if ESCPlugin.adjudicate(rear_wheel_speed - front_wheel_speed, front_wheel_speed, 22 | *_CALIBRATIONS[context.esc_mode()]): 23 | d = context.data() 24 | d.throttle = 0 25 | return InterventionEvent(context, SystemLiteral.DTCS, front_wheel_speed, rear_wheel_speed) 26 | return InterventionExitEvent(context, SystemLiteral.DTCS, front_wheel_speed, rear_wheel_speed) 27 | 28 | 29 | class DTCS(ESCPlugin): 30 | def __init__(self) -> None: 31 | super().__init__(("front_wheel_speed", "rear_wheel_speed"), 32 | (WHEEL_SPEED_CONTROLLER, THROTTLE_PEDAL, BRAKE_PEDAL)) 33 | 34 | @_override 35 | def pre_update(self, context: Context, kwargs: dict[str, _Any]) -> None: 36 | context.intervene(do_dtcs(context, kwargs["front_wheel_speed"], kwargs["rear_wheel_speed"])) 37 | -------------------------------------------------------------------------------- /leads/plugin/ebi.py: -------------------------------------------------------------------------------- 1 | from typing import override as _override, Any as _Any 2 | 3 | from leads.constant import SystemLiteral 4 | from leads.context import Context 5 | from leads.dt import THROTTLE_PEDAL, BRAKE_PEDAL 6 | from leads.event import InterventionEvent, InterventionExitEvent 7 | from leads.plugin.plugin import ESCPlugin 8 | 9 | 10 | def do_ebi(context: Context) -> InterventionEvent: 11 | return InterventionExitEvent(context, SystemLiteral.EBI) 12 | 13 | 14 | class EBI(ESCPlugin): 15 | def __init__(self) -> None: 16 | super().__init__((), (THROTTLE_PEDAL, BRAKE_PEDAL)) 17 | 18 | @_override 19 | def pre_update(self, context: Context, kwargs: dict[str, _Any]) -> None: 20 | context.intervene(do_ebi(context)) 21 | -------------------------------------------------------------------------------- /leads/plugin/plugin.py: -------------------------------------------------------------------------------- 1 | from typing import Any as _Any, override as _override 2 | 3 | from leads.constant import ESCMode 4 | from leads.context import Context 5 | from leads.registry import require_context 6 | 7 | 8 | class Plugin(object): 9 | def __init__(self, required_data: tuple[str, ...] = (), required_devices: tuple[str, ...] = ()) -> None: 10 | """ 11 | :param required_data: required data entries 12 | :param required_devices: required device tags 13 | """ 14 | super().__init__() 15 | self._required_data: tuple[str, ...] = required_data 16 | self._required_devices: tuple[str, ...] = required_devices 17 | self._enabled: bool = True 18 | 19 | def enabled(self, enabled: bool | None = None) -> bool | None: 20 | if enabled is None: 21 | return self._enabled 22 | self._enabled = enabled 23 | 24 | def required_data(self) -> tuple[str, ...]: 25 | return self._required_data 26 | 27 | def required_devices(self) -> tuple[str, ...]: 28 | return self._required_devices 29 | 30 | def on_load(self, context: Context) -> None: ... 31 | 32 | def pre_push(self, context: Context, kwargs: dict[str, _Any]) -> None: 33 | """ 34 | Note that the new data at this point is not available yet. 35 | :param context: target context 36 | :param kwargs: {required data: its value} 37 | """ 38 | ... 39 | 40 | def post_push(self, context: Context, kwargs: dict[str, _Any]) -> None: 41 | """ 42 | :param context: target context 43 | :param kwargs: {required data: its value} 44 | """ 45 | ... 46 | 47 | def pre_update(self, context: Context, kwargs: dict[str, _Any]) -> None: 48 | """ 49 | :param context: target context 50 | :param kwargs: {required data: its value} 51 | """ 52 | ... 53 | 54 | def post_update(self, context: Context, kwargs: dict[str, _Any]) -> None: 55 | """ 56 | :param context: target context 57 | :param kwargs: {required data: its value} 58 | """ 59 | ... 60 | 61 | 62 | class ESCPlugin(Plugin): 63 | @_override 64 | def enabled(self, enabled: bool | None = None) -> bool | None: 65 | if enabled is None: 66 | return require_context().esc_mode() != ESCMode.OFF and super().enabled() 67 | super().enabled(enabled) 68 | 69 | @staticmethod 70 | def adjudicate(d: float, base: float, absolute: float, percentage: float) -> bool: 71 | return d > 0 and ((absolute and d > absolute) or (percentage and d > base * percentage)) 72 | -------------------------------------------------------------------------------- /leads/registry.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar as _TypeVar 2 | 3 | from leads.context import Context 4 | from leads.types import OnRegister as _OnRegister, OnRegisterChain as _OnRegisterChain 5 | 6 | T = _TypeVar("T", bound=Context) 7 | 8 | _context_instance: T | None = None 9 | 10 | _on_register_context: _OnRegister[T] = lambda _: None 11 | 12 | 13 | def set_on_register_context(callback: _OnRegisterChain[T]) -> None: 14 | global _on_register_context 15 | _on_register_context = callback(_on_register_context) 16 | 17 | 18 | def register_context(context: T) -> None: 19 | global _context_instance 20 | if _context_instance: 21 | raise RuntimeError("Another context is already registered") 22 | _on_register_context(context) 23 | _context_instance = context 24 | 25 | 26 | def get_context() -> T | None: 27 | return _context_instance 28 | 29 | 30 | def require_context() -> T: 31 | if _context_instance: 32 | return _context_instance 33 | raise RuntimeError("No context registered") 34 | -------------------------------------------------------------------------------- /leads/sft.py: -------------------------------------------------------------------------------- 1 | from typing import Callable as _Callable 2 | 3 | from leads.dt import Device 4 | from leads.event import SuspensionEvent, SuspensionExitEvent 5 | from leads.logger import L 6 | from leads.registry import require_context 7 | 8 | 9 | def mark_device(device: Device, system: str, *related: str, append: bool = True) -> None: 10 | setattr(device, "__sft_marker__", list(set(getattr(device, "__sft_marker__") + [ 11 | system, *related] if append and hasattr(device, "__sft_marker__") else [system, *related]))) 12 | 13 | 14 | def read_device_marker(device: Device) -> list[str] | None: 15 | return getattr(device, "__sft_marker__") if hasattr(device, "__sft_marker__") else None 16 | 17 | 18 | class SystemFailureTracer(object): 19 | def __init__(self) -> None: 20 | super().__init__() 21 | self.on_fail: _Callable[[SuspensionEvent], None] = lambda _: None 22 | self.on_recover: _Callable[[SuspensionExitEvent], None] = lambda _: None 23 | self.on_device_fail: _Callable[[Device, str | Exception], None] = lambda _, __: None 24 | self.on_device_recover: _Callable[[Device], None] = lambda _: None 25 | self._system_failures: dict[str, int] = {} 26 | self._device_failures: dict[str, int] = {} 27 | 28 | def system_ok(self, system: str) -> bool: 29 | return system not in self._system_failures or self._system_failures[system] < 1 30 | 31 | def device_ok(self, tag: str) -> bool: 32 | return tag not in self._device_failures or self._device_failures[tag] < 1 33 | 34 | def fail(self, device: Device, error: str | Exception) -> None: 35 | if isinstance(error, Exception): 36 | error = repr(error) 37 | if not (systems := read_device_marker(device)): 38 | raise RuntimeWarning(f"No system marked for device {device}") 39 | if (tag := device.tag()) not in self._device_failures: 40 | self._device_failures[tag] = 0 41 | self._device_failures[tag] += 1 42 | self.on_device_fail(device, error) 43 | L.error(f"{device} error: {error}") 44 | for system in systems: 45 | if system not in self._system_failures: 46 | self._system_failures[system] = 0 47 | self._system_failures[system] += 1 48 | self.on_fail(e := SuspensionEvent(context := require_context(), system, error)) 49 | context.suspend(e) 50 | 51 | def recover(self, device: Device) -> None: 52 | if not (systems := read_device_marker(device)): 53 | raise RuntimeWarning(f"System not marked for device {device}") 54 | if (tag := device.tag()) in self._device_failures: 55 | self._device_failures[tag] -= 1 56 | if self._device_failures[tag] < 1: 57 | self._device_failures.pop(tag) 58 | self.on_device_recover(device) 59 | L.debug(f"{device} recovered") 60 | for system in systems: 61 | if system not in self._system_failures: 62 | continue 63 | self._system_failures[system] -= 1 64 | if self._system_failures[system] > 0: 65 | continue 66 | self._system_failures.pop(system) 67 | self.on_recover(e := SuspensionExitEvent(context := require_context(), system, "Recovered")) 68 | context.suspend(e) 69 | 70 | 71 | SFT: SystemFailureTracer = SystemFailureTracer() 72 | -------------------------------------------------------------------------------- /leads/types.py: -------------------------------------------------------------------------------- 1 | from typing import Callable as _Callable, SupportsInt as _SupportsInt, SupportsFloat as _SupportsFloat 2 | 3 | type Number = int | float | _SupportsInt | _SupportsFloat 4 | type Compressor[T] = _Callable[[dict[T, float], int], dict[T, float]] 5 | type OnRegister[T] = _Callable[[T], None] 6 | type OnRegisterChain[T] = _Callable[[OnRegister[T]], OnRegister[T]] 7 | type SupportedConfigValue = bool | int | float | str | None 8 | type SupportedConfig = SupportedConfigValue | tuple[SupportedConfig, ...] 9 | type DefaultHeader = tuple[ 10 | str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str] 11 | type DefaultHeaderFull = tuple[ 12 | str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str] 13 | type VisualHeader = tuple[ 14 | str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, 15 | str, str, str, str, str] 16 | type VisualHeaderFull = tuple[ 17 | str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, 18 | str, str, str, str, str, str, str, str] 19 | -------------------------------------------------------------------------------- /leads_arduino/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.util import find_spec as _find_spec 2 | 3 | if not _find_spec("serial"): 4 | raise ImportError("Please install `pyserial` to run this module\n>>>pip install pyserial") 5 | 6 | from leads_arduino.accelerometer import * 7 | from leads_arduino.arduino_proto import * 8 | from leads_arduino.arduino_nano import * 9 | from leads_arduino.arduino_micro import * 10 | from leads_arduino.pedal import * 11 | from leads_arduino.voltage_sensor import * 12 | from leads_arduino.wheel_speed_sensor import * 13 | -------------------------------------------------------------------------------- /leads_arduino/accelerometer.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass as _dataclass 2 | from typing import override as _override, Self as _Self 3 | 4 | from numpy import ndarray as _ndarray, sin as _sin, cos as _cos, array as _array, deg2rad as _deg2rad 5 | 6 | from leads import Device as _Device, Serializable as _Serializable 7 | 8 | 9 | def rotation_matrix(yaw: float, pitch: float, roll: float) -> _ndarray: 10 | yaw, pitch, roll = _deg2rad(yaw), _deg2rad(pitch), _deg2rad(roll) 11 | sy, cy, sp, cp, sr, cr = _sin(yaw), _cos(yaw), _sin(pitch), _cos(pitch), _sin(roll), _cos(roll) 12 | return _array([[cy, -sy, 0], [sy, cy, 0], [0, 0, 1]]) @ _array([[cp, 0, sp], [0, 1, 0], [-sp, 0, cp]]) @ _array( 13 | [[1, 0, 0], [0, cr, -sr], [0, sr, cr]]) 14 | 15 | 16 | _G: _ndarray = _array([0, 0, 9.8067]) 17 | 18 | 19 | @_dataclass 20 | class Acceleration(_Serializable): 21 | yaw: float 22 | pitch: float 23 | roll: float 24 | forward_acceleration: float 25 | lateral_acceleration: float 26 | vertical_acceleration: float 27 | 28 | def is_linear(self) -> bool: 29 | return isinstance(self, _LinearAcceleration) 30 | 31 | def linear(self) -> _Self: 32 | fg = rotation_matrix(self.yaw, self.pitch, self.roll).T @ _G 33 | return _LinearAcceleration(self.yaw, self.pitch, self.roll, float(self.forward_acceleration + fg[0]), 34 | float(self.lateral_acceleration + fg[1]), float(self.vertical_acceleration - fg[2])) 35 | 36 | 37 | class _LinearAcceleration(Acceleration): 38 | @_override 39 | def linear(self) -> _Self: 40 | return self 41 | 42 | 43 | class Accelerometer(_Device): 44 | def __init__(self) -> None: 45 | super().__init__() 46 | self._yaw: float = 0 47 | self._pitch: float = 0 48 | self._roll: float = 0 49 | self._fa: float = 0 50 | self._la: float = 0 51 | self._va: float = 0 52 | 53 | @_override 54 | def update(self, data: str) -> None: 55 | self._yaw, self._pitch, self._roll, self._fa, self._la, self._va = tuple(map(float, data.split(','))) 56 | 57 | @_override 58 | def read(self) -> Acceleration: 59 | return Acceleration(self._yaw, self._pitch, self._roll, self._fa, self._la, self._va) 60 | -------------------------------------------------------------------------------- /leads_arduino/arduino_micro.py: -------------------------------------------------------------------------------- 1 | from leads_arduino.arduino_proto import ArduinoProto 2 | 3 | 4 | class ArduinoMicro(ArduinoProto): 5 | pass 6 | -------------------------------------------------------------------------------- /leads_arduino/arduino_nano.py: -------------------------------------------------------------------------------- 1 | from leads_arduino.arduino_proto import ArduinoProto 2 | 3 | 4 | class ArduinoNano(ArduinoProto): 5 | pass 6 | -------------------------------------------------------------------------------- /leads_arduino/arduino_proto.py: -------------------------------------------------------------------------------- 1 | from typing import override as _override, Literal as _Literal 2 | 3 | from serial import Serial as _Serial 4 | 5 | from leads import Controller as _Controller, SFT as _SFT, L as _L 6 | from leads.comm import Entity as _Entity, Callback as _Callback, Service as _Service 7 | from leads_comm_serial import SerialConnection as _SerialConnection, AutoIdentity as _AutoIdentity 8 | 9 | 10 | class ArduinoProto(_Controller, _Entity, _AutoIdentity): 11 | """ 12 | Supports: 13 | - Any arduino connected through a USB (serial) port 14 | """ 15 | 16 | def __init__(self, port: str | _Literal["auto"], baud_rate: int = 9600) -> None: 17 | _Controller.__init__(self) 18 | _Entity.__init__(self, -1, _ArduinoCallback(self)) 19 | _AutoIdentity.__init__(self, port == "auto") 20 | self._serial: _Serial = _Serial() 21 | self._serial.baudrate = baud_rate 22 | self._connection: _SerialConnection | None = None 23 | self._serial.port = self.suggest_next_port() if port == "auto" else port 24 | 25 | @_override 26 | def port(self) -> str: 27 | return self._serial.port 28 | 29 | @_override 30 | def initialize(self, *parent_tags: str) -> None: 31 | super().initialize(*parent_tags) 32 | self.start(True) 33 | 34 | @_override 35 | def update(self, data: str) -> None: 36 | for d in self.devices(): 37 | if data.startswith(f"{d.tag()}:"): 38 | d.update(data[len(d.tag()) + 1:]) 39 | 40 | @_override 41 | def check_identity(self, connection: _SerialConnection) -> bool: 42 | connection.send(b"ic") 43 | return (msg := connection.receive()) and (msg.startswith(self.tag().encode()) or any( 44 | msg.startswith(d.tag().encode()) for d in self.devices())) 45 | 46 | @_override 47 | def run(self) -> None: 48 | self._callback.on_initialize(self) 49 | self._connection = self.establish_connection(self._serial) 50 | self._callback.on_connect(self, self._connection) 51 | self._stage(self._connection) 52 | 53 | @_override 54 | def write(self, payload: bytes) -> None: 55 | if not self._connection: 56 | raise IOError("Target must be connected to perform this operation") 57 | self._connection.send(payload) 58 | 59 | @_override 60 | def close(self) -> None: 61 | if self._connection: 62 | self._connection.close() 63 | 64 | 65 | class _ArduinoCallback(_Callback): 66 | def __init__(self, arduino: ArduinoProto) -> None: 67 | super().__init__() 68 | self._arduino: ArduinoProto = arduino 69 | 70 | @_override 71 | def on_receive(self, service: _Service, msg: bytes) -> None: 72 | self.super(service=service, msg=msg) 73 | try: 74 | self._arduino.update(msg.decode()) 75 | except UnicodeDecodeError: 76 | _L.debug(f"Discarding this message: {msg}") 77 | 78 | @_override 79 | def on_fail(self, service: _Service, error: Exception) -> None: 80 | self.super(service=service, error=error) 81 | _SFT.fail(self._arduino, error) 82 | -------------------------------------------------------------------------------- /leads_arduino/pedal.py: -------------------------------------------------------------------------------- 1 | from typing import override as _override 2 | 3 | from leads import Device as _Device 4 | 5 | 6 | class Pedal(_Device): 7 | """ 8 | See LEADS-Arduino. 9 | 10 | Supports: 11 | - Any analog pedal 12 | """ 13 | 14 | def __init__(self) -> None: 15 | super().__init__() 16 | self._position: float = 0 17 | 18 | @_override 19 | def update(self, data: str) -> None: 20 | self._position = float(data) 21 | 22 | @_override 23 | def read(self) -> float: 24 | """ 25 | :return: pedal position that ranges from 0 to 1 26 | """ 27 | return self._position 28 | -------------------------------------------------------------------------------- /leads_arduino/voltage_sensor.py: -------------------------------------------------------------------------------- 1 | from typing import override as _override 2 | 3 | from leads import Device as _Device 4 | 5 | 6 | class VoltageSensor(_Device): 7 | """ 8 | See LEADS-Arduino. 9 | 10 | Supports: 11 | - Any analog voltage sensor 12 | """ 13 | 14 | def __init__(self) -> None: 15 | super().__init__() 16 | self._voltage: float = 0 17 | 18 | @_override 19 | def update(self, data: str) -> None: 20 | self._voltage = float(data) 21 | 22 | @_override 23 | def read(self) -> float: 24 | """ 25 | :return: voltage 26 | """ 27 | return self._voltage 28 | -------------------------------------------------------------------------------- /leads_arduino/wheel_speed_sensor.py: -------------------------------------------------------------------------------- 1 | from time import time as _time 2 | from typing import override as _override 3 | 4 | from numpy import pi as _pi 5 | 6 | from leads import Device as _Device, get_device as _get_device, Odometer as _Odometer 7 | from leads_arduino.accelerometer import Accelerometer 8 | 9 | 10 | def rpm2kmh(rpm: float, wheel_circumference: float) -> float: 11 | """ 12 | :param rpm: revolutions per minute 13 | :param wheel_circumference: wheel circumference in centimeters 14 | :return: speed in kilometers per hour 15 | """ 16 | return rpm * wheel_circumference * 6e-4 17 | 18 | 19 | class WheelSpeedSensor(_Device): 20 | """ 21 | See LEADS-Arduino. 22 | 23 | Supports: 24 | - Any Hall effect sensor (switch) 25 | """ 26 | 27 | def __init__(self, wheel_diameter: float, num_divisions: int = 1, odometer_tag: str | None = None, 28 | accelerometer_tag: str | None = None) -> None: 29 | super().__init__() 30 | self._wheel_circumference: float = wheel_diameter * 2.54 * _pi 31 | self._num_divisions: int = num_divisions 32 | self._wheel_speed: float = 0 33 | self._last_valid: float = 0 34 | self._odometer_tag: str | None = odometer_tag 35 | self._odometer: _Odometer | None = None 36 | self._accelerometer_tag: str | None = accelerometer_tag 37 | self._accelerometer: Accelerometer | None = None 38 | self._last_acceleration: float = 0 39 | 40 | @_override 41 | def initialize(self, *parent_tags: str) -> None: 42 | super().initialize(*parent_tags) 43 | if self._odometer_tag: 44 | self._odometer = _get_device(self._odometer_tag) 45 | if not isinstance(self._odometer, _Odometer): 46 | raise TypeError(f"Unexpected odometer type: {type(self._odometer)}") 47 | if self._accelerometer_tag: 48 | self._accelerometer = _get_device(self._accelerometer_tag) 49 | if not isinstance(self._accelerometer, Accelerometer): 50 | raise TypeError(f"Unexpected accelerometer type: {type(self._accelerometer)}") 51 | 52 | @_override 53 | def update(self, data: str) -> None: 54 | t = _time() 55 | ws = rpm2kmh(float(data), self._wheel_circumference) / self._num_divisions 56 | if self._accelerometer and self._last_acceleration: 57 | a = self._accelerometer.read().linear().forward_acceleration 58 | v = (a + self._last_acceleration) * 1.8 * (t - self._last_valid) 59 | # add a small constant to avoid zero division 60 | if abs((ws - self._wheel_speed - v) / (v + 1e-10)) > 1.5: 61 | return 62 | self._last_acceleration = a 63 | self._wheel_speed = ws 64 | self._last_valid = t 65 | if self._odometer: 66 | self._odometer.write(self._wheel_circumference * 1e-5) 67 | 68 | @_override 69 | def read(self) -> float: 70 | """ 71 | :return: speed in kilometers per hour 72 | """ 73 | # add a small constant to avoid zero division 74 | r = rpm2kmh(60 / (1e-10 + _time() - self._last_valid), self._wheel_circumference) / self._num_divisions 75 | return 0 if r < .1 else r if r < 5 else self._wheel_speed 76 | -------------------------------------------------------------------------------- /leads_audio/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.util import find_spec as _find_spec 2 | 3 | if not _find_spec("sdl2"): 4 | raise ImportError("Please install `PySDL2` and `pysdl2-dll` to run this module\n>>>pip install PySDL2 pysdl2-dll") 5 | 6 | from leads_audio.prototype import * 7 | -------------------------------------------------------------------------------- /leads_audio/assets/confirm.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_audio/assets/confirm.mp3 -------------------------------------------------------------------------------- /leads_audio/assets/direction-indicator-off.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_audio/assets/direction-indicator-off.mp3 -------------------------------------------------------------------------------- /leads_audio/assets/direction-indicator-on.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_audio/assets/direction-indicator-on.mp3 -------------------------------------------------------------------------------- /leads_audio/assets/warning.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_audio/assets/warning.mp3 -------------------------------------------------------------------------------- /leads_audio/prototype.py: -------------------------------------------------------------------------------- 1 | from atexit import register as _register 2 | from threading import Thread as _Thread 3 | from time import sleep as _sleep 4 | 5 | from sdl2 import AUDIO_S16 as _AUDIO_S16 6 | from sdl2.sdlmixer import Mix_Init as _init, Mix_OpenAudioDevice as _open_audio_device, MIX_INIT_MP3 as _MIX_INIT_MP3, \ 7 | Mix_LoadMUS as _load_music, Mix_PlayMusic as _play_music, Mix_Music as _Music, \ 8 | Mix_GetError as _get_error, Mix_FreeMusic as _free_music, Mix_CloseAudio as _close_audio 9 | 10 | from leads import L as _L 11 | from leads_audio.system import _ASSETS_PATH 12 | 13 | 14 | def _ensure(flag: int) -> None: 15 | if flag < 0: 16 | raise RuntimeError(_get_error().decode()) 17 | 18 | 19 | _init(_MIX_INIT_MP3) 20 | 21 | 22 | def _try_open_audio_device() -> None: 23 | flag = -1 24 | while flag < 0: 25 | try: 26 | _ensure(flag := _open_audio_device(44100, _AUDIO_S16, 2, 2048, None, 1)) 27 | except RuntimeError as e: 28 | _L.error(repr(e)) 29 | _sleep(10) 30 | 31 | 32 | _Thread(name="sdl initializer", target=_try_open_audio_device, daemon=True).start() 33 | 34 | _register(_close_audio) 35 | 36 | 37 | class _SoundEffect(object): 38 | def __init__(self, name: str) -> None: 39 | self._name: str = name 40 | self._source: _Music | None = None 41 | 42 | def load_source(self) -> _Music: 43 | if self._source is None: 44 | self._source = _load_music(f"{_ASSETS_PATH}/{self._name}.mp3".encode()) 45 | return self._source 46 | 47 | def play(self) -> None: 48 | _ensure(_play_music(self.load_source(), 0)) 49 | 50 | def stop(self) -> None: 51 | _ensure(_free_music(self.load_source())) 52 | 53 | 54 | CONFIRM: _SoundEffect = _SoundEffect("confirm") 55 | DIRECTION_INDICATOR_ON: _SoundEffect = _SoundEffect("direction-indicator-on") 56 | DIRECTION_INDICATOR_OFF: _SoundEffect = _SoundEffect("direction-indicator-off") 57 | WARNING: _SoundEffect = _SoundEffect("warning") 58 | -------------------------------------------------------------------------------- /leads_audio/system.py: -------------------------------------------------------------------------------- 1 | from os.path import abspath as _abspath 2 | 3 | _ASSETS_PATH: str = f"{_abspath(__file__)[:-9]}assets" 4 | -------------------------------------------------------------------------------- /leads_can/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.util import find_spec as _find_spec 2 | 3 | if not _find_spec("can"): 4 | raise ImportError("Please install `python-can` to run this module\n>>>pip install python-can") 5 | 6 | from leads_can.prototype import * 7 | from leads_can.obd import * 8 | -------------------------------------------------------------------------------- /leads_can/obd.py: -------------------------------------------------------------------------------- 1 | from typing import override as _override 2 | 3 | from can import Message as _Message 4 | 5 | from leads import DataContainer as _DataContainer 6 | from leads_can.prototype import CANBus 7 | 8 | 9 | class OBD2(CANBus): 10 | @_override 11 | def write(self, payload: _DataContainer) -> None: 12 | t = payload.time_stamp() * .001 13 | super().write(_Message(t, 0x00, data=str(payload.voltage).encode())) 14 | super().write(_Message(t, 0x01, data=str(payload.speed).encode())) 15 | super().write(_Message(t, 0x10, data=str(payload.front_wheel_speed).encode())) 16 | super().write(_Message(t, 0x11, data=str(payload.rear_wheel_speed).encode())) 17 | super().write(_Message(t, 0x20, data=str(payload.forward_acceleration).encode())) 18 | super().write(_Message(t, 0x21, data=str(payload.lateral_acceleration).encode())) 19 | super().write(_Message(t, 0x30, data=str(payload.mileage).encode())) 20 | super().write(_Message(t, 0x40, data=str(payload.gps_ground_speed).encode(), 21 | is_error_frame=not payload.gps_valid)) 22 | super().write(_Message(t, 0x41, data=str(payload.latitude).encode(), 23 | is_error_frame=not payload.gps_valid)) 24 | super().write(_Message(t, 0x42, data=str(payload.longitude).encode(), 25 | is_error_frame=not payload.gps_valid)) 26 | super().write(_Message(t, 0x50, data=str(payload.throttle).encode())) 27 | super().write(_Message(t, 0x51, data=str(payload.brake).encode())) 28 | -------------------------------------------------------------------------------- /leads_can/prototype.py: -------------------------------------------------------------------------------- 1 | from typing import override as _override 2 | 3 | from can import Bus as _Bus, Notifier as _Notifier, Listener as _Listener, Message as _Message 4 | 5 | from leads import Controller as _Controller 6 | 7 | 8 | class CANBus(_Controller, _Listener): 9 | def __init__(self) -> None: 10 | _Controller.__init__(self) 11 | _Listener.__init__(self) 12 | self._bus: _Bus | None = None 13 | self._notifier: _Notifier | None = None 14 | 15 | def on_message_received(self, msg: _Message) -> None: 16 | for device in self.devices(): 17 | device.update(msg) 18 | 19 | @_override 20 | def initialize(self, *parent_tags: str) -> None: 21 | super().initialize(*parent_tags) 22 | self._bus = _Bus() 23 | self._notifier = _Notifier(self._bus, (self,)) 24 | 25 | @_override 26 | def write(self, payload: _Message) -> None: 27 | self._bus.send(payload) 28 | 29 | @_override 30 | def close(self) -> None: 31 | self._bus.close() 32 | -------------------------------------------------------------------------------- /leads_comm_serial/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.util import find_spec as _find_spec 2 | 3 | if not _find_spec("serial"): 4 | raise ImportError("Please install `pyserial` to run this module\n>>>pip install pyserial") 5 | 6 | from leads_comm_serial.connection import * 7 | from leads_comm_serial.identity import * 8 | from leads_comm_serial.sobd import * 9 | -------------------------------------------------------------------------------- /leads_comm_serial/connection.py: -------------------------------------------------------------------------------- 1 | from time import time as _time 2 | from typing import override as _override, Literal as _Literal, Self as _Self 3 | 4 | from serial import Serial as _Serial 5 | 6 | from leads.comm import ConnectionBase as _ConnectionBase 7 | 8 | 9 | class SerialConnection(_ConnectionBase): 10 | def __init__(self, serial: _Serial, remainder: bytes = b"", separator: bytes = b";") -> None: 11 | super().__init__(remainder, separator) 12 | self._serial: _Serial = serial 13 | 14 | @_override 15 | def closed(self) -> bool: 16 | return self._serial.closed 17 | 18 | def _require_open_serial(self, mandatory: bool = True) -> _Serial: 19 | if mandatory and self.closed(): 20 | raise IOError("An open serial is required") 21 | return self._serial 22 | 23 | @_override 24 | def receive(self, chunk_size: int = 1) -> bytes | None: 25 | if self._remainder != b"": 26 | return self.use_remainder() 27 | start = _time() 28 | try: 29 | msg = chunk = b"" 30 | while not ((t_o := self._serial.timeout) and _time() - start > t_o) and self._separator not in chunk: 31 | msg += (chunk := self._require_open_serial().read(chunk_size)) 32 | return self.with_remainder(msg) 33 | except IOError: 34 | return None 35 | 36 | @_override 37 | def send(self, msg: bytes | _Literal[b"disconnect"]) -> None: 38 | self._require_open_serial().write(msg + self._separator) 39 | 40 | @_override 41 | def close(self) -> None: 42 | self.disconnect() 43 | self._require_open_serial(False).close() 44 | 45 | def suspect(self, timeout: int = 1) -> _Self: 46 | self._serial.timeout = timeout 47 | return self 48 | 49 | def trust(self) -> _Self: 50 | self._serial.timeout = None 51 | return self 52 | -------------------------------------------------------------------------------- /leads_comm_serial/identity.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta as _ABCMeta, abstractmethod as _abstractmethod 2 | from threading import Lock as _Lock 3 | 4 | from serial import Serial as _Serial, SerialException as _SerialException 5 | from serial.tools.list_ports import comports as _comports 6 | 7 | from leads_comm_serial.connection import SerialConnection 8 | 9 | _available_ports: list[str] = [_p for _p, _, __ in _comports()] 10 | _lock: _Lock = _Lock() 11 | 12 | 13 | class AutoIdentity(object, metaclass=_ABCMeta): 14 | def __init__(self, retry: bool = False, remainder: bytes = b"", separator: bytes = b";") -> None: 15 | self._retry: bool = retry 16 | self._remainder: bytes = remainder 17 | self._separator: bytes = separator 18 | self._tried_ports: list[str] = [] 19 | _instances[self] = None 20 | 21 | def meta(self) -> tuple[bool, bytes, bytes]: 22 | return self._retry, self._remainder, self._separator 23 | 24 | def suggest_next_port(self, tried_port: str | None = None) -> str | None: 25 | if tried_port: 26 | self._tried_ports.append(tried_port) 27 | for port in _available_ports: 28 | if port not in self._tried_ports: 29 | return port 30 | 31 | @_abstractmethod 32 | def check_identity(self, connection: SerialConnection) -> bool: 33 | raise NotImplementedError 34 | 35 | def _establish_connection_no_lock(self, serial: _Serial) -> SerialConnection: 36 | try: 37 | _detect_ports() 38 | if port := _instances[self]: 39 | serial.port = port 40 | self._retry = False 41 | elif not serial.port: 42 | raise ConnectionError("No available port") 43 | elif serial.port not in _available_ports: 44 | raise ValueError("Port taken") 45 | serial.open() 46 | connection = SerialConnection(serial, self._remainder, self._separator).suspect() 47 | if port or self.check_identity(connection): 48 | return connection.trust() 49 | for instance in _instances.keys(): 50 | if instance.check_identity(connection): 51 | _instances[instance] = serial.port 52 | raise ValueError("Unexpected identity") 53 | except (_SerialException, ValueError) as e: 54 | serial.close() 55 | if not self._retry: 56 | raise ConnectionError(f"Unable to establish connection due to {repr(e)}") 57 | serial.port = self.suggest_next_port(serial.port) 58 | return self._establish_connection_no_lock(serial) 59 | 60 | def establish_connection(self, serial: _Serial) -> SerialConnection: 61 | _lock.acquire() 62 | try: 63 | return self._establish_connection_no_lock(serial) 64 | finally: 65 | _lock.release() 66 | 67 | 68 | _instances: dict[AutoIdentity, str | None] = {} 69 | _ports_detected: bool = False 70 | 71 | 72 | def _detect_ports() -> None: 73 | global _ports_detected 74 | if _ports_detected: 75 | return 76 | for port in tuple(_available_ports): 77 | serial = _Serial() 78 | serial.port = port 79 | serial.open() 80 | connection = SerialConnection(serial).suspect() 81 | for instance in _instances.keys(): 82 | connection._remainder = b"" 83 | connection._separator = instance.meta()[2] 84 | if instance.check_identity(connection): 85 | _instances[instance] = port 86 | _available_ports.remove(port) 87 | break 88 | serial.close() 89 | _ports_detected = True 90 | -------------------------------------------------------------------------------- /leads_comm_serial/sobd/__init__.py: -------------------------------------------------------------------------------- 1 | from leads_comm_serial.sobd.sobd import * 2 | -------------------------------------------------------------------------------- /leads_comm_serial/sobd/sobd.py: -------------------------------------------------------------------------------- 1 | from typing import Literal as _Literal, override as _override 2 | 3 | from serial import Serial as _Serial 4 | 5 | from leads import require_config as _require_config, Device as _Device, L as _L 6 | from leads.comm import Entity as _Entity, Callback as _Callback, Service as _Service, ConnectionBase as _ConnectionBase 7 | from leads_comm_serial.connection import SerialConnection 8 | from leads_comm_serial.identity import AutoIdentity 9 | 10 | 11 | class SOBD(_Device, _Entity, AutoIdentity): 12 | def __init__(self, port: str | _Literal["auto"], baud_rate: int = 9600, password: str = "") -> None: 13 | _Device.__init__(self) 14 | _Entity.__init__(self, -1, _SOBDCallback(self)) 15 | AutoIdentity.__init__(self, port == "auto") 16 | self._serial: _Serial = _Serial() 17 | self._serial.baudrate = baud_rate 18 | self._connection: SerialConnection | None = None 19 | self._serial.port = self.suggest_next_port() if port == "auto" else port 20 | self._password: str = password 21 | self._locked: bool = password != "" 22 | 23 | @_override 24 | def port(self) -> str: 25 | return self._serial.port 26 | 27 | @_override 28 | def initialize(self, *parent_tags: str) -> None: 29 | super().initialize(*parent_tags) 30 | self.start(True) 31 | 32 | @_override 33 | def update(self, data: str) -> None: 34 | if data.startswith("pwd="): 35 | if data[4:] != self._password: 36 | self.close() 37 | else: 38 | self._locked = False 39 | if self._locked: 40 | return 41 | if data.startswith("dbl="): 42 | _require_config().w_debug_level = data[4:].upper() 43 | else: 44 | self.write("\n".join(_L.history_messages()).encode()) 45 | 46 | @_override 47 | def check_identity(self, connection: SerialConnection) -> bool: 48 | connection.send(b"ic") 49 | return (msg := connection.receive()) and msg.startswith(self.tag().encode()) 50 | 51 | @_override 52 | def run(self) -> None: 53 | self._callback.on_initialize(self) 54 | self._connection = self.establish_connection(self._serial) 55 | self._callback.on_connect(self, self._connection) 56 | self._stage(self._connection) 57 | 58 | @_override 59 | def write(self, payload: bytes) -> None: 60 | if not self._connection: 61 | raise IOError("Target must be connected to perform this operation") 62 | self._connection.send(payload) 63 | 64 | @_override 65 | def close(self) -> None: 66 | if self._connection: 67 | self._connection.close() 68 | 69 | 70 | class _SOBDCallback(_Callback): 71 | def __init__(self, sobd: SOBD) -> None: 72 | super().__init__() 73 | self._sobd: SOBD = sobd 74 | 75 | @_override 76 | def on_connect(self, service: _Service, connection: _ConnectionBase) -> None: 77 | _L.debug(f"SOBD connected on {self._sobd.port()}") 78 | 79 | 80 | @_override 81 | def on_receive(self, service: _Service, msg: bytes) -> None: 82 | self.super(service=service, msg=msg) 83 | try: 84 | self._sobd.update(msg.decode()) 85 | except UnicodeDecodeError: 86 | _L.debug(f"Discarding this message: {msg}") 87 | -------------------------------------------------------------------------------- /leads_emulation/__init__.py: -------------------------------------------------------------------------------- 1 | from random import randint as _randint 2 | from time import time as _time 3 | from typing import override as _override 4 | 5 | from numpy import sin as _sin, pi as _pi 6 | 7 | from leads import Controller as _Controller, DataContainer as _DataContainer 8 | 9 | 10 | class _EmulatedController(_Controller): 11 | def __init__(self, minimum: int = 30, maximum: int = 40, skid_possibility: float = .1) -> None: 12 | super().__init__() 13 | self.minimum: int = minimum 14 | self.maximum: int = maximum 15 | self.skid_possibility: float = skid_possibility 16 | self._last_speed: float = 0 17 | self._last_time: float = _time() 18 | self._max_throttle: float = 0 19 | self._max_brake: float = 0 20 | 21 | def generate_rear_wheel_speed(self, front_wheel_speed: float) -> float: 22 | return front_wheel_speed if self.skid_possibility <= 0 else front_wheel_speed + int(_randint( 23 | -int(1 / self.skid_possibility), int(1 / self.skid_possibility)) * self.skid_possibility) 24 | 25 | def generate_forward_acceleration(self, speed: float) -> float: 26 | t = _time() 27 | try: 28 | return (speed - self._last_speed) / 3.6 / (t - self._last_time) 29 | finally: 30 | self._last_speed = speed 31 | self._last_time = t 32 | 33 | def generate_throttle(self, forward_acceleration: float) -> float: 34 | if forward_acceleration <= 0: 35 | return 0 36 | if forward_acceleration > self._max_throttle: 37 | self._max_throttle = forward_acceleration 38 | return forward_acceleration / self._max_throttle 39 | 40 | def generate_brake(self, forward_acceleration: float) -> float: 41 | if forward_acceleration >= 0: 42 | return 0 43 | if (forward_acceleration := -forward_acceleration) > self._max_brake: 44 | self._max_brake = forward_acceleration 45 | return forward_acceleration / self._max_brake 46 | 47 | 48 | class RandomController(_EmulatedController): 49 | @_override 50 | def read(self) -> _DataContainer: 51 | return _DataContainer(voltage=48.0, 52 | speed=(fws := _randint(self.minimum, self.maximum)), 53 | front_wheel_speed=fws, 54 | rear_wheel_speed=self.generate_rear_wheel_speed(fws), 55 | forward_acceleration=(fa := self.generate_forward_acceleration(fws)), 56 | gps_valid=True, 57 | gps_ground_speed=fws, 58 | latitude=_randint(4315, 4415) / 100, 59 | longitude=-_randint(7888, 7988) / 100, 60 | throttle=self.generate_throttle(fa), 61 | brake=self.generate_brake(fa)) 62 | 63 | 64 | class SinController(_EmulatedController): 65 | def __init__(self, minimum: int = 30, maximum: int = 40, skid_possibility: float = .1, 66 | acceleration: float = .01) -> None: 67 | super().__init__(minimum, maximum, skid_possibility) 68 | self.acceleration: float = acceleration 69 | self.magnitude: int = int((maximum - minimum) * .5) 70 | self.counter: float = 0 71 | 72 | @_override 73 | def read(self) -> _DataContainer: 74 | try: 75 | return _DataContainer(voltage=48.0, 76 | speed=(fws := (_sin(self.counter) * self.magnitude + self.magnitude)), 77 | front_wheel_speed=fws, 78 | rear_wheel_speed=self.generate_rear_wheel_speed(fws), 79 | forward_acceleration=(fa := self.generate_forward_acceleration(fws)), 80 | gps_valid=True, 81 | gps_ground_speed=fws, 82 | latitude=_randint(4315, 4415) / 100, 83 | longitude=-_randint(7888, 7988) / 100, 84 | throttle=self.generate_throttle(fa), 85 | brake=self.generate_brake(fa)) 86 | finally: 87 | self.counter = (self.counter + self.acceleration) % (2 * _pi) 88 | -------------------------------------------------------------------------------- /leads_emulation/replay.py: -------------------------------------------------------------------------------- 1 | from base64 import b64decode as _b64decode 2 | from io import BytesIO as _BytesIO 3 | from typing import override as _override, Iterator as _Iterator, Any as _Any, TypeVar as _TypeVar, \ 4 | Generic as _Generic, Literal as _Literal 5 | 6 | from PIL.Image import open as _open, Image as _Image 7 | from numpy import ndarray as _ndarray, array as _array 8 | 9 | from leads import Controller as _Controller, DataContainer as _DataContainer, get_controller as _get_controller, \ 10 | VisualDataContainer as _VisualDataContainer, Device as _Device 11 | from leads.data_persistence import CSVDataset as _CSVDataset 12 | from leads_video import Camera as _Camera 13 | 14 | T = _TypeVar("T", bound=_DataContainer) 15 | 16 | 17 | class ReplayController(_Controller, _Generic[T]): 18 | def __init__(self, dataset: _CSVDataset, data_container_constructor: type[T]) -> None: 19 | super().__init__() 20 | self._dataset: _CSVDataset = dataset 21 | self._constructor: type[T] = data_container_constructor 22 | self._iterator: _Iterator[dict[str, _Any]] = iter(dataset) 23 | self._current_data_container: T | None = None 24 | 25 | @_override 26 | def read(self) -> T: 27 | try: 28 | d = next(self._iterator) 29 | except StopIteration: 30 | return self._constructor() 31 | t = d.pop("t") 32 | dc = self._constructor(**d) 33 | setattr(dc, "_time_stamp", t) 34 | self._current_data_container = dc 35 | return dc 36 | 37 | def current_data_container(self) -> T | None: 38 | return self._current_data_container 39 | 40 | @_override 41 | def close(self) -> None: 42 | self._dataset.close() 43 | 44 | 45 | class ReplayCamera(_Camera): 46 | def __init__(self, channel: _Literal["front", "left", "right", "rear"], 47 | resolution: tuple[int, int] | None = None) -> None: 48 | super().__init__(-1, resolution) 49 | self._channel: _Literal["front", "left", "right", "rear"] = channel 50 | self._controller: ReplayController | None = None 51 | 52 | @_override 53 | def initialize(self, *parent_tags: str) -> None: 54 | _Device.initialize(self, *parent_tags) 55 | if not isinstance(controller := _get_controller(parent_tags[-1]), ReplayController): 56 | raise TypeError("Emulated cameras must be initialized with a replay controller") 57 | self._controller = controller 58 | 59 | @_override 60 | def read(self) -> _ndarray | None: 61 | return _array(self.read_pil()) 62 | 63 | @_override 64 | def read_pil(self) -> _Image | None: 65 | if not isinstance(dc := self._controller.current_data_container(), _VisualDataContainer): 66 | raise TypeError("Emulated cameras require visual data containers") 67 | return _open(_BytesIO(_b64decode(getattr(dc, f"{self._channel}_view_base64")))) 68 | 69 | @_override 70 | def close(self) -> None: 71 | pass 72 | -------------------------------------------------------------------------------- /leads_gpio/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.util import find_spec as _find_spec 2 | 3 | if not _find_spec("gpiozero"): 4 | raise ImportError("Please install `gpiozero` to run this module\n>>>pip install gpiozero") 5 | if not _find_spec("pynmea2"): 6 | raise ImportError("Please install `pynmea2` to run this module\n>>>pip install pynmea2") 7 | if not _find_spec("serial"): 8 | raise ImportError("Please install `pyserial` to run this module\n>>>pip install pyserial") 9 | 10 | from leads_gpio.button import * 11 | from leads_gpio.cpu_monitor import * 12 | from leads_gpio.gps_receiver import * 13 | from leads_gpio.led import * 14 | from leads_gpio.led_group import * 15 | -------------------------------------------------------------------------------- /leads_gpio/button.py: -------------------------------------------------------------------------------- 1 | from typing import override as _override 2 | 3 | from gpiozero import Button as _Button 4 | 5 | from leads import Device as _Device, CallbackChain as _CallbackChain 6 | 7 | 8 | class ButtonCallback(_CallbackChain): 9 | def on_pressed(self) -> None: ... 10 | 11 | def on_released(self) -> None: ... 12 | 13 | 14 | class Button(_Device): 15 | """ 16 | Listen to a button. 17 | Supports: 18 | - Any binary button 19 | """ 20 | 21 | def __init__(self, pin: int, callback: ButtonCallback = ButtonCallback()) -> None: 22 | super().__init__(pin) 23 | self._button: _Button = _Button(pin) 24 | self._callback: ButtonCallback = callback 25 | 26 | def _when_activated(self) -> None: 27 | self._callback.on_pressed() 28 | 29 | def _when_deactivated(self) -> None: 30 | self._callback.on_released() 31 | 32 | @_override 33 | def initialize(self, *parent_tags: str) -> None: 34 | super().initialize(*parent_tags) 35 | self._button.when_activated = self._when_activated 36 | self._button.when_deactivated = self._when_deactivated 37 | 38 | @_override 39 | def read(self) -> bool: 40 | return self._button.is_active 41 | 42 | @_override 43 | def write(self, callback: ButtonCallback) -> None: 44 | callback.bind_chain(self._callback) 45 | self._callback = callback 46 | -------------------------------------------------------------------------------- /leads_gpio/cpu_monitor.py: -------------------------------------------------------------------------------- 1 | from typing import override as _override 2 | 3 | from gpiozero import CPUTemperature as _CPUTemperature 4 | 5 | from leads import Device as _Device 6 | 7 | 8 | class CPUMonitor(_Device): 9 | def __init__(self) -> None: 10 | super().__init__() 11 | self._cpu_temp: _CPUTemperature = _CPUTemperature() 12 | 13 | @_override 14 | def read(self) -> dict[str, float]: 15 | return {"temp": self._cpu_temp.temperature} 16 | -------------------------------------------------------------------------------- /leads_gpio/gps_receiver.py: -------------------------------------------------------------------------------- 1 | from typing import override as _override, Literal as _Literal 2 | 3 | from pynmea2 import parse as _parse 4 | from pynmea2.types.talker import TalkerSentence as _TalkerSentence 5 | from serial import Serial as _Serial 6 | 7 | from leads import Device as _Device, SFT as _SFT 8 | from leads.comm import Entity as _Entity, Callback as _Callback, Service as _Service 9 | from leads_comm_serial import SerialConnection as _SerialConnection, AutoIdentity as _AutoIdentity, SerialConnection 10 | 11 | 12 | class NMEAGPSReceiver(_Device, _Entity, _AutoIdentity): 13 | """ 14 | Supports: 15 | - Any USB (serial) GPS receiver with NMEA 0183 output 16 | """ 17 | 18 | def __init__(self, port: str | _Literal["auto"], baud_rate: int = 9600) -> None: 19 | _Device.__init__(self, port) 20 | _Entity.__init__(self, -1, _NMEAGPSCallback(self)) 21 | _AutoIdentity.__init__(self, port == "auto", separator=b"\n") 22 | self._serial: _Serial = _Serial() 23 | self._serial.baudrate = baud_rate 24 | self._connection: _SerialConnection | None = None 25 | self._serial.port = self.suggest_next_port() if port == "auto" else port 26 | self._valid: bool = False 27 | self._ground_speed: float = 0 28 | self._latitude: float = 0 29 | self._longitude: float = 0 30 | self._quality: int = 0 31 | self._num_satellites: int = 0 32 | 33 | @_override 34 | def port(self) -> str: 35 | return self._serial.port 36 | 37 | @_override 38 | def initialize(self, *parent_tags: str) -> None: 39 | super().initialize(*parent_tags) 40 | self.start(True) 41 | 42 | @_override 43 | def check_identity(self, connection: SerialConnection) -> bool: 44 | try: 45 | _parse(connection.receive().decode()) 46 | return True 47 | except ValueError: 48 | return False 49 | 50 | @_override 51 | def update(self, data: _TalkerSentence) -> None: 52 | if hasattr(data, "latitude"): 53 | self._latitude = float(data.latitude) 54 | if hasattr(data, "longitude"): 55 | self._longitude = float(data.longitude) 56 | if hasattr(data, "is_valid"): 57 | if (v := data.is_valid) and not self._valid: 58 | _SFT.recover(self) 59 | elif not v and self._valid: 60 | _SFT.fail(self, "No fix") 61 | self._valid = v 62 | if NMEAGPSReceiver._has_field(data.fields, "spd_over_grnd", 6): 63 | self._ground_speed = float(ground_speed) * 1.852 if (ground_speed := data.data[6]) else 0 64 | if NMEAGPSReceiver._has_field(data.fields, "gps_qual", 5): 65 | self._quality = int(data.data[5]) 66 | if NMEAGPSReceiver._has_field(data.fields, "num_sats", 6): 67 | self._num_satellites = int(data.data[6]) 68 | 69 | @_override 70 | def read(self) -> tuple[bool, float, float, float, int, int]: 71 | """ 72 | :return: (validity, ground speed, latitude, longitude, quality, num of satellites) 73 | """ 74 | return self._valid, self._ground_speed, self._latitude, self._longitude, self._quality, self._num_satellites 75 | 76 | @_override 77 | def run(self) -> None: 78 | self._callback.on_initialize(self) 79 | self._connection = self.establish_connection(self._serial) 80 | self._callback.on_connect(self, self._connection) 81 | self._stage(self._connection) 82 | 83 | @_override 84 | def close(self) -> None: 85 | if self._connection: 86 | self._connection.close() 87 | 88 | @staticmethod 89 | def _has_field(fields: tuple[tuple[str, str, ...], ...], target_field: str, at: int) -> bool: 90 | return len(fields) > at and fields[at][1] == target_field 91 | 92 | 93 | class _NMEAGPSCallback(_Callback): 94 | def __init__(self, receiver: NMEAGPSReceiver) -> None: 95 | super().__init__() 96 | self._receiver: _Device = receiver 97 | 98 | @_override 99 | def on_connect(self, service: _Service, connection: _SerialConnection) -> None: 100 | _SFT.fail(self._receiver, "No fix") 101 | 102 | @_override 103 | def on_receive(self, service: _Service, msg: bytes) -> None: 104 | self.super(service=service, msg=msg) 105 | self._receiver.update(_parse(msg.decode())) 106 | 107 | @_override 108 | def on_fail(self, service: _Service, error: Exception) -> None: 109 | self.super(service=service, error=error) 110 | _SFT.fail(self._receiver, error) 111 | -------------------------------------------------------------------------------- /leads_gpio/led.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum as _IntEnum 2 | from typing import override as _override 3 | 4 | from gpiozero import LED as _LED 5 | 6 | from leads import Device as _Device 7 | 8 | 9 | class LEDCommand(_IntEnum): 10 | OFF: int = 0 11 | ON: int = 1 12 | BLINK: int = 2 13 | BLINK_ONCE: int = 3 14 | 15 | 16 | class LED(_Device): 17 | """ 18 | Control a LED digitally. 19 | Supports: 20 | - Any 2-pin LED 21 | """ 22 | def __init__(self, pin: int, blink_time_on: float = 1, blink_time_off: float = 1) -> None: 23 | super().__init__(pin) 24 | self._led: _LED = _LED(pin) 25 | self._blink_time_on: float = blink_time_on 26 | self._blink_time_off: float = blink_time_off 27 | 28 | @_override 29 | def read(self) -> bool: 30 | return self._led.is_active 31 | 32 | @_override 33 | def write(self, payload: LEDCommand) -> None: 34 | match payload: 35 | case LEDCommand.OFF: 36 | self._led.off() 37 | case LEDCommand.ON: 38 | self._led.on() 39 | case LEDCommand.BLINK: 40 | self._led.blink(self._blink_time_on, self._blink_time_off) 41 | case LEDCommand.BLINK_ONCE: 42 | self._led.blink(self._blink_time_on, self._blink_time_off, 1) 43 | -------------------------------------------------------------------------------- /leads_gpio/led_group.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta as _ABCMeta, abstractmethod as _abstractmethod 2 | from dataclasses import dataclass as _dataclass 3 | from threading import Thread as _Thread 4 | from time import sleep as _sleep 5 | from typing import override as _override 6 | 7 | from leads import Device as _Device 8 | from leads_gpio.led import LED, LEDCommand 9 | from leads_gpio.types import TransitionDirection as _TransitionDirection 10 | 11 | 12 | class LEDGroupAnimation(object, metaclass=_ABCMeta): 13 | @_abstractmethod 14 | def do(self, command: LEDCommand, *leds: LED) -> None: 15 | raise NotImplementedError 16 | 17 | 18 | class Entire(LEDGroupAnimation): 19 | @_override 20 | def do(self, command: LEDCommand, *leds: LED) -> None: 21 | for led in leds: 22 | led.write(command) 23 | 24 | 25 | class Transition(LEDGroupAnimation): 26 | def __init__(self, direction: _TransitionDirection, interval: int = 1000) -> None: 27 | self._direction: _TransitionDirection = direction 28 | self._interval: float = interval * .001 29 | 30 | def async_do(self, command: LEDCommand, *leds: LED) -> None: 31 | match self._direction: 32 | case "left2right": 33 | for led in leds: 34 | led.write(command) 35 | _sleep(self._interval) 36 | case "right2left": 37 | for led in leds[::-1]: 38 | led.write(command) 39 | _sleep(self._interval) 40 | 41 | @_override 42 | def do(self, command: LEDCommand, *leds: LED) -> None: 43 | _Thread(name="transition", target=self.async_do, args=(command, *leds), daemon=True).start() 44 | 45 | 46 | @_dataclass 47 | class LEDGroupCommand(object): 48 | command: LEDCommand 49 | animation: LEDGroupAnimation 50 | 51 | 52 | class LEDGroup(_Device): 53 | def __init__(self, *leds: LED) -> None: 54 | super().__init__() 55 | self._leds: tuple[LED, ...] = leds 56 | 57 | @_override 58 | def write(self, payload: LEDGroupCommand) -> None: 59 | payload.animation.do(payload.command, *self._leds) 60 | -------------------------------------------------------------------------------- /leads_gpio/types.py: -------------------------------------------------------------------------------- 1 | from typing import Literal as _Literal 2 | 3 | type TransitionDirection = _Literal["left2right", "right2left"] 4 | -------------------------------------------------------------------------------- /leads_gui/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.util import find_spec as _find_spec 2 | 3 | if not _find_spec("PIL"): 4 | raise ImportError("Please install `Pillow` to run this module\n>>>pip install Pillow") 5 | if not _find_spec("screeninfo"): 6 | raise ImportError("Please install `screeninfo` to run this module\n>>>pip install screeninfo") 7 | if not _find_spec("customtkinter"): 8 | raise ImportError("Please install `customtkinter` to run this module\n>>>pip install customtkinter") 9 | 10 | from os.path import abspath as _abspath 11 | from typing import Callable as _Callable, Any as _Any 12 | from customtkinter import set_default_color_theme as _set_default_color_theme 13 | 14 | from leads import LEADS as _LEADS, set_on_register_config as _set_on_register_config, \ 15 | get_controller as _get_controller, MAIN_CONTROLLER as _MAIN_CONTROLLER 16 | from leads.types import OnRegister as _OnRegister 17 | from leads_gui.config import * 18 | from leads_gui.prototype import * 19 | from leads_gui.icons import * 20 | from leads_gui.accelerometer import * 21 | from leads_gui.speedometer import * 22 | from leads_gui.typography import * 23 | from leads_gui.proxy import * 24 | from leads_gui.performance_checker import * 25 | from leads_gui.photo import * 26 | 27 | _set_default_color_theme(f"{_abspath(__file__)[:-11]}assets/leads-theme.json") 28 | 29 | 30 | def _on_register_config(chain: _OnRegister[Config]) -> _OnRegister[Config]: 31 | def _(cfg: Config) -> None: 32 | chain(cfg) 33 | if cfg.theme: 34 | _set_default_color_theme(cfg.theme) 35 | 36 | return _ 37 | 38 | 39 | _set_on_register_config(_on_register_config) 40 | 41 | 42 | def initialize(window: Pot, 43 | render: _Callable[[ContextManager], None], 44 | leads: _LEADS[_Any]) -> ContextManager: 45 | main_controller = _get_controller(_MAIN_CONTROLLER) 46 | ctx = ContextManager(window) 47 | render(ctx) 48 | 49 | def on_refresh(_) -> None: 50 | leads.push(main_controller.read()) 51 | leads.update() 52 | 53 | window.set_on_refresh(on_refresh) 54 | return ctx 55 | -------------------------------------------------------------------------------- /leads_gui/assets/icons/battery-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/battery-black.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/battery-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/battery-red.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/battery-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/battery-white.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/brake-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/brake-black.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/brake-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/brake-red.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/brake-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/brake-white.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/car-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/car-black.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/car-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/car-red.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/car-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/car-white.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/engine-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/engine-black.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/engine-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/engine-red.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/engine-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/engine-white.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/esc-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/esc-black.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/esc-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/esc-red.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/esc-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/esc-white.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/hazard-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/hazard-black.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/hazard-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/hazard-red.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/hazard-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/hazard-white.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/high-beam-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/high-beam-black.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/high-beam-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/high-beam-red.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/high-beam-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/high-beam-white.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/left-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/left-black.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/left-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/left-red.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/left-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/left-white.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/light-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/light-black.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/light-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/light-red.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/light-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/light-white.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/motor-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/motor-black.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/motor-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/motor-red.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/motor-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/motor-white.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/right-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/right-black.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/right-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/right-red.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/right-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/right-white.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/satellite-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/satellite-black.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/satellite-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/satellite-red.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/satellite-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/satellite-white.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/speed-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/speed-black.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/speed-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/speed-red.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/speed-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/speed-white.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/stopwatch-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/stopwatch-black.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/stopwatch-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/stopwatch-red.png -------------------------------------------------------------------------------- /leads_gui/assets/icons/stopwatch-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/icons/stopwatch-white.png -------------------------------------------------------------------------------- /leads_gui/assets/leads-theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "CTk": { 3 | "fg_color": [ 4 | "gray95", 5 | "gray10" 6 | ] 7 | }, 8 | "CTkToplevel": { 9 | "fg_color": [ 10 | "gray95", 11 | "gray10" 12 | ] 13 | }, 14 | "CTkFrame": { 15 | "corner_radius": 4, 16 | "border_width": 0, 17 | "border_color": "gray50", 18 | "fg_color": "transparent" 19 | }, 20 | "CTkLabel": { 21 | "corner_radius": 0, 22 | "fg_color": "transparent", 23 | "text_color": [ 24 | "black", 25 | "white" 26 | ] 27 | }, 28 | "CTkButton": { 29 | "corner_radius": 4, 30 | "border_width": 0, 31 | "border_color": "gray50", 32 | "fg_color": [ 33 | "gray85", 34 | "gray20" 35 | ], 36 | "hover_color": [ 37 | "gray80", 38 | "gray25" 39 | ], 40 | "text_color": [ 41 | "black", 42 | "white" 43 | ], 44 | "text_color_disabled": [ 45 | "gray74", 46 | "gray60" 47 | ] 48 | }, 49 | "CTkSegmentedButton": { 50 | "corner_radius": 4, 51 | "border_width": 0, 52 | "fg_color": [ 53 | "gray85", 54 | "gray20" 55 | ], 56 | "selected_color": "green", 57 | "selected_hover_color": "green", 58 | "unselected_color": "transparent", 59 | "unselected_hover_color": [ 60 | "gray80", 61 | "gray25" 62 | ], 63 | "text_color": [ 64 | "black", 65 | "white" 66 | ], 67 | "text_color_disabled": [ 68 | "gray74", 69 | "gray60" 70 | ] 71 | }, 72 | "CTkFont": { 73 | "macOS": { 74 | "family": "Arial", 75 | "size": 14, 76 | "weight": "normal" 77 | }, 78 | "Windows": { 79 | "family": "Arial", 80 | "size": 14, 81 | "weight": "normal" 82 | }, 83 | "Linux": { 84 | "family": "Arial", 85 | "size": 14, 86 | "weight": "normal" 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /leads_gui/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_gui/assets/logo.png -------------------------------------------------------------------------------- /leads_gui/config.py: -------------------------------------------------------------------------------- 1 | from typing import Any as _Any, Literal as _Literal 2 | 3 | from leads import ConfigTemplate as _ConfigTemplate, L as _L, get_config as _get_config 4 | 5 | 6 | class Config(_ConfigTemplate): 7 | def __init__(self, base: dict[str, _Any]) -> None: 8 | self.width: int = 720 9 | self.height: int = 480 10 | self.fullscreen: bool = False 11 | self.no_title_bar: bool = False 12 | self.theme: str = "" 13 | self.theme_mode: _Literal["system", "light", "dark"] = "system" 14 | self.manual_mode: bool = False 15 | self.refresh_rate: int = 30 16 | self.m_ratio: float = .7 17 | self.num_external_screens: int = 0 18 | self.font_size_small: int = 14 19 | self.font_size_medium: int = 28 20 | self.font_size_large: int = 42 21 | self.font_size_x_large: int = 56 22 | super().__init__(base) 23 | 24 | def magnify_font_sizes(self, factor: float) -> None: 25 | """ 26 | Magnify font sizes by the factor. 27 | :param factor: the factor 28 | """ 29 | if _get_config(): 30 | raise RuntimeError("This method must be called before the config is registered") 31 | self._frozen = False 32 | self.font_size_small = int(self.font_size_small * factor) 33 | self.font_size_medium = int(self.font_size_medium * factor) 34 | self.font_size_large = int(self.font_size_large * factor) 35 | self.font_size_x_large = int(self.font_size_x_large * factor) 36 | self._frozen = True 37 | _L.debug(f"Font sizes magnified by {factor}") 38 | 39 | def auto_magnify_font_sizes(self) -> None: 40 | """ 41 | Automatically magnify font sizes to match the original proportion. 42 | """ 43 | self.magnify_font_sizes(self.width / Config({}).width) 44 | -------------------------------------------------------------------------------- /leads_gui/icons.py: -------------------------------------------------------------------------------- 1 | from enum import StrEnum as _StrEnum 2 | from typing import Callable as _Callable, override as _override 3 | 4 | from PIL import Image as _Image 5 | from customtkinter import CTkImage as _CTkImage 6 | 7 | from leads import require_config as _require_config 8 | from leads_gui.system import _ASSETS_PATH 9 | 10 | _ICONS_PATH: str = f"{_ASSETS_PATH}/icons" 11 | 12 | 13 | class Color(_StrEnum): 14 | BLACK: str = "black" 15 | WHITE: str = "white" 16 | RED: str = "red" 17 | 18 | 19 | class _Icon(_Callable[[int, Color | None], _CTkImage]): 20 | def __init__(self, name: str) -> None: 21 | self._name: str = name 22 | 23 | def load_source(self, color: Color) -> _Image: 24 | return _Image.open(f"{_ICONS_PATH}/{self._name}-{color}.png") 25 | 26 | def __call__(self, size: int | None = None, color: Color | None = None) -> _CTkImage: 27 | if size is None: 28 | size = _require_config().font_size_medium 29 | return _CTkImage(self.load_source(color if color else Color.BLACK), 30 | None if color else self.load_source(Color.WHITE), 31 | size=(size, size)) 32 | 33 | 34 | class _Logo(_Icon): 35 | def __init__(self) -> None: 36 | super().__init__("logo") 37 | 38 | @_override 39 | def load_source(self, color: Color) -> _Image: 40 | return _Image.open(f"{_ASSETS_PATH}/logo.png") 41 | 42 | @_override 43 | def __call__(self, size: int | None = None, color: Color | None = None) -> _CTkImage: 44 | if size is None: 45 | size = _require_config().font_size_medium 46 | return _CTkImage(self.load_source(Color.BLACK), size=(size, size)) 47 | 48 | 49 | Logo: _Logo = _Logo() 50 | 51 | Battery: _Icon = _Icon("battery") 52 | Brake: _Icon = _Icon("brake") 53 | Car: _Icon = _Icon("car") 54 | ESC: _Icon = _Icon("esc") 55 | Engine: _Icon = _Icon("engine") 56 | Hazard: _Icon = _Icon("hazard") 57 | HighBeam: _Icon = _Icon("high-beam") 58 | Left: _Icon = _Icon("left") 59 | Light: _Icon = _Icon("light") 60 | Motor: _Icon = _Icon("motor") 61 | Right: _Icon = _Icon("right") 62 | Satellite: _Icon = _Icon("satellite") 63 | Speed: _Icon = _Icon("speed") 64 | Stopwatch: _Icon = _Icon("stopwatch") 65 | -------------------------------------------------------------------------------- /leads_gui/performance_checker.py: -------------------------------------------------------------------------------- 1 | from collections import deque as _deque 2 | from time import time as _time 3 | 4 | from numpy import average as _average, poly1d as _poly1d, polyfit as _polyfit 5 | 6 | from leads import require_config as _require_config 7 | 8 | 9 | class PerformanceChecker(object): 10 | def __init__(self) -> None: 11 | self._refresh_rate: int = _require_config().refresh_rate 12 | self._interval: float = 1 / self._refresh_rate 13 | self._time_seq: _deque[float] = _deque((_time(),), maxlen=self._refresh_rate * 10) 14 | self._delay_seq: _deque[float] = _deque((.001,), maxlen=self._refresh_rate * 10) 15 | self._net_delay_seq: _deque[float] = _deque((.001,), maxlen=self._refresh_rate * 10) 16 | self._model: _poly1d | None = None 17 | self._last_frame: float = _time() 18 | 19 | def frame_rate(self) -> float: 20 | return 1 / _average(self._delay_seq) 21 | 22 | def net_delay(self) -> float: 23 | return float(_average(self._net_delay_seq)) 24 | 25 | def record_frame(self, last_interval: float) -> None: 26 | # add a small constant to avoid zero division 27 | self._time_seq.append(t := _time()) 28 | self._delay_seq.append(delay := 1e-10 + t - self._last_frame) 29 | self._net_delay_seq.append(delay - last_interval) 30 | self._model = _poly1d(_polyfit(self._time_seq, self._net_delay_seq, 5)) 31 | self._last_frame = t 32 | 33 | def next_interval(self) -> float: 34 | return self._interval - max(min(self._model(_time()) if len(self._net_delay_seq) > self._refresh_rate else 0, 35 | self._interval - .001), 0) 36 | -------------------------------------------------------------------------------- /leads_gui/photo.py: -------------------------------------------------------------------------------- 1 | from base64 import b64decode as _b64decode 2 | from io import BytesIO as _BytesIO 3 | from tkinter import Misc as _Misc, Event as _Event 4 | from typing import Callable as _Callable, override as _override 5 | 6 | from PIL.Image import Image as _Image, open as _open 7 | from PIL.ImageTk import PhotoImage as _PhotoImage 8 | from customtkinter import Variable as _Variable, StringVar as _StringVar 9 | 10 | from leads_gui.prototype import CanvasBased, VariableControlled 11 | from leads_gui.types import Color as _Color 12 | 13 | 14 | class ImageVariable(_Variable): 15 | def __init__(self, master: _Misc, image: _Image | None, name: str | None = None) -> None: 16 | super().__init__(master, False, name) 17 | self._image: _Image | None = image 18 | 19 | @_override 20 | def set(self, value: _Image | None) -> None: 21 | super().set(not super().get()) 22 | self._image = value 23 | 24 | @_override 25 | def get(self) -> _Image | None: 26 | return self._image 27 | 28 | 29 | class Photo(CanvasBased, VariableControlled): 30 | def __init__(self, 31 | master: _Misc, 32 | theme_key: str = "CTkLabel", 33 | width: float = 0, 34 | height: float = 0, 35 | variable: _StringVar | ImageVariable | None = None, 36 | fg_color: _Color | None = None, 37 | hover_color: _Color | None = None, 38 | bg_color: _Color | None = None, 39 | corner_radius: float | None = None, 40 | clickable: bool = False, 41 | command: _Callable[[_Event], None] = lambda _: None) -> None: 42 | CanvasBased.__init__(self, master, theme_key, width, height, fg_color, hover_color, bg_color, corner_radius, 43 | clickable, command) 44 | VariableControlled.__init__(self, variable if variable else _StringVar(master)) 45 | self.attach(self.partially_render) 46 | self._image: _PhotoImage | None = None 47 | 48 | @_override 49 | def dynamic_renderer(self, canvas: CanvasBased) -> None: 50 | canvas.clear("d") 51 | w, h, hc, vc, limit = canvas.meta() 52 | if image := self._variable.get(): 53 | if isinstance(image, str): 54 | image = _open(_BytesIO(_b64decode(image))) 55 | self._image = _PhotoImage(image.resize((int(w), int(h)))) 56 | canvas.collect("d0", canvas.create_image(hc, vc, image=self._image)) 57 | 58 | @_override 59 | def raw_renderer(self, canvas: CanvasBased) -> None: 60 | canvas.clear() 61 | canvas.draw_fg(self._fg_color, self._hover_color, self._corner_radius) 62 | self.dynamic_renderer(canvas) 63 | -------------------------------------------------------------------------------- /leads_gui/proxy.py: -------------------------------------------------------------------------------- 1 | from tkinter import Misc as _Misc 2 | from typing import override as _override 3 | 4 | from leads_gui.prototype import CanvasBased, VariableControlled 5 | from leads_gui.types import Color as _Color 6 | 7 | 8 | class ProxyCanvas(CanvasBased): 9 | def __init__(self, 10 | master: _Misc, 11 | theme_key: str, 12 | *canvases: CanvasBased, 13 | mode: int = 0, 14 | width: float = 0, 15 | height: float = 0, 16 | fg_color: _Color | None = None, 17 | hover_color: _Color | None = None, 18 | bg_color: _Color | None = None, 19 | corner_radius: float | None = None) -> None: 20 | super().__init__(master, theme_key, width, height, fg_color, hover_color, bg_color, corner_radius, True, 21 | lambda _: self.next_mode()) 22 | for canvas in canvases: 23 | if isinstance(canvas, VariableControlled): 24 | canvas.detach() 25 | self._canvases: tuple[CanvasBased, ...] = canvases 26 | self._mode: int = mode 27 | self._attach() 28 | 29 | def _attach(self) -> None: 30 | if isinstance(canvas := self._canvases[self._mode], VariableControlled): 31 | canvas.attach(self.partially_render) 32 | 33 | def next_mode(self) -> None: 34 | self.mode((self._mode + 1) % len(self._canvases)) 35 | 36 | def mode(self, mode: int | None = None) -> int | None: 37 | if mode is None: 38 | return self._mode 39 | if isinstance(canvas := self._canvases[self._mode], VariableControlled): 40 | canvas.detach() 41 | self._mode = mode 42 | self.render() 43 | self._attach() 44 | 45 | @_override 46 | def dynamic_renderer(self, canvas: CanvasBased) -> None: 47 | self._canvases[self._mode].dynamic_renderer(canvas) 48 | 49 | @_override 50 | def raw_renderer(self, canvas: CanvasBased) -> None: 51 | self._canvases[self._mode].raw_renderer(canvas) 52 | -------------------------------------------------------------------------------- /leads_gui/system.py: -------------------------------------------------------------------------------- 1 | from os.path import abspath as _abspath 2 | from platform import system as _system 3 | 4 | 5 | def get_system_kernel() -> str: 6 | return _system().lower() 7 | 8 | 9 | _ASSETS_PATH: str = f"{_abspath(__file__)[:-9]}assets" 10 | -------------------------------------------------------------------------------- /leads_gui/types.py: -------------------------------------------------------------------------------- 1 | from tkinter import Widget as _Widget 2 | 3 | from customtkinter import CTkBaseClass as _CTkBaseClass 4 | 5 | type Widget = _Widget | _CTkBaseClass 6 | type Font = tuple[str, int] 7 | type Color = str | tuple[str, str] 8 | -------------------------------------------------------------------------------- /leads_gui/typography.py: -------------------------------------------------------------------------------- 1 | from tkinter import Misc as _Misc, Event as _Event 2 | from typing import Callable as _Callable, override as _override 3 | 4 | from customtkinter import StringVar as _StringVar 5 | 6 | from leads_gui.prototype import CanvasBased, TextBased, VariableControlled 7 | from leads_gui.types import Font as _Font, Color as _Color 8 | 9 | 10 | class Typography(TextBased, VariableControlled): 11 | def __init__(self, 12 | master: _Misc, 13 | theme_key: str = "CTkLabel", 14 | width: float = 0, 15 | height: float = 0, 16 | variable: _StringVar | None = None, 17 | font: _Font | None = None, 18 | text_color: _Color | None = None, 19 | fg_color: _Color | None = None, 20 | hover_color: _Color | None = None, 21 | bg_color: _Color | None = None, 22 | corner_radius: float | None = None, 23 | clickable: bool = False, 24 | command: _Callable[[_Event], None] = lambda _: None) -> None: 25 | TextBased.__init__(self, master, theme_key, width, height, font, text_color, fg_color, hover_color, bg_color, 26 | corner_radius, clickable, command) 27 | VariableControlled.__init__(self, variable if variable else _StringVar(master)) 28 | self.attach(self.partially_render) 29 | 30 | @_override 31 | def dynamic_renderer(self, canvas: CanvasBased) -> None: 32 | canvas.clear("d") 33 | v = self._variable.get() 34 | w, h, hc, vc, limit = canvas.meta() 35 | font = self._font 36 | if (target_font_size := h - 28) < font[1]: 37 | font = (font[0], target_font_size) 38 | canvas.collect("d0", canvas.create_text(w * .5, h * .5, width=w, text=v, justify="center", 39 | fill=self._text_color, font=font)) 40 | 41 | @_override 42 | def raw_renderer(self, canvas: CanvasBased) -> None: 43 | canvas.clear() 44 | canvas.draw_fg(self._fg_color, self._hover_color, self._corner_radius) 45 | self.dynamic_renderer(canvas) 46 | -------------------------------------------------------------------------------- /leads_vec/__entry__.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser as _ArgumentParser, BooleanOptionalAction as _BooleanOptionalAction 2 | from importlib.metadata import version as _package_version, PackageNotFoundError as _PackageNotFoundError 3 | from os import getlogin as _get_login 4 | from os.path import abspath as _abspath 5 | from sys import exit as _exit, version as _version 6 | from warnings import filterwarnings as _filterwarnings 7 | 8 | from leads import L as _L, register_controller as _register_controller, Controller as _Controller, \ 9 | MAIN_CONTROLLER as _MAIN_CONTROLLER 10 | from leads_gui.system import get_system_kernel as _get_system_kernel 11 | from leads_vec.run import run 12 | 13 | MODULE_PATH = _abspath(__file__)[:-13] 14 | 15 | 16 | def parse_path(path: str | None) -> str | None: 17 | return path.replace(":INTERNAL", MODULE_PATH) if path else None 18 | 19 | 20 | def __entry__() -> None: 21 | _filterwarnings("ignore") 22 | 23 | parser = _ArgumentParser(prog="LEADS VeC", description="Lightweight Embedded Assisted Driving System VeC", 24 | epilog="Project Neura: https://projectneura.org\n" 25 | "GitHub: https://github.com/ProjectNeura/LEADS") 26 | parser.add_argument("action", choices=("info", "replay", "benchmark", "run")) 27 | parser.add_argument("-c", "--config", default=None, help="specify a configuration file") 28 | parser.add_argument("-d", "--devices", default=f"{MODULE_PATH}/devices.py", help="specify a devices module") 29 | parser.add_argument("-m", "--main", default=f"{MODULE_PATH}/cli.py", help="specify a main module") 30 | parser.add_argument("-r", "--register", choices=("systemd", "config", "reverse_proxy", "splash_screen"), 31 | default=None, help="register a service") 32 | parser.add_argument("-mfs", "--magnify-font-sizes", type=float, default=1, help="magnify font sizes by a factor") 33 | parser.add_argument("--emu", action=_BooleanOptionalAction, default=False, help="use emulator") 34 | parser.add_argument("--auto-mfs", action=_BooleanOptionalAction, default=False, 35 | help="automatically magnify font sizes to match the original proportion") 36 | parser.add_argument("--ignore-import-error", action=_BooleanOptionalAction, default=False, 37 | help="ignore `ImportError`") 38 | args = parser.parse_args() 39 | if args.action == "info": 40 | from leads_vec.__version__ import __version__ 41 | from ._bootloader import frpc_exists as _frpc_exists 42 | 43 | leads_version = "Unknown" 44 | try: 45 | leads_version = _package_version("leads") 46 | except _PackageNotFoundError: 47 | _L.warn("Failed to retrieve package version (did you install through pip?)") 48 | _L.info(f"LEADS VeC", 49 | f"System Kernel: {_get_system_kernel().upper()}", 50 | f"Python Version: {_version}", 51 | f"User: {_get_login()}", 52 | f"`frpc` Available: {_frpc_exists()}", 53 | f"Module Path: {MODULE_PATH}", 54 | f"LEADS Version: {leads_version}", 55 | f"LEADS VeC Version: {__version__}", 56 | sep="\n") 57 | else: 58 | match args.action: 59 | case "replay": 60 | args.devices = f"{MODULE_PATH}/replay.py" 61 | args.emu = False 62 | _L.debug("Replay mode enabled") 63 | case "benchmark": 64 | _register_controller(_MAIN_CONTROLLER, _Controller()) 65 | args.devices = f"{MODULE_PATH}/benchmark.py" 66 | args.main = f"{MODULE_PATH}/benchmark.py" 67 | _L.debug("Benchmark mode enabled") 68 | _exit(run(parse_path(args.config), parse_path(args.devices), parse_path(args.main), args.register, 69 | args.magnify_font_sizes, args.emu, args.auto_mfs, args.ignore_import_error)) 70 | _exit() 71 | -------------------------------------------------------------------------------- /leads_vec/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.util import find_spec as _find_spec 2 | 3 | if not _find_spec("pynput"): 4 | raise ImportError("Please install `pynput` to run this module\n>>>pip install pynput") 5 | 6 | from leads_vec.run import * 7 | from leads_vec.__entry__ import __entry__ 8 | from leads_vec.config import * 9 | from leads_vec.utils import * 10 | -------------------------------------------------------------------------------- /leads_vec/__main__.py: -------------------------------------------------------------------------------- 1 | from leads_vec.__entry__ import __entry__ 2 | 3 | 4 | if __name__ == "__main__": 5 | __entry__() 6 | -------------------------------------------------------------------------------- /leads_vec/__version__.py: -------------------------------------------------------------------------------- 1 | __version__: str = "Hamilton" 2 | -------------------------------------------------------------------------------- /leads_vec/_bootloader/__init__.py: -------------------------------------------------------------------------------- 1 | from leads_vec._bootloader.frp import * 2 | from leads_vec._bootloader.splash import * 3 | from leads_vec._bootloader.systemd import * 4 | -------------------------------------------------------------------------------- /leads_vec/_bootloader/bgrt-fallback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_vec/_bootloader/bgrt-fallback.png -------------------------------------------------------------------------------- /leads_vec/_bootloader/frp.py: -------------------------------------------------------------------------------- 1 | from os.path import exists as _exists 2 | from subprocess import run as _run, PIPE as _PIPE 3 | from threading import Thread as _Thread 4 | 5 | from leads import L as _L 6 | from leads_gui.system import get_system_kernel as _get_system_kernel 7 | 8 | 9 | def auto_path() -> str: 10 | return "%AppData%/frp" if _get_system_kernel() == "windows" else "/usr/local/frp" 11 | 12 | 13 | def frpc_exists() -> bool: 14 | return _exists(f"{(path := auto_path())}/frpc") and _exists(f"{path}/frpc.toml") 15 | 16 | 17 | def start_frpc() -> None: 18 | if not frpc_exists(): 19 | raise FileNotFoundError("`frpc` not found") 20 | 21 | def wrapper() -> None: 22 | r = _run((f"{(path := auto_path())}/frpc", "-c", f"{path}/frpc.toml"), stdout=_PIPE, stderr=_PIPE) 23 | _L.error("`frpc` exits prematurely") 24 | _L.debug(f"Console output:\n{r.stdout.decode()}") 25 | 26 | _Thread(name="frpc", target=wrapper, daemon=True).start() 27 | -------------------------------------------------------------------------------- /leads_vec/_bootloader/leads-vec.service.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if test ! -r "/usr/local/leads/config.json" 4 | then 5 | printf "Error: Config file does not exist or not readable\n" >&2 6 | exit 1 7 | fi 8 | # adjust the arguments according to your needs 9 | leads-vec -c /usr/local/leads/config.json run -------------------------------------------------------------------------------- /leads_vec/_bootloader/splash.py: -------------------------------------------------------------------------------- 1 | from os.path import abspath as _abspath 2 | from shutil import copyfile as _copyfile, move as _move 3 | 4 | from leads_gui.system import get_system_kernel as _get_system_kernel 5 | 6 | 7 | def register_splash_screen() -> None: 8 | if _get_system_kernel() != "linux": 9 | raise SystemError("Unsupported operating system") 10 | _move("/usr/share/plymouth/themes/spinner/bgrt-fallback.png", 11 | "/usr/share/plymouth/themes/spinner/bgrt-fallback-backup.png") 12 | _copyfile(f"{_abspath(__file__)[:-9]}bgrt-fallback.png", "/usr/share/plymouth/themes/spinner/bgrt-fallback.png") 13 | _move("/usr/share/plymouth/themes/spinner/watermark.png", "/usr/share/plymouth/themes/spinner/watermark-backup.png") 14 | _copyfile(f"{_abspath(__file__)[:-9]}watermark.png", "/usr/share/plymouth/themes/spinner/watermark.png") 15 | 16 | 17 | def register_lock_screen() -> None: 18 | if _get_system_kernel() != "linux": 19 | raise SystemError("Unsupported operating system") 20 | _move("/usr/share/plymouth/ubuntu-logo.png", "/usr/share/plymouth/ubuntu-logo-backup.png") 21 | _copyfile(f"{_abspath(__file__)[:-9]}watermark.png", "/usr/share/plymouth/ubuntu-logo.png") 22 | -------------------------------------------------------------------------------- /leads_vec/_bootloader/systemd.py: -------------------------------------------------------------------------------- 1 | from os import chmod as _chmod, getlogin as _get_login, makedirs as _mkdirs 2 | from os.path import abspath as _abspath, exists as _exists 3 | from subprocess import run as _run 4 | 5 | from leads import L as _L 6 | from leads_gui.system import get_system_kernel as _get_system_kernel 7 | from leads_vec.config import Config 8 | 9 | 10 | def register_leads_vec() -> None: 11 | if _get_system_kernel() != "linux": 12 | raise SystemError("Unsupported operating system") 13 | if not _exists("/usr/local/leads/config.json"): 14 | _L.debug("Config file not found. Creating \"/usr/local/leads/config.json\"...") 15 | if not _exists("/usr/local/leads"): 16 | _mkdirs("/usr/local/leads") 17 | with open("/usr/local/leads/config.json", "w") as f: 18 | f.write(str(Config({}))) 19 | _chmod("/usr/local/leads/config.json", 0o644) 20 | _chmod(script := f"{_abspath(__file__)[:-10]}leads-vec.service.sh", 0o755) 21 | if not _exists(user_systemd := f"/home/{(username := _get_login())}/.config/systemd/user"): 22 | _L.debug(f"User Systemd not found. Creating \"{user_systemd}\"...") 23 | _mkdirs(user_systemd) 24 | with open(f"{user_systemd}/leads-vec.service", "w") as f: 25 | f.write( 26 | "[Unit]\n" 27 | "Description=LEADS VeC\n" 28 | "After=graphical-session.target\n" 29 | "[Service]\n" 30 | "Type=simple\n" 31 | f"ExecStart=/bin/bash {script}\n" 32 | f"Restart=always\n" 33 | f"RestartSec=1s\n" 34 | "[Install]\n" 35 | "WantedBy=default.target" 36 | ) 37 | if not _exists(user_systemd_wants := f"{user_systemd}/default.target.wants"): 38 | _L.debug(f"User Systemd broken. Creating \"{user_systemd_wants}\"...") 39 | _mkdirs(user_systemd_wants) 40 | _chmod(user_systemd_wants, 0o777) 41 | _run(("usermod", "-aG", "dialout", username)) 42 | -------------------------------------------------------------------------------- /leads_vec/_bootloader/watermark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ProjectNeura/LEADS/95e6a1547292248fb8e19f2aa4d0b054161a53bc/leads_vec/_bootloader/watermark.png -------------------------------------------------------------------------------- /leads_vec/benchmark.py: -------------------------------------------------------------------------------- 1 | from base64 import b64encode 2 | from io import BytesIO 3 | from time import time 4 | from typing import Callable 5 | 6 | from PIL.Image import open 7 | from customtkinter import CTkLabel, DoubleVar 8 | from cv2 import VideoCapture, imencode, IMWRITE_JPEG_QUALITY, CAP_PROP_FPS 9 | 10 | from leads import L, require_config 11 | from leads_gui import RuntimeData, Pot, ContextManager, Speedometer 12 | 13 | 14 | def video_tester(container: Callable[[], None]) -> float: 15 | start = time() 16 | sum_delay = 0 17 | i = 0 18 | while True: 19 | if (t := time()) - start > 10: 20 | return sum_delay / i 21 | container() 22 | sum_delay += time() - t 23 | i += 1 24 | 25 | 26 | def video_test() -> dict[str, float]: 27 | r = {} 28 | vc = VideoCapture(require_config().get("benchmark_camera_port", 0)) 29 | r["camera fps"] = vc.get(CAP_PROP_FPS) 30 | if not vc.isOpened(): 31 | L.error("No camera available") 32 | return r 33 | 34 | def test1() -> None: 35 | vc.read() 36 | 37 | def test2() -> None: 38 | _, frame = vc.read() 39 | imencode(".jpg", frame, (IMWRITE_JPEG_QUALITY, 90))[1].tobytes() 40 | 41 | def test3() -> None: 42 | _, frame = vc.read() 43 | im = imencode(".jpg", frame, (IMWRITE_JPEG_QUALITY, 90))[1].tobytes() 44 | b64encode(im) 45 | 46 | def test4() -> None: 47 | _, frame = vc.read() 48 | im = imencode(".jpg", frame, (IMWRITE_JPEG_QUALITY, 90))[1].tobytes() 49 | b64encode(im) 50 | open(BytesIO(im)) 51 | 52 | r["video capture"] = 1 / video_tester(test1) 53 | r["video capture + encoding"] = 1 / video_tester(test2) 54 | r["video capture + Base64 encoding"] = 1 / video_tester(test3) 55 | r["video capture + PIL"] = 1 / video_tester(test4) 56 | return r 57 | 58 | 59 | class Callbacks(object): 60 | def __init__(self) -> None: 61 | self.t: float = time() 62 | self.speed: DoubleVar | None = None 63 | 64 | def on_refresh(self, window: Pot) -> None: 65 | self.speed.set((d := time() - self.t) * 20) 66 | if d > 10: 67 | window.kill() 68 | 69 | 70 | def main() -> int: 71 | report = {} 72 | L.info("GUI test starting, this takes about 10 seconds") 73 | rd = RuntimeData() 74 | callbacks = Callbacks() 75 | w = Pot(800, 256, 30, rd, callbacks.on_refresh, "Benchmark", no_title_bar=False) 76 | callbacks.speed = DoubleVar(w.root()) 77 | uim = ContextManager(w) 78 | uim.layout([[CTkLabel(w.root(), text="Do NOT close the window", height=240), 79 | Speedometer(w.root(), height=240, variable=callbacks.speed)]]) 80 | uim.show() 81 | L.info("GUI test complete") 82 | L.info("Video test starting, this takes about 40 seconds") 83 | report["gui"] = w.frame_rate() 84 | report.update(video_test()) 85 | L.info("Video test complete") 86 | for k, v in report.items(): 87 | L.info(f"{k}: {v:.3f}") 88 | camera_fps = report.pop("camera fps") 89 | baseline = {"gui": 30, "video capture": camera_fps, "video capture + encoding": camera_fps, 90 | "video capture + Base64 encoding": camera_fps, "video capture + PIL": camera_fps} 91 | score = 0 92 | for k, v in report.items(): 93 | score += v / baseline[k] 94 | L.info(f"Score: {100 * score / len(report):.2f}%") 95 | return 0 96 | 97 | 98 | _: None = None 99 | -------------------------------------------------------------------------------- /leads_vec/config.py: -------------------------------------------------------------------------------- 1 | from typing import Any as _Any 2 | 3 | from leads_gui import Config as _Config 4 | 5 | 6 | class Config(_Config): 7 | def __init__(self, base: dict[str, _Any]) -> None: 8 | self.comm_port: int = 16900 9 | self.comm_stream: bool = False 10 | self.comm_stream_port: int = 16901 11 | self.data_dir: str = "data" 12 | self.use_ltm: bool = False 13 | super().__init__(base) 14 | -------------------------------------------------------------------------------- /leads_vec/devices_visual.py: -------------------------------------------------------------------------------- 1 | from typing import override 2 | 3 | from leads import device, MAIN_CONTROLLER, mark_device, FRONT_VIEW_CAMERA, LEFT_VIEW_CAMERA, RIGHT_VIEW_CAMERA, \ 4 | REAR_VIEW_CAMERA, require_config 5 | from leads_vec.config import Config 6 | from leads_video import LightweightBase64Camera 7 | 8 | import_error: ImportError | None = None 9 | try: 10 | from leads_vec.devices import _ 11 | except ImportError as e: 12 | import_error = e 13 | 14 | config: Config = require_config() 15 | CAMERA_RESOLUTION: tuple[int, int] | None = config.get("camera_resolution") 16 | QUALITY: int = config.get("camera_quality", 25) 17 | CAMERA_TAGS: list[str] = [] 18 | CAMERA_ARGS: list[tuple[int, tuple[int, int] | None, int]] = [] 19 | if (port := config.get("front_view_camera_port")) is not None: 20 | CAMERA_TAGS.append(FRONT_VIEW_CAMERA) 21 | CAMERA_ARGS.append((port, CAMERA_RESOLUTION, QUALITY)) 22 | if (port := config.get("left_view_camera_port")) is not None: 23 | CAMERA_TAGS.append(LEFT_VIEW_CAMERA) 24 | CAMERA_ARGS.append((port, CAMERA_RESOLUTION, QUALITY)) 25 | if (port := config.get("right_view_camera_port")) is not None: 26 | CAMERA_TAGS.append(RIGHT_VIEW_CAMERA) 27 | CAMERA_ARGS.append((port, CAMERA_RESOLUTION, QUALITY)) 28 | if (port := config.get("rear_view_camera_port")) is not None: 29 | CAMERA_TAGS.append(REAR_VIEW_CAMERA) 30 | CAMERA_ARGS.append((port, CAMERA_RESOLUTION, QUALITY)) 31 | 32 | 33 | @device(CAMERA_TAGS, MAIN_CONTROLLER, CAMERA_ARGS) 34 | class Cameras(LightweightBase64Camera): 35 | @override 36 | def initialize(self, *parent_tags: str) -> None: 37 | mark_device(self, "Jarvis") 38 | super().initialize(*parent_tags) 39 | 40 | 41 | if import_error: 42 | raise import_error 43 | -------------------------------------------------------------------------------- /leads_vec/replay.py: -------------------------------------------------------------------------------- 1 | from leads import controller, MAIN_CONTROLLER, require_config, VisualDataContainer, DataContainer, device, \ 2 | FRONT_VIEW_CAMERA, LEFT_VIEW_CAMERA, RIGHT_VIEW_CAMERA, REAR_VIEW_CAMERA 3 | from leads.data_persistence import CSVDataset, VISUAL_HEADER_ONLY 4 | from leads_emulation.replay import ReplayController, ReplayCamera 5 | from leads_vec.config import Config 6 | 7 | config: Config = require_config() 8 | CAMERA_RESOLUTION: tuple[int, int] | None = config.get("camera_resolution") 9 | dataset: CSVDataset = CSVDataset(f"{config.data_dir}/main.csv") 10 | visual: bool = set(VISUAL_HEADER_ONLY).issubset(dataset.read_header()) 11 | 12 | 13 | @controller(MAIN_CONTROLLER, args=(dataset, VisualDataContainer if visual else DataContainer)) 14 | class MainController(ReplayController[DataContainer]): 15 | pass 16 | 17 | 18 | if visual: 19 | @device((FRONT_VIEW_CAMERA, LEFT_VIEW_CAMERA, RIGHT_VIEW_CAMERA, REAR_VIEW_CAMERA), MAIN_CONTROLLER, [ 20 | ("front", CAMERA_RESOLUTION), ("left", CAMERA_RESOLUTION), ("right", CAMERA_RESOLUTION), 21 | ("rear", CAMERA_RESOLUTION) 22 | ]) 23 | class Cameras(ReplayCamera): 24 | pass 25 | -------------------------------------------------------------------------------- /leads_vec/run.py: -------------------------------------------------------------------------------- 1 | from importlib.util import spec_from_file_location as _spec_from_file_location, module_from_spec as _module_from_spec 2 | from os.path import abspath as _abspath 3 | from os.path import exists as _exists 4 | from typing import Literal as _Literal 5 | 6 | from leads import register_controller as _register_controller, MAIN_CONTROLLER as _MAIN_CONTROLLER, \ 7 | L as _L, load_config as _load_config, register_config as _register_config, release as _release 8 | from leads_vec.config import Config 9 | 10 | 11 | def run(config: str | None, devices: str, main: str, register: _Literal["systemd", "config", "reverse_proxy"] | None, 12 | magnify_font_sizes: float, emu: bool, auto_mfs: bool, ignore_import_error: bool) -> int: 13 | match register: 14 | case "systemd": 15 | from ._bootloader import register_leads_vec as _create_service 16 | 17 | _create_service() 18 | _L.debug("Service registered") 19 | _L.debug(f"Service script is located at \"{_abspath(__file__)[:-6]}_bootloader/leads-vec.service.sh\"") 20 | return 0 21 | case "config": 22 | if _exists("config.json"): 23 | r = input("\"config.json\" already exists. Overwrite? (Y/n) >>>").lower() 24 | if r.lower() != "y": 25 | _L.error("Aborted") 26 | return 1 27 | with open("config.json", "w") as f: 28 | f.write(str(Config({}))) 29 | _L.debug("Configuration file saved to \"config.json\"") 30 | return 0 31 | case "reverse_proxy": 32 | from ._bootloader import start_frpc as _start_frpc 33 | 34 | _start_frpc() 35 | _L.debug("`frpc` started") 36 | case "splash_screen": 37 | from ._bootloader import register_splash_screen as _register_splash_screen, \ 38 | register_lock_screen as _register_lock_screen 39 | 40 | _register_splash_screen() 41 | _L.debug("Replaced splash screen") 42 | _register_lock_screen() 43 | _L.debug("Replaced lock screen") 44 | return 0 45 | config = _load_config(config, Config) if config else Config({}) 46 | _L.debug("Configuration loaded:", str(config)) 47 | if (f := magnify_font_sizes) != 1: 48 | config.magnify_font_sizes(f) 49 | if auto_mfs: 50 | config.auto_magnify_font_sizes() 51 | _register_config(config) 52 | spec = _spec_from_file_location("main", main) 53 | spec.loader.exec_module(main := _module_from_spec(spec)) 54 | try: 55 | main = getattr(main, "main") 56 | except AttributeError as e: 57 | _L.error(f"No main function in \"{main}\": {repr(e)}") 58 | return 1 59 | try: 60 | if emu: 61 | raise SystemError("User specifies to use emulator") 62 | spec = _spec_from_file_location("_", devices) 63 | spec.loader.exec_module(_module_from_spec(spec)) 64 | except (ImportError, SystemError) as e: 65 | _L.debug(repr(e)) 66 | if isinstance(e, ImportError): 67 | if ignore_import_error: 68 | _L.debug(f"Ignoring import error: {repr(e)}") 69 | return main() 70 | else: 71 | _L.warn(f"Specified devices module ({devices}) is not available, using emulation module instead...") 72 | _release() 73 | try: 74 | from leads_emulation import SinController as _Controller 75 | 76 | _register_controller(_MAIN_CONTROLLER, _Controller(0, 200, .05)) 77 | except ImportError as e: 78 | _L.error(f"Emulator error: {repr(e)}") 79 | return 1 80 | return main() 81 | -------------------------------------------------------------------------------- /leads_vec/utils.py: -------------------------------------------------------------------------------- 1 | from leads import LEADS as _LEADS, Plugin as _Plugin, SystemLiteral as _SystemLiteral, DTCS as _DTCS, ABS as _ABS, \ 2 | EBI as _EBI, ATBS as _ATBS, set_on_register_context as _set_on_register_context 3 | from leads.types import OnRegister as _OnRegister 4 | 5 | 6 | def register_plugins(plugins: dict[str, _Plugin] | None = None) -> None: 7 | if not plugins: 8 | plugins = {_SystemLiteral.DTCS: _DTCS(), _SystemLiteral.ABS: _ABS(), _SystemLiteral.EBI: _EBI(), 9 | _SystemLiteral.ATBS: _ATBS()} 10 | 11 | def _on_register_context(chain: _OnRegister[_LEADS]) -> _OnRegister[_LEADS]: 12 | def _(ctx: _LEADS) -> None: 13 | chain(ctx) 14 | for k, p in plugins.items(): 15 | ctx.plugin(k, p) 16 | 17 | return _ 18 | 19 | _set_on_register_context(_on_register_context) 20 | -------------------------------------------------------------------------------- /leads_vec_dp/__entry__.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser as _ArgumentParser 2 | from sys import exit as _exit 3 | 4 | from leads_vec_dp.run import run 5 | 6 | 7 | def __entry__() -> None: 8 | parser = _ArgumentParser(prog="LEADS VeC DP", 9 | description="Lightweight Embedded Assisted Driving System VeC Data Processor", 10 | epilog="GitHub: https://github.com/ProjectNeura/LEADS") 11 | parser.add_argument("workflow", help="specify a workflow file") 12 | args = parser.parse_args() 13 | _exit(run(args.workflow)) 14 | -------------------------------------------------------------------------------- /leads_vec_dp/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.util import find_spec as _find_spec 2 | 3 | if not _find_spec("yaml"): 4 | raise ImportError("Please install `pyyaml` to run this module\n>>>pip install pyyaml") 5 | 6 | from leads_vec_dp.__entry__ import __entry__ 7 | from leads_vec_dp.run import * 8 | -------------------------------------------------------------------------------- /leads_vec_dp/__main__.py: -------------------------------------------------------------------------------- 1 | from leads_vec_dp.__entry__ import __entry__ 2 | 3 | if __name__ == "__main__": 4 | __entry__() 5 | -------------------------------------------------------------------------------- /leads_vec_dp/run.py: -------------------------------------------------------------------------------- 1 | from atexit import register as _register 2 | from typing import Any as _Any 3 | 4 | from yaml import load as _load, SafeLoader as _SafeLoader 5 | 6 | from leads import L as _L 7 | from leads.data_persistence import CSVDataset as _CSVDataset 8 | from leads.data_persistence.analyzer import InferredDataset as _InferredDataset, Inference as _Inference, \ 9 | SafeSpeedInference as _SafeSpeedInference, SpeedInferenceByAcceleration as _SpeedInferenceByAcceleration, \ 10 | SpeedInferenceByMileage as _SpeedInferenceByMileage, \ 11 | SpeedInferenceByGPSGroundSpeed as _SpeedInferenceByGPSGroundSpeed, \ 12 | SpeedInferenceByGPSPosition as _SpeedInferenceByGPSPosition, \ 13 | ForwardAccelerationInferenceBySpeed as _ForwardAccelerationInferenceBySpeed, \ 14 | MileageInferenceBySpeed as _MileageInferenceBySpeed, \ 15 | MileageInferenceByGPSPosition as _MileageInferenceByGPSPosition, \ 16 | VisualDataRealignmentByLatency as _VisualDataRealignmentByLatency 17 | from leads.data_persistence.analyzer.processor import Processor as _Processor 18 | from leads_video import extract_video as _extract_video 19 | 20 | INFERENCE_METHODS: dict[str, type[_Inference]] = { 21 | "safe-speed": _SafeSpeedInference, 22 | "speed-by-acceleration": _SpeedInferenceByAcceleration, 23 | "speed-by-mileage": _SpeedInferenceByMileage, 24 | "speed-by-gps-ground-speed": _SpeedInferenceByGPSGroundSpeed, 25 | "speed-by-gps-position": _SpeedInferenceByGPSPosition, 26 | "forward-acceleration-by-speed": _ForwardAccelerationInferenceBySpeed, 27 | "mileage-by-speed": _MileageInferenceBySpeed, 28 | "mileage-by-gps-position": _MileageInferenceByGPSPosition, 29 | "visual-data-realignment-by-latency": _VisualDataRealignmentByLatency 30 | } 31 | 32 | 33 | def _optional_kwargs(source: dict[str, _Any], key: str) -> dict[str, _Any]: 34 | return source[key] if key in source else {} 35 | 36 | 37 | def run(target: str) -> int: 38 | with open(target) as f: 39 | target = _load(f.read(), _SafeLoader) 40 | if "inferences" in target: 41 | dataset = _InferredDataset(target["dataset"]) 42 | inferences = target["inferences"] 43 | if "clear" in inferences: 44 | dataset.clear_all(inferences["clear"]) 45 | inferences.pop("clear") 46 | methods = [] 47 | for method in inferences["methods"]: 48 | methods.append(INFERENCE_METHODS[method]()) 49 | inferences.pop("methods") 50 | repeat = 1 51 | if "repeat" in inferences: 52 | repeat = inferences["repeat"] 53 | inferences.pop("repeat") 54 | for _ in range(repeat): 55 | _L.info(f"Affected {(n := dataset.complete(*methods, **inferences))} row{"s" if n > 1 else ""}") 56 | else: 57 | dataset = _CSVDataset(target["dataset"]) 58 | _register(dataset.close) 59 | processor = _Processor(dataset) 60 | for job in target["jobs"]: 61 | _L.info(f"Executing job {job["name"]}...") 62 | match job["uses"]: 63 | case "bake": 64 | processor.bake() 65 | _L.info("Baking Results", *processor.baking_results(), sep="\n") 66 | case "process": 67 | processor.process(**_optional_kwargs(job, "with")) 68 | _L.info("Results", *processor.results(), sep="\n") 69 | case "draw-lap": 70 | processor.draw_lap(**_optional_kwargs(job, "with")) 71 | case "suggest-on-lap": 72 | _L.info(*processor.suggest_on_lap(job["with"]["lap_index"]), sep="\n") 73 | case "draw-comparison-of-laps": 74 | processor.draw_comparison_of_laps(**_optional_kwargs(job, "with")) 75 | case "extract-video": 76 | _extract_video(dataset, file := job["with"]["file"], job["with"]["tag"]) 77 | _L.info(f"Video saved as {file}") 78 | case "save-as": 79 | dataset.save(file := job["with"]["file"]) 80 | _L.info(f"Dataset saved as {file}") 81 | 82 | return 0 83 | -------------------------------------------------------------------------------- /leads_vec_rc/__entry__.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser as _ArgumentParser 2 | 3 | from uvicorn import run as _run 4 | 5 | from leads import register_config as _register_config, load_config as _load_config 6 | from leads_vec_rc.config import Config 7 | 8 | 9 | def __entry__() -> None: 10 | parser = _ArgumentParser(prog="LEADS VeC RC", 11 | description="Lightweight Embedded Assisted Driving System VeC Remote Controller", 12 | epilog="GitHub: https://github.com/ProjectNeura/LEADS") 13 | parser.add_argument("-c", "--config", default=None, help="specify a configuration file") 14 | parser.add_argument("-p", "--port", type=int, default=8000, help="specify a server port") 15 | args = parser.parse_args() 16 | _register_config(_load_config(args.config, Config) if args.config else Config({})) 17 | from leads_vec_rc.cli import app 18 | 19 | _run(app, host="0.0.0.0", port=args.port, log_level="warning") 20 | -------------------------------------------------------------------------------- /leads_vec_rc/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.util import find_spec as _find_spec 2 | 3 | if not _find_spec("fastapi"): 4 | raise ImportError("Please install `fastapi` to run this module\n>>>pip install \"fastapi[standard]\"") 5 | if not _find_spec("uvicorn"): 6 | raise ImportError("Please install `uvicorn` to run this module\n>>>pip install \"fastapi[standard]\"") 7 | 8 | from leads_vec_rc.__entry__ import __entry__ 9 | from leads_vec_rc.config import * 10 | -------------------------------------------------------------------------------- /leads_vec_rc/__main__.py: -------------------------------------------------------------------------------- 1 | from leads_vec_rc.__entry__ import __entry__ 2 | 3 | if __name__ == "__main__": 4 | __entry__() 5 | -------------------------------------------------------------------------------- /leads_vec_rc/cli.py: -------------------------------------------------------------------------------- 1 | from atexit import register 2 | from datetime import datetime 3 | from json import loads, JSONDecodeError 4 | from os import makedirs 5 | from os.path import abspath, exists 6 | from time import sleep 7 | from typing import Any, override 8 | 9 | from fastapi import FastAPI 10 | from fastapi.middleware.cors import CORSMiddleware 11 | 12 | from leads import require_config, L, DataContainer 13 | from leads.comm import Service, Client, start_client, create_client, Callback, Connection, ConnectionBase 14 | from leads.data_persistence import DataPersistence, CSV, DEFAULT_HEADER_FULL, VISUAL_HEADER_FULL 15 | from leads_vec_rc.config import Config 16 | 17 | config: Config = require_config() 18 | if not exists(config.data_dir): 19 | L.debug(f"Data directory not found. Creating \"{abspath(config.data_dir)}\"...") 20 | makedirs(config.data_dir) 21 | CAR_WIDTH: float = config.get("car_width", 2) 22 | CAR_LENGTH: float = config.get("car_length", 1) 23 | CAR_MASS: float = config.get("car_mass", 400) 24 | CAR_CENTER_OF_MASS: float = config.get("car_center_of_mass", .25) 25 | 26 | time_stamp_record: DataPersistence[int] = DataPersistence(2000) 27 | csv: CSV | None = None 28 | 29 | 30 | def try_create_csv(data: dict[str, Any]) -> None: 31 | global csv 32 | if csv: 33 | return 34 | csv = CSV(f"{config.data_dir}/{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}.csv", 35 | VISUAL_HEADER_FULL if set(VISUAL_HEADER_FULL).issubset(data.keys()) else DEFAULT_HEADER_FULL, 36 | time_stamp_record) 37 | register(csv.close) 38 | 39 | 40 | def retry(service: Service) -> Client: 41 | L.warn("Retrying connection...") 42 | return start_client(config.comm_addr, create_client(service.port(), callback), True) 43 | 44 | 45 | class CommCallback(Callback): 46 | def __init__(self) -> None: 47 | super().__init__() 48 | self.client: Client = start_client(config.comm_addr, create_client(config.comm_port, self), True) 49 | self.current_data: dict[str, Any] = DataContainer().to_dict() 50 | self.current_data.update({"report_rate": 0, "cfc_fl": 0, "cfc_fr": 0, "cfc_rl": 0, "cfc_rr": 0}) 51 | 52 | @override 53 | def on_connect(self, service: Service, connection: Connection) -> None: 54 | self.super(service=service, connection=connection) 55 | L.info("Connected") 56 | 57 | @override 58 | def on_fail(self, service: Service, error: Exception) -> None: 59 | self.super(service=service, error=error) 60 | L.error(f"Comm client error: {repr(error)}") 61 | sleep(10) 62 | self.client = retry(service) 63 | 64 | @override 65 | def on_receive(self, service: Service, msg: bytes) -> None: 66 | self.super(service=service, msg=msg) 67 | try: 68 | d = loads(msg.decode()) 69 | mg = CAR_MASS * 2.451675 70 | d["report_rate"] = 1000 * num_ts / (time_stamp_record[-1] - time_stamp_record[0]) if (num_ts := len( 71 | time_stamp_record)) > 1 else 0 72 | f_forward = CAR_MASS * d["forward_acceleration"] * CAR_CENTER_OF_MASS * .5 / CAR_WIDTH 73 | f_lateral = CAR_MASS * d["lateral_acceleration"] * CAR_CENTER_OF_MASS * .5 / CAR_LENGTH 74 | d["cfc_fl"] = mg + f_lateral - f_forward 75 | d["cfc_fr"] = mg - f_lateral - f_forward 76 | d["cfc_rl"] = mg + f_lateral + f_forward 77 | d["cfc_rr"] = mg - f_lateral + f_forward 78 | self.current_data = d 79 | if config.save_data: 80 | try_create_csv(d) 81 | csv.write_frame(*(d[key] for key in csv.header())) 82 | else: 83 | time_stamp_record.append(int(d["t"])) 84 | except JSONDecodeError: 85 | pass 86 | 87 | @override 88 | def on_disconnect(self, service: Service, connection: ConnectionBase) -> None: 89 | self.super(service=service, connection=connection) 90 | L.info("Disconnected") 91 | sleep(10) 92 | self.client = retry(service) 93 | 94 | 95 | callback: CommCallback = CommCallback() 96 | app: FastAPI = FastAPI(title="LEADS VeC Remote Analyst") 97 | app.add_middleware( 98 | CORSMiddleware, 99 | allow_origins=["*"], 100 | allow_credentials=True, 101 | allow_methods=["GET"], 102 | allow_headers=["*"], 103 | ) 104 | 105 | 106 | @app.get("/") 107 | async def index() -> str: 108 | return "LEADS VeC Remote Controller" 109 | 110 | 111 | @app.get("/current") 112 | async def current() -> dict[str, Any]: 113 | return callback.current_data 114 | 115 | 116 | @app.get("/time_stamp") 117 | async def time_stamp() -> list[int]: 118 | return time_stamp_record.to_list() 119 | 120 | 121 | @app.get("/time_lap") 122 | async def time_lap() -> str: 123 | callback.client.send(b"time_lap") 124 | return "done" 125 | 126 | 127 | @app.get("/hazard") 128 | async def hazard() -> str: 129 | callback.client.send(b"hazard") 130 | return "done" 131 | 132 | 133 | @app.get("/m1") 134 | async def m1() -> str: 135 | callback.client.send(b"m1") 136 | return "done" 137 | 138 | 139 | @app.get("/m3") 140 | async def m3() -> str: 141 | callback.client.send(b"m3") 142 | return "done" 143 | -------------------------------------------------------------------------------- /leads_vec_rc/config.py: -------------------------------------------------------------------------------- 1 | from typing import Any as _Any 2 | 3 | from leads import ConfigTemplate as _ConfigTemplate 4 | 5 | 6 | class Config(_ConfigTemplate): 7 | def __init__(self, base: dict[str, _Any]) -> None: 8 | self.comm_addr: str = "127.0.0.1" 9 | self.comm_port: int = 16900 10 | self.data_dir: str = "data" 11 | self.save_data: bool = False 12 | super().__init__(base) 13 | -------------------------------------------------------------------------------- /leads_video/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.util import find_spec as _find_spec 2 | 3 | if not _find_spec("cv2"): 4 | raise ImportError( 5 | "Please install `opencv-python-headless` to run this module\n>>>pip install opencv-python-headless") 6 | if not _find_spec("PIL"): 7 | raise ImportError("Please install `Pillow` to run this module\n>>>pip install Pillow") 8 | 9 | from leads_video.camera import * 10 | from leads_video.utils import * 11 | -------------------------------------------------------------------------------- /leads_video/camera.py: -------------------------------------------------------------------------------- 1 | from base64 import b64encode as _b64encode 2 | from io import BytesIO as _BytesIO 3 | from threading import Thread as _Thread 4 | from time import time as _time, sleep as _sleep 5 | from typing import override as _override 6 | 7 | from PIL.Image import fromarray as _fromarray, Image as _Image, open as _open 8 | from cv2 import VideoCapture as _VideoCapture, cvtColor as _cvtColor, COLOR_BGR2RGB as _COLOR_BGR2RGB, \ 9 | imencode as _imencode, COLOR_RGB2BGR as _COLOR_RGB2BGR, IMWRITE_JPEG_QUALITY as _IMWRITE_JPEG_QUALITY 10 | from numpy import ndarray as _ndarray, pad as _pad, array as _array 11 | 12 | from leads import Device as _Device, ShadowDevice as _ShadowDevice 13 | 14 | 15 | class Camera(_Device): 16 | def __init__(self, port: int, resolution: tuple[int, int] | None = None) -> None: 17 | super().__init__(port) 18 | self._resolution: tuple[int, int] | None = resolution 19 | self._video_capture: _VideoCapture | None = None 20 | self._birth: float = 0 21 | 22 | @_override 23 | def initialize(self, *parent_tags: str) -> None: 24 | super().initialize(*parent_tags) 25 | self._video_capture = _VideoCapture(self._pins[0]) 26 | 27 | @_override 28 | def write(self, payload: tuple[int, int] | None) -> None: 29 | """ 30 | :param payload: [width, height] 31 | """ 32 | self._resolution = payload 33 | 34 | def transform(self, x: _ndarray) -> _ndarray: 35 | height, width = x.shape[:-1] 36 | resolution_width, resolution_height = self._resolution 37 | target_ratio = resolution_height / resolution_width 38 | target_height = int(target_ratio * width) 39 | pad_left, pad_right, pad_top, pad_bottom = 0, 0, 0, 0 40 | if height > target_height: 41 | target_width = int(height / target_ratio) 42 | pad_left = (target_width - width) // 2 43 | pad_right = target_width - width - pad_left 44 | else: 45 | pad_top = (target_height - height) // 2 46 | pad_bottom = target_height - height - pad_top 47 | return _array(_fromarray(_pad(x, ((pad_left, pad_right), (pad_top, pad_bottom), (0, 0)))).resize( 48 | self._resolution)) 49 | 50 | @_override 51 | def read(self) -> _ndarray | None: 52 | ret, frame = self._video_capture.read() 53 | self._birth = _time() 54 | return _cvtColor(self.transform(frame) if self._resolution else frame, _COLOR_BGR2RGB) if ret else None 55 | 56 | def read_numpy(self) -> _ndarray | None: 57 | return self.read() 58 | 59 | def read_pil(self) -> _Image | None: 60 | return None if (frame := self.read_numpy()) is None else _fromarray(frame) 61 | 62 | def latency(self) -> float: 63 | return _time() - self._birth 64 | 65 | @_override 66 | def close(self) -> None: 67 | self._video_capture.release() 68 | 69 | 70 | class LowLatencyCamera(Camera, _ShadowDevice): 71 | def __init__(self, port: int, resolution: tuple[int, int] | None = None) -> None: 72 | Camera.__init__(self, port, resolution) 73 | _ShadowDevice.__init__(self, port) 74 | self._frame: _ndarray | None = None 75 | 76 | @_override 77 | def loop(self) -> None: 78 | if self._video_capture: 79 | self._frame = super().read() 80 | 81 | @_override 82 | def read(self) -> _ndarray | None: 83 | return self._frame 84 | 85 | 86 | class Base64Camera(LowLatencyCamera): 87 | def __init__(self, port: int, resolution: tuple[int, int] | None = None, quality: int = 90) -> None: 88 | super().__init__(port, resolution) 89 | self._quality: int = quality 90 | self._shadow_thread2: _Thread | None = None 91 | self._bytes: bytes = b"" 92 | self._base64: str = "" 93 | 94 | @_override 95 | def loop(self) -> None: 96 | super().loop() 97 | 98 | @staticmethod 99 | def encode(frame: _ndarray, quality: int = 100) -> bytes: 100 | return _imencode(".jpg", _cvtColor(frame, _COLOR_RGB2BGR), (_IMWRITE_JPEG_QUALITY, quality))[1].tobytes() 101 | 102 | def loop2(self) -> None: 103 | if (frame := self._frame) is not None: 104 | self._bytes = Base64Camera.encode(frame, self._quality) 105 | self._base64 = _b64encode(self._bytes).decode() 106 | 107 | def run2(self) -> None: 108 | while True: 109 | self.loop2() 110 | _sleep(.002) 111 | 112 | @_override 113 | def initialize(self, *parent_tags: str) -> None: 114 | super().initialize(*parent_tags) 115 | self._shadow_thread2 = _Thread(name=f"{id(self)} shadow2", target=self.run2, daemon=True) 116 | self._shadow_thread2.start() 117 | 118 | @_override 119 | def read(self) -> str: 120 | return self._base64 121 | 122 | @_override 123 | def read_numpy(self) -> _ndarray | None: 124 | return self._frame 125 | 126 | @_override 127 | def read_pil(self) -> _Image | None: 128 | return _open(_BytesIO(self._bytes)) if self._bytes else None 129 | 130 | 131 | class LightweightBase64Camera(Base64Camera): 132 | @_override 133 | def loop(self) -> None: 134 | super().loop() 135 | super().loop2() 136 | 137 | @_override 138 | def initialize(self, *parent_tags: str) -> None: 139 | LowLatencyCamera.initialize(self, *parent_tags) 140 | -------------------------------------------------------------------------------- /leads_video/utils.py: -------------------------------------------------------------------------------- 1 | from base64 import b64decode as _b64decode 2 | from binascii import Error as _BinasciiError 3 | from io import BytesIO as _BytesIO 4 | from typing import Any as _Any, Literal as _Literal 5 | 6 | from PIL.Image import Image as _Image, open as _open, UnidentifiedImageError as _UnidentifiedImageError 7 | from cv2 import VideoWriter as _VideoWriter, VideoWriter_fourcc as _VideoWriter_fourcc, cvtColor as _cvtColor, \ 8 | COLOR_RGB2BGR as _COLOR_RGB2BGR 9 | from numpy import array as _array 10 | 11 | from leads import has_device as _has_device, get_device as _get_device 12 | from leads.data_persistence import CSVDataset as _CSVDataset 13 | from leads_video.camera import Camera 14 | 15 | 16 | def get_camera(tag: str, required_type: type[Camera] = Camera) -> Camera | None: 17 | if not _has_device(tag): 18 | return None 19 | cam = _get_device(tag) 20 | if not isinstance(cam, required_type): 21 | raise TypeError(f"Device \"{tag}\" is supposed to be a camera") 22 | return cam 23 | 24 | 25 | def _decode_frame(row: dict[str, _Any], tag: str) -> _Image: 26 | if not (frame := row[tag]): 27 | raise ValueError 28 | return _open(_BytesIO(_b64decode(frame))) 29 | 30 | 31 | def extract_video(dataset: _CSVDataset, file: str, channel: _Literal["front", "left", "right", "rear"]) -> None: 32 | if not file.endswith(".mp4"): 33 | file += ".mp4" 34 | channel = f"{channel}_view_base64" 35 | prev_row = None 36 | resolution = None 37 | fps = 0 38 | cache = None 39 | for row in dataset: 40 | if not resolution: 41 | try: 42 | frame = _decode_frame(row, channel) 43 | cache = _cvtColor(_array(frame), _COLOR_RGB2BGR) 44 | resolution = _decode_frame(row, channel).size 45 | except (ValueError, _BinasciiError, _UnidentifiedImageError): 46 | pass 47 | if prev_row: 48 | local_fps = 1000 / (row["t"] - prev_row["t"]) 49 | if local_fps > fps: 50 | fps = local_fps 51 | prev_row = row 52 | if not resolution or not fps or cache is None: 53 | raise AttributeError("Failed to determine video resolution, frame rate, or cache") 54 | writer = _VideoWriter(file, _VideoWriter_fourcc(*"mp4v"), fps, resolution) 55 | for row in dataset: 56 | try: 57 | writer.write(cache := _cvtColor(_array(_decode_frame(row, channel)), _COLOR_RGB2BGR)) 58 | except (ValueError, _BinasciiError, _UnidentifiedImageError): 59 | writer.write(cache) 60 | writer.release() 61 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "leads" 7 | version = "0.9.7" 8 | description = "Lightweight Embedded Assisted Driving System" 9 | license = "Apache-2.0" 10 | readme = "README.md" 11 | requires-python = ">=3.12" 12 | authors = [ 13 | { name = "Project Neura", email = "central@projectneura.org" } 14 | ] 15 | dependencies = ["numpy", "pandas"] 16 | 17 | [project.optional-dependencies] 18 | standard = [ 19 | "Pillow", "PySDL2", "customtkinter", "gpiozero", "opencv-python-headless", "pynmea2", "pysdl2-dll", "pyserial", 20 | "screeninfo" 21 | ] 22 | gpio = ["leads[standard]", "lgpio"] 23 | vec = ["leads[gpio]", "pynput"] 24 | vec-no-gpio = ["leads[standard]", "pynput"] 25 | vec-rc = ["leads[standard]", "fastapi[standard]"] 26 | vec-dp = ["leads[standard]", "matplotlib", "pyyaml"] 27 | 28 | [tool.hatch.build.targets.sdist] 29 | only-include = ["leads", "leads_arduino", "leads_audio", "leads_can", "leads_comm_serial", "leads_emulation", 30 | "leads_gpio", "leads_gui", "leads_video", "leads_vec", "leads_vec_rc", "leads_vec_dp", "design", "docs"] 31 | 32 | [tool.hatch.build.targets.wheel] 33 | packages = ["leads", "leads_arduino", "leads_audio", "leads_can", "leads_comm_serial", "leads_emulation", "leads_gpio", 34 | "leads_gui", "leads_video", "leads_vec", "leads_vec_rc", "leads_vec_dp"] 35 | 36 | [project.urls] 37 | Homepage = "https://leads.projectneura.org" 38 | Documentation = "https://leads-docs.projectneura.org" 39 | Repository = "https://github.com/ProjectNeura/LEADS" 40 | 41 | [project.scripts] 42 | leads-vec-rc = "leads_vec_rc:__entry__" 43 | leads-vec-dp = "leads_vec_dp:__entry__" 44 | 45 | [project.gui-scripts] 46 | leads-vec = "leads_vec:__entry__" 47 | -------------------------------------------------------------------------------- /scripts/frp-config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | abort() { 4 | printf "%s\n" "$@" >&2 5 | exit 1 6 | } 7 | 8 | if test ! -d "/usr/local/frp" 9 | then abort "Error: /usr/local/frp not found" 10 | fi 11 | 12 | execute() { 13 | if ! "$@" 14 | then abort "$(printf "Failed: %s" "$@")" 15 | fi 16 | } 17 | 18 | execute_root() { 19 | execute "sudo" "$@" 20 | } 21 | 22 | require_argument() { 23 | if test -z "$1" 24 | then abort "Error: Required argument $2 does not exist" 25 | else echo "$1" 26 | fi 27 | } 28 | 29 | argument_exists_or() { 30 | if test -z "$1" 31 | then echo "$2" 32 | else echo "$1" 33 | fi 34 | } 35 | 36 | echo "Configuring client..." 37 | execute_root "echo" "serverAddr = \"$(require_argument "$1" "frp_server_ip")\"" > "/usr/local/frp/frpc.toml" 38 | { 39 | execute_root "echo" "serverPort = $(argument_exists_or "$3" "7000")" 40 | execute_root "echo" "auth.method = \"token\"" 41 | execute_root "echo" "auth.token = \"$(require_argument "$2" "frp_token")\"" 42 | execute_root "echo" "[[proxies]]" 43 | execute_root "echo" "name = \"leads-vec-comm\"" 44 | execute_root "echo" "type = \"tcp\"" 45 | execute_root "echo" "localIP = \"127.0.0.1\"" 46 | execute_root "echo" "localPort = $(argument_exists_or "$4" "16900")" 47 | execute_root "echo" "remotePort = $(argument_exists_or "$4" "16900")" 48 | } >> "/usr/local/frp/frpc.toml" 49 | echo "Configuring server..." 50 | execute_root "echo" "bindPort = $(argument_exists_or "$3" "7000")" > "/usr/local/frp/frps.toml" 51 | { 52 | execute_root "echo" "auth.method = \"token\"" 53 | execute_root "echo" "auth.token = \"$(require_argument "$2" "frp_token")\"" 54 | execute_root "echo" "vhostHTTPPort = 80" 55 | execute_root "echo" "vhostHTTPSPort = 443" 56 | } >> "/usr/local/frp/frps.toml" -------------------------------------------------------------------------------- /scripts/frp-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | abort() { 4 | printf "%s\n" "$@" >&2 5 | exit 1 6 | } 7 | 8 | if test -d "/usr/local/frp" 9 | then abort "Error: /usr/local/frp already exists" 10 | fi 11 | 12 | execute() { 13 | if ! "$@" 14 | then abort "$(printf "Failed: %s" "$@")" 15 | fi 16 | } 17 | 18 | execute_root() { 19 | execute "sudo" "$@" 20 | } 21 | 22 | if ! command -v curl > /dev/null 23 | then 24 | echo "cURL is not available, installing..." 25 | execute_root "apt" "install" "-y" "curl" 26 | fi 27 | latest_release=$(curl -s "https://api.github.com/repos/fatedier/frp/releases/latest" | grep -o '"tag_name": "[^"]*' | grep -o '[^"]*$' | cut -c 2-) 28 | if test -z "$latest_release" 29 | then abort "Error: Failed to retrieve the latest release" 30 | fi 31 | filename="frp_${latest_release}_$(uname -s | tr "[:upper:]" "[:lower:]")_$(uname -m | sed "s/x86_64/amd64/g" | sed "s/aarch/arm/g")" 32 | echo "Downloading ${filename}.tar.gz..." 33 | execute_root "wget" "-O" "frp.tar.gz" "https://github.com/fatedier/frp/releases/download/v${latest_release}/${filename}.tar.gz" 34 | echo "Extracting..." 35 | execute_root "tar" "-xzvf" "frp.tar.gz" 36 | echo "Moving ${filename} to /usr/local/frp..." 37 | execute_root "mv" "${filename}" "/usr/local/frp" 38 | echo "Cleaning up..." 39 | execute_root "rm" "frp.tar.gz" -------------------------------------------------------------------------------- /scripts/python-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | abort() { 4 | printf "%s\n" "$@" >&2 5 | exit 1 6 | } 7 | 8 | execute() { 9 | if ! "$@" 10 | then abort "$(printf "Failed: %s" "$@")" 11 | fi 12 | } 13 | 14 | execute_root() { 15 | execute "sudo" "$@" 16 | } 17 | 18 | echo "Adding APT repository..." 19 | execute_root "add-apt-repository" "ppa:deadsnakes/ppa" "-y" 20 | execute_root "apt" "update" 21 | echo "Installing Python 3.12..." 22 | execute_root "apt" "install" "-y" "gcc" "python3.12" "python3.12-dev" "python3.12-venv" 23 | echo "Installing Tcl/Tk..." 24 | execute_root "apt" "install" "-y" "python3.12-tk" 25 | echo "Creating Virtual Environment..." 26 | execute_root "python3.12" "-m" "venv" "/usr/local/leads/venv" 27 | echo "Creating Soft Links..." 28 | execute_root "echo" "#!/bin/bash" > "/bin/python-leads" 29 | execute_root "echo" '/usr/local/leads/venv/bin/python "$@"' >> "/bin/python-leads" 30 | execute_root "chmod" "+x" "/bin/python-leads" 31 | execute_root "ln" "-s" "/usr/local/leads/venv/bin/pip" "/bin/pip-leads" -------------------------------------------------------------------------------- /scripts/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | abort() { 4 | printf "%s\n" "$@" >&2 5 | exit 1 6 | } 7 | 8 | execute() { 9 | if ! "$@" 10 | then abort "$(printf "Failed: %s" "$@")" 11 | fi 12 | } 13 | 14 | execute_root() { 15 | execute "sudo" "$@" 16 | } 17 | 18 | echo "Installing Python..." 19 | execute_root "wget" "-O" "python-install.sh" "https://raw.githubusercontent.com/ProjectNeura/LEADS/main/scripts/python-install.sh" 20 | execute_root "/bin/bash" "python-install.sh" 21 | echo "Cleaning up..." 22 | execute_root "rm" "python-install.sh" 23 | echo "Installing dependencies..." 24 | execute_root "pip-leads" "install" "leads[vec]" 25 | echo "Creating executable entries..." 26 | execute_root "echo" "#!/bin/bash" > "/bin/leads-vec" 27 | execute_root "echo" 'python-leads -m leads_vec "$@"' >> "/bin/leads-vec" 28 | execute_root "chmod" "+x" "/bin/leads-vec" 29 | execute_root "echo" "#!/bin/bash" > "/bin/leads-vec-rc" 30 | execute_root "echo" 'python-leads -m leads_vec_rc "$@"' >> "/bin/leads-vec-rc" 31 | execute_root "chmod" "+x" "/bin/leads-vec-rc" 32 | execute_root "echo" "#!/bin/bash" > "/bin/leads-vec-dp" 33 | execute_root "echo" 'python-leads -m leads_vec_dp "$@"' >> "/bin/leads-vec-dp" 34 | execute_root "chmod" "+x" "/bin/leads-vec-dp" 35 | execute_root "chmod" "666" "/usr/local/leads/venv/lib/python3.12/site-packages/leads/_ltm/core" 36 | echo "Verifying..." 37 | execute "leads-vec" "info" -------------------------------------------------------------------------------- /scripts/uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | abort() { 4 | printf "%s\n" "$@" >&2 5 | exit 1 6 | } 7 | 8 | execute() { 9 | if ! "$@" 10 | then abort "$(printf "Failed: %s" "$@")" 11 | fi 12 | } 13 | 14 | execute_root() { 15 | execute "sudo" "$@" 16 | } 17 | 18 | ask() { 19 | printf "%s >>>" "$@" >&0 20 | read -r input 21 | echo "$input" | tr "[:upper:]" "[:lower:]" 22 | } 23 | 24 | if test -e "/home/$(logname)/.config/systemd/user/leads-vec.service" 25 | then 26 | if systemctl --user is-enabled --quiet "leads-vec" 27 | then 28 | echo "Disabling Systemd service leads-vec..." 29 | execute_root "systemctl" "--user" "disable" "leads-vec" 30 | else echo "Systemd service leads-vec not enabled, skipping..." 31 | fi 32 | echo "Removing Systemd service leads-vec..." 33 | execute_root "rm" "/home/$(logname)/.config/systemd/user/leads-vec.service" 34 | else echo "Systemd service leads-vec not detected, skipping..." 35 | fi 36 | 37 | if test -e "/usr/local/frp" 38 | then 39 | if test "$(ask "Do you want to remove FRP? (Y/n)")" = "y" 40 | then 41 | echo "Removing FRP..." 42 | execute_root "rm" "-r" "/usr/local/frp" 43 | else echo "Abort" 44 | fi 45 | else echo "FRP not detected, skipping..." 46 | fi 47 | 48 | if test -e "/usr/local/leads/config.json" 49 | then 50 | if test "$(ask "Do you want to remove the configuration file? (Y/n)")" = "y" 51 | then execute_root "rm" "/usr/local/leads/config.json" 52 | else echo "Abort" 53 | fi 54 | else echo "Configuration file not detected, skipping..." 55 | fi 56 | 57 | if test -e "/bin/leads-vec" 58 | then 59 | echo "Removing soft link /bin/leads-vec..." 60 | execute_root "rm" "/bin/leads-vec" 61 | else echo "Soft link /bin/leads-vec not detected, skipping..." 62 | fi 63 | 64 | if test -e "/bin/pip-leads" 65 | then 66 | echo "Removing soft link /bin/pip-leads..." 67 | execute_root "rm" "/bin/pip-leads" 68 | else echo "Soft link /bin/pip-leads not detected, skipping..." 69 | fi 70 | 71 | if test -e "/bin/python-leads" 72 | then 73 | echo "Removing soft link /bin/python-leads..." 74 | execute_root "rm" "/bin/python-leads" 75 | else echo "Soft link /bin/python-leads not detected, skipping..." 76 | fi 77 | 78 | if test -e "/usr/local/leads/venv" 79 | then 80 | echo "Removing virtual environment..." 81 | execute_root "rm" "-r" "/usr/local/leads/venv" 82 | else echo "Virtual environment not detected, skipping..." 83 | fi 84 | 85 | echo "LEADS has been uninstalled" --------------------------------------------------------------------------------