390 lines
11 KiB
C++
390 lines
11 KiB
C++
#include <Arduino.h>
|
|
#include <SoftwareSerial.h>
|
|
#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
|
|
uint8_t rti_close_bytes_left = 0;
|
|
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) {
|
|
digitalWrite(LED_PIN, HIGH); // Turn debug LED ON
|
|
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);
|
|
digitalWrite(LED_PIN, LOW); // Turn debug LED OFF
|
|
power_button_active = false;
|
|
}
|
|
}
|
|
|
|
void set_screen_state(bool open) {
|
|
if (screen_open != open) {
|
|
screen_open = open;
|
|
rti_byte_index = 0; // Reset byte index to send new sequence immediately
|
|
if (!screen_open) {
|
|
rti_close_bytes_left = 30; // Send the OFF packet (3 bytes) 10 times
|
|
trigger_power_button();
|
|
} else {
|
|
trigger_power_button();
|
|
}
|
|
}
|
|
}
|
|
|
|
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) {
|
|
set_screen_state(!screen_open);
|
|
} 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;
|
|
set_screen_state(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;
|
|
set_screen_state(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 {
|
|
if (rti_close_bytes_left > 0) {
|
|
uint8_t byte_to_send = 0;
|
|
// OFF sequence: 0x4F (OFF), 0x20 (standard brightness), 0x83 (execute)
|
|
if (rti_byte_index == 0) byte_to_send = 0x4F;
|
|
else if (rti_byte_index == 1) byte_to_send = 0x20;
|
|
else byte_to_send = 0x83;
|
|
|
|
rti_write_byte(byte_to_send);
|
|
rti_byte_index = (rti_byte_index + 1) % 3;
|
|
rti_close_bytes_left--;
|
|
} else {
|
|
// Stay completely silent on serial when screen is closed to let it retract/close
|
|
rti_byte_index = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|