#!/usr/bin/python3
# Display Jeep data from canbus live 

# import libraries
import can
import curses

canFilter = list()

# can bus variables, change these for vcan or can
canIHS = "vcan0"
canC = "vcan1"

# defined types to process the data. x = can message , a = byte 1 , b = byte 2
def raw8(x,a):
    return(x[a])

def raw16(x,a,b):
    return((x[a]<<8) + x[b])

def volt(x,a):
    return(x[a] / 10)

def temp(x,a):
    return(round((((x[a] - 40) * (9 / 5)) + 32)))

def tilt(x,a,b):
    return(round(((x[a]<<8) + x[b] - 2048) / 10))

def rpm(x,a,b):
    if x[a] == 0xFF:
        return(0)
    return((x[a]<<8) +  x[b])

def mph(x,a,b):
    return(round(((x[a]<<8) + x[b]) / 200,1))

def psi(x,a):
    return(round(((x[a] * 4) * 0.145038)))

def gear(x,a):
    if x[a] == 0x4E:
        return('N')
    elif x[a] == 0x52:
        return('R')
    elif x[a] == 0x31:
        return('1')
    elif x[a] == 0x32:
        return('2')
    elif x[a] == 0x33:
        return('3')
    elif x[a] == 0x34:
        return('4')
    elif x[a] == 0x35:
        return('5')
    elif x[a] == 0x36:
        return('6')
    elif x[a] == 0x37:
        return('7')
    elif x[a] == 0x38:
        return('8')
    elif x[a] == 0x50:
        return('P')
    elif x[a] == 0x44:
        return('D')

def xfer(x,a):
    if x[a] == 0x00:
        return('4x2H')
    elif x[a] == 0x02:
        return('N')
    elif x[a] == 0x10:
        return('4x4H')
    elif x[a] == 0x20:
        return('N')
    elif x[a] == 0x40:
        return('4x4L')
    elif x[a] == 0x80:
        return('Shifting')

def steer(x,a,b):
    return(((x[a]<<8) + x[b]) - 0x1000)

def wrapper(msg,name,func,output,*args):
    return(func(msg,*args))

def pstemp(x,a):
    return(round(((x[a] * (9 / 5)) + 32)))

# ----------------------------------------------------------------------
# Candidate helper functions for unverified fields. None of these are
# referenced by the active monitorlist below -- they sit here ready to
# wire in once you've confirmed an ID + byte offset on YOUR vehicle.
# See: https://magikh0e.pl/pubCarHacking/bus-message-reference.html#candidate-ids
# for the candidate ID table (HIGH / MEDIUM / LOW confidence groupings).
# ----------------------------------------------------------------------

def fuel(x,a):
    # Fuel level percentage. Most FCA platforms broadcast 0..255 across
    # empty-to-full, so divide by 2.55 for a 0..100% read. Some platforms
    # use 0..100 directly -- if your readings look halved, drop the / 2.55.
    return(round(x[a] / 2.55))

def pct(x,a):
    # Generic 0..100% from a 0..255 byte. Throttle position, accelerator
    # pedal, EGR position, anything similar.
    return(round(x[a] / 2.55))

def amb(x,a):
    # Outside ambient temperature. Broadcast in Celsius with a +40 offset
    # on most platforms; some use 0.5 degree resolution (byte / 2 - 40).
    # Returns Fahrenheit. Adjust if your reading is half what you expect.
    return(round((((x[a] / 2) - 40) * 9 / 5) + 32))

def bit(x,a,mask):
    # Test a single bit in byte `a`. mask is the bit value (1, 2, 4, 8,
    # 0x10, 0x20, 0x40, 0x80). Returns 'Y' or 'N' for terminal display.
    # Use for brake switch, seatbelt, door open, light state, etc.
    return('Y' if (x[a] & mask) else 'N')

def tpms_psi(x,a):
    # TPMS tire pressure. Some FCA platforms encode as byte / 4 for PSI
    # directly; others store kPa and need conversion. The /4 path matches
    # the JL Wrangler convention. Returns PSI as integer.
    return(round(x[a] / 4))

def raw32(x,a,b,c,d):
    # 32-bit big-endian read. For odometer (units typically 0.1 mile,
    # so divide by 10 after this), fuel-trim totalizers, etc.
    return((x[a] << 24) + (x[b] << 16) + (x[c] << 8) + x[d])

def odo(x,a,b,c,d):
    # Odometer: 32-bit big-endian, units 0.1 mile on most FCA platforms.
    # Returns miles as integer. Verify the byte order on your vehicle --
    # some platforms are little-endian or use km.
    return(round(raw32(x,a,b,c,d) / 10))

