#!/bin/bash
#
# lights.sh -- expose vehicle turn-signal state as marker files.
#
# Watches $291 on the body-control CAN bus and translates the
# turn-signal nibble (high nibble of byte 3) into one of four
# marker files inside $VEHICLE:
#
# no blinker active -> all blinker-* files removed
# left blinker -> $VEHICLE/blinker-left
# right blinker -> $VEHICLE/blinker-right
# hazards -> $VEHICLE/blinker-hazard
#
# Pattern: file-existence-as-flag. Other processes can poll
# `test -e $VEHICLE/blinker-left` to know whether the left turn
# signal is currently on, with zero IPC machinery. The price is
# you need a memory-backed filesystem (tmpfs) for $VEHICLE so
# repeated touch/rm don't wear out the SD card.
#
# Originally created: 01.2022 by jmccorm
# Last updated: 05.2026 (polish by magikh0e)
#
# USAGE
# ./lights.sh
#
# REQUIRES
# - can-utils (candump) apt install can-utils
# - $CANC interface up at the correct bitrate (125 kbps if it's
# really CAN-IHS, 500 kbps if it's CAN-C -- see BUS NOTE below)
# - $VEHICLE writable tmpfs strongly recommended
#
# BYTE LAYOUT ($291 byte 3 high nibble, observed on the JEEP platform)
#
# 0x_0 no blinker
# 0x_4 left blinker
# 0x_8 right blinker
# 0x_C hazards (both)
#
# Low nibble of byte 3 not yet decoded. Other bytes of $291
# carry running lights / cabin lights / dimmer / headlights --
# see in BMR for the
# full byte map.
#
# BUS NOTE
# The original jmccorm version of this script reads from `can1`
# and labels it CAN-C. redracer's backlight.sh (same $291 ID,
# different fields) reads from `can1` and labels it CAN-IHS.
# $291 is body-control state so its natural home is CAN-IHS,
# but the TIPM / CGW gateway re-broadcasts many CAN-IHS frames
# onto CAN-C for powertrain modules that need them -- which
# means $291 is observable on BOTH buses on most FCA platforms.
# The CANC= setting below is preserved from jmccorm's original
# script; flip it to your CAN-IHS interface if your bus
# wiring differs.
#
# CLEANUP DISCIPLINE
# Pattern A (explicit reset): on SIGINT / SIGTERM, all blinker
# marker files are removed. Reasoning: a stale blinker-left
# file after the script has died is misinformation -- consumers
# polling these files should see "no blinker" once the script
# stops monitoring, not the last-observed state frozen
# indefinitely. See In-Vehicle
# Event Handlers for the cleanup-discipline taxonomy.
#
# REVISION NOTES (2026-05-16)
# - Added edge filter: only act when byte-3 high nibble CHANGES.
# Original would re-touch the same file on every $291 frame
# (~10 Hz cadence) which is wasted I/O even on tmpfs.
# - Fixed multi-file bug: `touch blinker-right` without first
# removing `blinker-left` left both files present when the
# blinker switched directions. Each case branch now clears
# all blinker-* before touching the correct one.
# - Added SIGINT/SIGTERM trap (pattern A explicit reset).
# - Added outer reconnect loop so vehicle sleep doesn't end
# the script silently.
# - Pre-flight checks $CANC interface and $VEHICLE writability.
# Base directory for vehicle status.
# NOTE: A memory-based filesystem (tmpfs) is HIGHLY RECOMMENDED.
VEHICLE=/run/vehicle/lights
mkdir -p "$VEHICLE"
# Name for the CAN bus that carries $291. See BUS NOTE above; this
# defaults to jmccorm's original `can1`.
CANC=can1
DEBUG=false # Values: true, false, raw
CAN_ID=291
RETRY_DELAY=5
LAST_BLINKER="__init__"
CLEANED=0
cleanup() {
[ "$CLEANED" -eq 1 ] && return 0
CLEANED=1
rm -f "$VEHICLE"/blinker-* 2>/dev/null
exit 0
}
trap cleanup INT TERM
# Pre-flight.
if ! ip link show "$CANC" >/dev/null 2>&1; then
echo "ERROR: CAN interface $CANC not found" >&2
echo " Bring it up first, e.g.:" >&2
echo " ip link set $CANC up type can bitrate 125000" >&2
exit 1
fi
if ! touch "$VEHICLE/.write_test" 2>/dev/null; then
echo "ERROR: $VEHICLE is not writable" >&2
exit 1
fi
rm -f "$VEHICLE/.write_test"
# Outer reconnect loop. Vehicle sleep eventually silences the bus;
# without this loop the script exits silently when candump's pipe
# ends and doesn't come back when the vehicle wakes.
while true; do
candump -L "$CANC,0${CAN_ID}:0FFFF" 2>/dev/null \
| while read TIME BUS DATA; do
[ "$DEBUG" == "raw" ] && echo "CAN data: $DATA"
# candump -L line format: "(ts) bus ID#PAYLOAD"
# DATA arrives as "291#0011223344556677"; strip "291#" prefix.
PAYLOAD=${DATA:4}
# Byte 3 high nibble carries the blinker state.
# Payload char index 6 = first nibble of byte 3 (bytes are
# 0:byte0, 2:byte1, 4:byte2, 6:byte3, ...).
BLINKER="${PAYLOAD:6:1}"
# Edge filter: skip if the blinker state hasn't changed. $291
# broadcasts at ~10 Hz; without this we'd do filesystem ops on
# every frame.
[ "$BLINKER" == "$LAST_BLINKER" ] && continue
# Update the marker files atomically: clear all blinker-*
# first, then create the correct one for the new state. This
# is the fix for the original's left-to-right transition bug
# where both files could be present.
rm -f "$VEHICLE"/blinker-* 2>/dev/null
case "$BLINKER" in
"0") : ;; # no blinker
"4") touch "$VEHICLE/blinker-left" ;;
"8") touch "$VEHICLE/blinker-right" ;;
"C") touch "$VEHICLE/blinker-hazard" ;;
*)
[ "$DEBUG" == "true" ] && \
echo "lights.sh: unknown blinker nibble: $BLINKER" >&2
;;
esac
[ "$DEBUG" == "true" ] && echo "blinker -> $BLINKER"
LAST_BLINKER="$BLINKER"
done
echo "[reconnect] candump pipe ended; retrying in ${RETRY_DELAY}s" >&2
sleep "$RETRY_DELAY"
done