#include #include #include "lin_frame.h" #define IR_ARDUINO_PIN PIN_PA0 // Digital pin 10 #define LED_PIN PIN_PA7 // Digital pin 3 #define RPI_POWER_PIN PIN_PA2 // GPIO 16 of the RPi is connected to PA2 of the ATtiny #define RX_PIN PIN_PB2 // Digital pin 2 #define TX_PIN PIN_PB0 // Digital pin 0 (Unused dummy pin or LIN TX) #define FAULT_PIN PIN_PB1 // Digital pin 1 #define CS_PIN PIN_PB3 // Digital pin 11 #define RTI_TX_PIN PIN_PA5 // Digital pin 5 #define SYN_FIELD 0x55 #define SWM_ID 0x20 SoftwareSerial LINBusSerial(RX_PIN, TX_PIN); void rti_write_byte(uint8_t data) { // Disable interrupts to ensure precise bit-banging timing uint8_t oldSREG = SREG; cli(); // Start bit (LOW) digitalWrite(RTI_TX_PIN, LOW); delayMicroseconds(417); // 1 / 2400 baud ≈ 416.67 us // 8 Data bits (LSB first) for (uint8_t i = 0; i < 8; i++) { if (data & (1 << i)) { digitalWrite(RTI_TX_PIN, HIGH); } else { digitalWrite(RTI_TX_PIN, LOW); } delayMicroseconds(417); } // Stop bit (HIGH) digitalWrite(RTI_TX_PIN, HIGH); delayMicroseconds(417); // Restore interrupts SREG = oldSREG; } // RC-6 timing constants // 1 time unit (1t) = 444us static const uint16_t RC6_T = 444; // Mark = LOW on wire (simulating TSOP receiving an IR burst) void sendMark(uint16_t us) { digitalWrite(IR_ARDUINO_PIN, LOW); delayMicroseconds(us); } // Space = HIGH on wire (simulating TSOP idle) void sendSpace(uint16_t us) { digitalWrite(IR_ARDUINO_PIN, HIGH); delayMicroseconds(us); } // Send a single RC-6 bit using Manchester encoding void sendRC6Bit(uint8_t bit, uint8_t width) { if (bit) { sendMark(RC6_T * width); sendSpace(RC6_T * width); } else { sendSpace(RC6_T * width); sendMark(RC6_T * width); } } // Send RC-6 Mode 6A (MCE) frame void sendRC6_MCE(uint32_t data, uint8_t toggle) { // Disable interrupts to ensure precise IR timing noInterrupts(); // Leader: 6t mark + 2t space sendMark(RC6_T * 6); sendSpace(RC6_T * 2); // Start bit: always 1 sendRC6Bit(1, 1); // Mode bits: 1, 1, 0 (mode 6) sendRC6Bit(1, 1); sendRC6Bit(1, 1); sendRC6Bit(0, 1); // Toggle bit (double width = 2t per half-bit) sendRC6Bit(toggle & 1, 2); // 32 data bits, MSB first for (int i = 31; i >= 0; i--) { sendRC6Bit((data >> i) & 1, 1); } // Final return to idle state (HIGH) sendSpace(1000); // Re-enable interrupts interrupts(); // Wait to let receiver process delay(40); } uint32_t get_mce_code(uint8_t button) { switch (button) { case 1: return 0x800f041e; // UP case 2: return 0x800f041f; // DOWN case 3: return 0x800f0420; // LEFT case 4: return 0x800f0421; // RIGHT 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; } } void send_ir_for_button(uint8_t button, uint8_t toggle) { uint32_t code = get_mce_code(button); if (code == 0) return; digitalWrite(LED_PIN, HIGH); // Turn debug LED ON sendRC6_MCE(code, toggle); digitalWrite(LED_PIN, LOW); // Turn debug LED OFF } byte b, n; LinFrame frame; unsigned long last_frame_time = 0; uint8_t current_button = 0; uint8_t toggle_bit = 0; 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; // RPi Power control variables and functions bool power_button_active = false; unsigned long power_button_start_time = 0; void trigger_power_button() { if (!power_button_active) { pinMode(RPI_POWER_PIN, OUTPUT); digitalWrite(RPI_POWER_PIN, LOW); power_button_active = true; power_button_start_time = millis(); } } void update_power_button() { if (power_button_active && (millis() - power_button_start_time >= 150)) { pinMode(RPI_POWER_PIN, INPUT); power_button_active = false; } } void process_button_state(uint8_t active_button) { unsigned long now = millis(); if (active_button != 0) { 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) { trigger_power_button(); } } } else { // 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; } void handle_frame() { if (frame.get_byte(0) != SWM_ID) return; if (!frame.isValid()) return; // Extract the data bytes // SWM button frame has 4 data bytes 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 (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); } void setup() { pinMode(IR_ARDUINO_PIN, OUTPUT); digitalWrite(IR_ARDUINO_PIN, HIGH); // Idle state (HIGH) pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, LOW); // LED OFF // Configure RPi power pin (High-Z input initially) pinMode(RPI_POWER_PIN, INPUT); // Configure RTI TX pin pinMode(RTI_TX_PIN, OUTPUT); digitalWrite(RTI_TX_PIN, HIGH); // Serial idle is HIGH // Enable MCP2004 pinMode(CS_PIN, OUTPUT); digitalWrite(CS_PIN, HIGH); pinMode(FAULT_PIN, OUTPUT); digitalWrite(FAULT_PIN, HIGH); LINBusSerial.begin(9600); // No timer initialization needed for bit-banging frame = LinFrame(); last_frame_time = millis(); last_lin_activity_time = millis(); last_rti_send_time = millis(); } void loop() { update_power_button(); if (LINBusSerial.available()) { 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(); if (b == SYN_FIELD && n > 2 && frame.get_byte(n - 1) == 0) { frame.pop_byte(); handle_frame(); frame.reset(); } else if (n == LinFrame::kMaxBytes) { frame.reset(); } else { frame.append_byte(b); } } // Timeout: if no LIN frames received for 200ms, assume no button is pressed if (millis() - last_frame_time > 200) { 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; if (screen_open) { uint8_t byte_to_send = 0; // 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; rti_write_byte(byte_to_send); rti_byte_index = (rti_byte_index + 1) % 3; } else { // Stay completely silent on serial when screen is closed to let it retract/close rti_byte_index = 0; } } }