# list of can ID's and details to monitor in this order:
# (ID, Channel, [("name", process, type, byte1, byte2)])
monitorlist=[(0x2C2,
              canIHS,
              [("Batt V",volt,str,2),
               ("Batt ?",raw8,hex,0)]),
             (0x02B,
              canC,
              [("Roll",tilt,str,0,1),
               ("Tilt",tilt,str,2,3),
               ("Yaw",tilt,str,4,5)]),
             (0x322,
              canIHS,
              [("RPM",rpm,str,0,1),
               ("MPH",mph,str,2,3)]),
             (0x127,
              canC,
              [("IAT",temp,str,0),
               ("Coolant",temp,str,1)]),
             (0x13D,
              canC,
              [("Oil Temp",temp,str,3),
               ("Oil Pres",psi,str,2)]),
             (0x093,
              canC,
              [("Gear",gear,str,2)]),
             (0x277,
              canC,
              [("Transfer",xfer,str,0)]),
             (0x023,
              canC,
              [("Steer Angl",steer,str,0,1),
               ("Rate",steer,str,2,3)]),
             (0x128,
              canC,
              [("PS Temp",pstemp,str,1),
               ("PS PSI",psi,str,2)])
              ]

# ----------------------------------------------------------------------
# Candidate entries (UNVERIFIED). Uncomment after confirming the ID +
# byte offset on YOUR vehicle with candump. IDs vary by model year /
# platform -- the values below are starting points from community RE
# work (notably the JL Wrangler spreadsheet), not authoritative facts.
#
# To enable: move the entry from this comment block into monitorlist
# above, between the closing ] of the last entry and the trailing ].
# ----------------------------------------------------------------------
#
# monitorlist += [
#
#     # ---- HIGH confidence: fields broadcast on every modern FCA platform
#     #                       (IDs may differ from these on your year/model)
#
#     (0x308, canIHS, [("Fuel %", fuel, str, 0)]),
#     (0x29C, canIHS, [("Outside F", amb, str, 0)]),
#     (0x129, canC,   [("Throttle", pct, str, 2)]),
#     (0x2D0, canIHS, [("Cruise",   raw8, str, 0)]),
#
#     # ---- MEDIUM confidence: documented on JL Wrangler, verify on yours
#
#     (0x371, canIHS, [("TPMS FL",  tpms_psi, str, 0),
#                      ("TPMS FR",  tpms_psi, str, 1),
#                      ("TPMS RL",  tpms_psi, str, 2),
#                      ("TPMS RR",  tpms_psi, str, 3)]),
#
#     (0x2A8, canIHS, [("Door FL",  bit, str, 0, 0x01),
#                      ("Door FR",  bit, str, 0, 0x02),
#                      ("Door RL",  bit, str, 0, 0x04),
#                      ("Door RR",  bit, str, 0, 0x08)]),
#
#     (0x214, canIHS, [("Hi-beam",  bit, str, 0, 0x01),
#                      ("Lo-beam",  bit, str, 0, 0x02),
#                      ("Turn L",   bit, str, 0, 0x10),
#                      ("Turn R",   bit, str, 0, 0x20)]),
#
#     (0x3F1, canIHS, [("HVAC fan", raw8, str, 0)]),
#     (0x1F1, canIHS, [("Belt drv", bit, str, 0, 0x01),
#                      ("Belt pax", bit, str, 0, 0x02)]),
#
#     (0x3E0, canIHS, [("Odo mi",   odo, str, 0, 1, 2, 3)]),
#
#     # ---- Brake switch: easiest field to discover/verify yourself
#     # Diff candump output with brake pressed vs released; the toggling
#     # bit in a 100ms-cadence message is your target. Common candidates
#     # on FCA: $122 (we already read this for ignition), $20B, $2D9.
#
#     # (0x20B, canIHS, [("Brake",   bit, str, 0, 0x10)]),
#
# ]
# ----------------------------------------------------------------------

stdscr = curses.initscr()

# setup
for monitor in monitorlist:
 # build out the can bus filtering list. only receive messages that we care about.
 canFilter.append({"can_id": monitor[0], "can_mask": 0xFFF, "can_channel": monitor[1]})
 for detail in monitor[2]:
  # place item titles, according to how they appear in the list
  stdscr.addstr(monitorlist.index(monitor),(monitor[2].index(detail) * 30),detail[0])

stdscr.refresh()

# define the can bus
bus = can.interface.Bus('', bustype='socketcan', filter=canFilter)

# Process every single message received from the canbus
try:
 for msg in bus:
  for monitor in monitorlist:
   if msg.arbitration_id == monitor[0] and msg.channel == monitor[1]:
    for detail in monitor[2]:
     stdscr.addstr(monitorlist.index(monitor),((monitor[2].index(detail) * 30) + 15), '%-5s' % detail[2](wrapper((msg.data),*detail)))
     stdscr.refresh()

except:
  bus.shutdown()
  curses.nocbreak()
  stdscr.keypad(0)
  curses.echo()
  curses.endwin()
  raise
