diff --git a/Arduino/LIN_to_IR/LIN_to_IR.cpp b/Arduino/LIN_to_IR/LIN_to_IR.cpp index 1cba575..e3f05eb 100644 --- a/Arduino/LIN_to_IR/LIN_to_IR.cpp +++ b/Arduino/LIN_to_IR/LIN_to_IR.cpp @@ -10,10 +10,15 @@ #define FAULT_PIN PIN_PB1 // Digital pin 1 #define CS_PIN PIN_PB3 // Digital pin 11 +#define RTI_RX_PIN PIN_PA4 // Digital pin 6 (Unused/LED COM) +#define RTI_TX_PIN PIN_PA5 // Digital pin 5 +#define MUX_LOGIC_SEL_PIN PIN_PA6 // Digital pin 4 + #define SYN_FIELD 0x55 #define SWM_ID 0x20 SoftwareSerial LINBusSerial(RX_PIN, TX_PIN); +SoftwareSerial rtiSerial(RTI_RX_PIN, RTI_TX_PIN); // RC-6 timing constants // 1 time unit (1t) = 444us @@ -86,6 +91,7 @@ uint32_t get_mce_code(uint8_t button) { case 5: return 0x800f0422; // ENTER / OK case 6: return 0x800f0423; // BACK case 7: return 0x800f0410; // Volume Up (Shutdown) + case 9: return 0x800f0411; // Volume Down (Sleep) default: return 0; } } @@ -109,33 +115,70 @@ unsigned long last_ir_send_time = 0; unsigned long last_lin_activity_time = 0; bool is_car_on = false; +// RTI Screen variables +bool screen_open = true; // Screen should open at startup +unsigned long last_rti_send_time = 0; +uint8_t rti_byte_index = 0; + +// Debouncing variables for combinations +bool combination_active = false; +uint8_t pending_button = 0; +unsigned long pending_button_time = 0; +bool button_triggered = false; + void process_button_state(uint8_t active_button) { unsigned long now = millis(); if (active_button != 0) { - if (current_button == 0) { - // Button was just pressed! - current_button = active_button; - toggle_bit ^= 1; // Toggle bit alternates on each new press - send_ir_for_button(current_button, toggle_bit); - last_ir_send_time = now; - } else if (current_button == active_button) { - // Button is being held down! - // Send repeat code every 250ms - if (now - last_ir_send_time >= 250) { - send_ir_for_button(current_button, toggle_bit); - last_ir_send_time = now; + if (active_button >= 8) { + // Combinations trigger immediately without delay + if (current_button != active_button) { + current_button = active_button; + pending_button = 0; + button_triggered = true; + + if (current_button == 8) { + screen_open = !screen_open; + rti_byte_index = 0; // Reset byte index to send new sequence immediately + } else if (current_button == 9) { + toggle_bit ^= 1; + send_ir_for_button(9, toggle_bit); + last_ir_send_time = now; + } + } else if (current_button == 9) { + // Repeat sleep command if held + if (now - last_ir_send_time >= 250) { + send_ir_for_button(9, toggle_bit); + last_ir_send_time = now; + } } } else { - // A different button was pressed! - current_button = active_button; - toggle_bit ^= 1; - send_ir_for_button(current_button, toggle_bit); - last_ir_send_time = now; + // Single buttons: delay by 80ms to check if a combination is being pressed + if (pending_button != active_button) { + pending_button = active_button; + pending_button_time = now; + button_triggered = false; + } else if (!button_triggered) { + if (now - pending_button_time >= 80) { + current_button = active_button; + button_triggered = true; + toggle_bit ^= 1; + send_ir_for_button(current_button, toggle_bit); + last_ir_send_time = now; + } + } else { + // Button was already triggered and is being held down + if (now - last_ir_send_time >= 250) { + send_ir_for_button(current_button, toggle_bit); + last_ir_send_time = now; + } + } } } else { // Idle state current_button = 0; + pending_button = 0; + button_triggered = false; } last_frame_time = now; @@ -153,14 +196,36 @@ void handle_frame() { uint8_t d0 = frame.get_byte(1); uint8_t d1 = frame.get_byte(2); + bool enter_pressed = (d1 & 0x08) != 0; + bool back_pressed = (d1 & 0x01) != 0; + bool right_pressed = (d0 & 0x08) != 0; + uint8_t active_button = 0; - if (d0 & 0x01) active_button = 1; // UP - else if (d0 & 0x02) active_button = 2; // DOWN - else if (d0 & 0x04) active_button = 3; // LEFT - else if (d0 & 0x08) active_button = 4; // RIGHT - else if (d1 & 0x08) active_button = 5; // ENTER / OK - else if (d1 & 0x01) active_button = 6; // BACK + if (back_pressed && enter_pressed) { + active_button = 8; // Special combination: BACK + ENTER (Toggle Screen) + combination_active = true; + } else if (right_pressed && enter_pressed) { + active_button = 9; // Special combination: RIGHT + ENTER (Sleep Raspberry) + combination_active = true; + } else if (combination_active) { + // A combination was active, but it's no longer detected as a combination. + // If any buttons are still pressed, ignore them to prevent false single button triggering. + if (enter_pressed || back_pressed || right_pressed || (d0 & 0x01) || (d0 & 0x02) || (d0 & 0x04)) { + active_button = 0; // Ignore + } else { + combination_active = false; // All buttons released, clear flag + active_button = 0; + } + } else { + // Normal single button decoding + if (d0 & 0x01) active_button = 1; // UP + else if (d0 & 0x02) active_button = 2; // DOWN + else if (d0 & 0x04) active_button = 3; // LEFT + else if (d0 & 0x08) active_button = 4; // RIGHT + else if (d1 & 0x08) active_button = 5; // ENTER / OK + else if (d1 & 0x01) active_button = 6; // BACK + } process_button_state(active_button); } @@ -172,6 +237,10 @@ void setup() { pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, LOW); // LED OFF + // Mux logic select pin + pinMode(MUX_LOGIC_SEL_PIN, OUTPUT); + digitalWrite(MUX_LOGIC_SEL_PIN, HIGH); + // Enable MCP2004 pinMode(CS_PIN, OUTPUT); digitalWrite(CS_PIN, HIGH); @@ -180,9 +249,12 @@ void setup() { digitalWrite(FAULT_PIN, HIGH); LINBusSerial.begin(9600); + rtiSerial.begin(2400); + frame = LinFrame(); last_frame_time = millis(); last_lin_activity_time = millis(); + last_rti_send_time = millis(); } void loop() { @@ -190,6 +262,7 @@ void loop() { last_lin_activity_time = millis(); if (!is_car_on) { is_car_on = true; + screen_open = true; // Automatically open screen when car turns back on } b = LINBusSerial.read(); n = frame.num_bytes(); @@ -207,15 +280,42 @@ void loop() { // Timeout: if no LIN frames received for 200ms, assume no button is pressed if (millis() - last_frame_time > 200) { - current_button = 0; + if (pending_button == 0) { + current_button = 0; + button_triggered = false; + } } // Car off detection: if no LIN activity for 5 seconds, send Volume Up to trigger shutdown if (is_car_on && (millis() - last_lin_activity_time > 5000)) { is_car_on = false; + screen_open = false; // Close screen when car is off for (int i = 0; i < 3; i++) { send_ir_for_button(7, i & 1); delay(100); } } + + // Send RTI screen serial command byte every 100ms (non-blocking) + unsigned long now = millis(); + if (now - last_rti_send_time >= 100) { + last_rti_send_time = now; + + uint8_t byte_to_send = 0; + if (screen_open) { + // ON sequence: 0x4C (NTSC), 0x2F (max brightness), 0x83 (execute) + if (rti_byte_index == 0) byte_to_send = 0x4C; + else if (rti_byte_index == 1) byte_to_send = 0x2F; + else byte_to_send = 0x83; + } else { + // OFF sequence: 0x46 (OFF), 0x00 (min brightness), 0x83 (execute) + if (rti_byte_index == 0) byte_to_send = 0x46; + else if (rti_byte_index == 1) byte_to_send = 0x00; + else byte_to_send = 0x83; + } + + rtiSerial.write(byte_to_send); + rti_byte_index = (rti_byte_index + 1) % 3; + } } + diff --git a/README.md b/README.md index b49e829..8cba524 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # VolvoRTI — Android Auto Retrofit for Volvo RTI -An integrated Android Auto retrofit system for the retracting Volvo Road and Traffic Information (RTI) navigation screen. Using a Raspberry Pi 4, custom microcontrollers, custom PCB routing, and 3D-printed mechanical enclosures, this project aims to replace the legacy navigation unit with a modern infotainment hub running LineageOS (Android). +An integrated Android Auto retrofit system for the retracting Volvo Road and Traffic Information (RTI) navigation screen. Using a Raspberry Pi 4, custom microcontrollers, custom PCB routing, and 3D-printed mechanical enclosures, this project replaces the legacy navigation unit with a modern infotainment hub running LineageOS (Android).
Volvo RTI Retrofit Focused Screen @@ -16,16 +16,17 @@ An integrated Android Auto retrofit system for the retracting Volvo Road and Tra ## 🛠️ System Architecture -The retrofit consists of three key layers working in unison: **vehicle bus integration (LIN)**, **screen power/command control (RTI Serial)**, and the **core computer (Raspberry Pi 4)**. +The retrofit consists of two key layers working in unison: the **ATtiny84 Controller** (vehicle bus decoding + screen motor control) and the **core computer** (Raspberry Pi 4 running Android). ```mermaid graph TD %% Vehicles Inputs - SWM[Steering Wheel Module - LIN Bus] -- "LIN Frame ID 0x20" --> ATtiny[ATtiny84 Steering Controller] + SWM[Steering Wheel Module - LIN Bus] -- "LIN Frame ID 0x20" --> ATtiny[ATtiny84 Controller] CEM[Central Electronic Module - LIN] -- "LIN Keep-Alive Ping" --> ATtiny - %% ATtiny Processing + %% ATtiny Processing & Control ATtiny -- "RC-6 MCE IR Protocol (Bit-banged)" --> Pi_IR[Raspberry Pi GPIO 24 / IR Input] + ATtiny -- "2400 Baud Serial Control" --> Screen[Volvo RTI Retractable Screen] %% Pi Processing subgraph Raspberry Pi 4 [Raspberry Pi 4 - LineageOS] @@ -34,13 +35,12 @@ graph TD Hotspot[Init Services] -- "Auto-start on Boot" --> AP[Wi-Fi AP: VolvoC70_AndroidAuto] end - %% Screen Control - ATmega[Arduino Screen Controller] -- "2400 Baud Serial Control" --> Screen[Volvo RTI Retractable Screen] - Pi_Video[Raspberry Pi HDMI] -- "VGA / RGBS Video Signal" --> VideoMux[Video Mux] --> Screen + %% Video Routing + Pi_Video[Raspberry Pi HDMI] -- "HDMI Video Signal" --> Screen classDef hardware fill:#f9f,stroke:#333,stroke-width:2px; classDef software fill:#bbf,stroke:#333,stroke-width:2px; - class SWM,CEM,ATtiny,ATmega,Screen,VideoMux hardware; + class SWM,CEM,ATtiny,Screen hardware; class KM,SD,Hotspot,AP software; ``` @@ -49,10 +49,10 @@ graph TD ## 📁 Repository Structure * 📁 **`Arduino/`**: Microcontroller firmware written for the Arduino ecosystem. - * `LIN_to_IR/`: Decodes Steering Wheel Module (SWM) buttons from the LIN bus and outputs RC-6 Mode 6A IR commands to control Android. Monitors LIN inactivity to signal shutdown. - * `RTI_Control/`: Drives the serial protocol of the Volvo retractable screen, sending commands to raise the screen, set brightness, and keep it active. + * `LIN_to_IR/`: The unified controller firmware. Decodes Steering Wheel Module (SWM) buttons from the LIN bus, outputs RC-6 Mode 6A IR commands to control Android, and drives the Volvo screen motor serial interface (automatic opening/closing, toggle, and car power state detection). + * `RTI_Control/`: Legacy screen-only driver firmware (retains original standalone serial protocol testing logic). * `IR_remote_test/`: Test firmware for verifying RC-6 IR signal generation. -* 📁 **`Kicad/`**: Schematic, PCB layout, footprints, and Gerber files for a custom Raspberry Pi shield designed to host the MCP2004 LIN transceiver, video muxing, and power routing. +* 📁 **`Kicad/`**: Schematic, PCB layout, footprints, and Gerber files for a custom Raspberry Pi shield hosting the MCP2004 LIN transceiver, video routing, and power control. * 📁 **`CAD/`**: SolidWorks 3D CAD design files (`.SLDASM`, `.SLDPRT`) and 3D-printable `.STL` files for the custom mechanical Pi enclosure (`case_top` & `case_bottom`). * 📁 **`Raspberry/`**: Configuration scripts and mapping files for LineageOS/Android, including custom `ir-keytable` mappings and key event hooks. * 📁 **`Photos/`**: High-quality hardware builds, installation logs, and system diagrams. @@ -61,13 +61,20 @@ graph TD ## ⚙️ Detailed Module Breakdown -### 1. Steering Wheel Integration (`Arduino/LIN_to_IR`) -The **ATtiny84** intercepts steering wheel buttons from the Steering Wheel Module (SWM) on LIN frame `0x20`. These button actions are translated into bit-banged RC-6 Mode 6A (MCE Remote) signals and fed directly to the Raspberry Pi. +### 1. Steering & Screen Integration (`Arduino/LIN_to_IR`) + +The **ATtiny84** is the central hardware coordinator. It decodes steering wheel buttons from the LIN bus (`0x20`), controls the screen's extension motor via serial commands, and signals the Raspberry Pi using IR commands. + +#### Core Behaviors & Logic +* **Startup Extension**: The screen automatically opens upon startup (ignition on). +* **Manual Screen Toggle**: Pressing **BACK + ENTER** at the same time toggles the screen between fully open and fully closed states. +* **Manual Sleep Command**: Pressing **RIGHT + ENTER** at the same time sends a Volume Down IR command to put the Raspberry Pi to sleep. +* **Smart Power-down (Car-Off Detection)**: The ATtiny monitors LIN traffic. If no LIN communication is detected for **5 seconds**, the system automatically retracts the screen and sends a Volume Up IR command 3 times to trigger a clean Raspberry Pi OS shutdown. #### Button Map & Command Codes When buttons are pressed, the ATtiny transmits the corresponding Microsoft MCE remote scancodes: -| Button | LIN Frame Trigger | Active Button Code | MCE Scancode | Action on LineageOS | +| Input / Combo | LIN Frame Trigger | Active Button Code | MCE Scancode | Action on LineageOS / Hardware | | :--- | :--- | :--- | :--- | :--- | | **UP** | `d0 & 0x01` | `1` | `0x800f041e` | Navigate Up | | **DOWN** | `d0 & 0x02` | `2` | `0x800f041f` | Navigate Down | @@ -76,20 +83,18 @@ When buttons are pressed, the ATtiny transmits the corresponding Microsoft MCE r | **ENTER** | `d1 & 0x08` | `5` | `0x800f0422` | Select / OK | | **BACK** | `d1 & 0x01` | `6` | `0x800f0423` | Back / Exit | | **Volume Up (Shutdown)** | LIN Silent > 5s | `7` | `0x800f0410` | Trigger Soft Shutdown | - -#### Smart Power-down (Car-Off Detection) -To avoid battery drain, the system implements automated shutdown: -* The ATtiny monitors LIN traffic. Since the car's Central Electronic Module (CEM) continuously polls the LIN bus when the ignition is on, active communication indicates the vehicle is running. -* If no LIN serial data is detected for **5 seconds**, the ATtiny assumes the car is off, changes state, and sends the MCE code `0x800f0410` (`KEY_VOLUMEUP`) 3 times to guarantee receipt. -* Android intercepts this key and shuts down cleanly via the command line (`reboot -p`). +| **BACK + ENTER** | Both buttons held | `8` | *None (Local)* | Toggles Screen Motor (Open/Close) | +| **RIGHT + ENTER** | Both buttons held | `9` | `0x800f0411` | Send Sleep / Volume Down | --- -### 2. Retractable Screen Serial Driver (`Arduino/RTI_Control`) -The Volvo RTI screen requires continuous serial commands at **2400 baud** over a single communication line to stay awake and raised. The screen controller script performs this keep-alive task. +### 2. Retractable Screen Serial Driver -* **Display Modes**: Configurable via display mode hex codes (RGB: `0x40`, PAL: `0x45`, NTSC: `0x4C`, Screen Off/Retract: `0x46`). -* **Brightness Control**: Employs a 16-level hex sequence (`0x20` to `0x2F` / `0x6E`) to adjust the backlighting dynamically. +The Volvo RTI screen housing requires continuous serial packets at **2400 baud** over a single communication line to stay awake and raised. + +* **Display ON sequence**: `0x4C` (NTSC), `0x2F` (max brightness), `0x83` (execute). +* **Display OFF sequence**: `0x46` (OFF), `0x00` (min brightness), `0x83` (execute). +* **Keep-Alive Periodicity**: The ATtiny84 continuously writes these 3-byte command packets at a non-blocking 100ms interval (one byte every 100ms) to ensure smooth operation without blocking LIN bus reception. ---