#!/bin/bash
#
# vpd_show - show the contents of Superhawk3 "VPD" a.k.a. "FRU data"
#
#   probably works fairly well for all Superhawk* and Centaur

VERBOSE=:

function vpd_read_w_addr {
  /fabos/share/vpdRead -offset $1 -size $2 | sed -e '/embeddedVpdRead/d'
}

function vpd_read {
  vpd_read_w_addr $1 $2 | sed -e 's/^....: //' -e 's/[[:blank:]]*$//'
}

function ascii_from_hex_1_byte {
  declare -i n=0x$1
  if [ $n -gt 31 -a $n -lt 127 ]
  then
    printf "%b" `printf "\\%o" $n`
  else
    printf "."
  fi
}

function ascii_from_hex_n_bytes {
  declare -i len=0x$1
  for ((i=0;i!=len;++i))
  do
    shift
    ascii_from_hex_1_byte $1
  done
}

function ascii_from_hex {
  for x
  do
    ascii_from_hex_1_byte $x
  done
}

function show_raw {
  vpd_read_w_addr 0 100 |
  while read ADR B0 B1 B2 B3 B4 B5 B6 B7 B8 B9 Ba Bb Bc Bd Be Bf
  do
    BYTES="$B0 $B1 $B2 $B3 $B4 $B5 $B6 $B7 $B8 $B9 $Ba $Bb $Bc $Bd $Be $Bf"
    ASCII=`ascii_from_hex $BYTES`
    echo "$ADR $BYTES $ASCII"
  done
}

function int_from_le {
  declare -i multiplier=1
  declare -i number=0
  while [ $# != 0 ]
  do
    declare -i digit=0x$1
    let "number += multiplier * digit"
    let "multiplier *= 256"
    shift
  done
  echo "$number"
}

function int_from_timestamp {
  declare -i n=$(int_from_le $1 $2 $3)
  printf "%d" $n
}

function date_from_timestamp {
  awk -v Dell_time=$(int_from_timestamp $1 $2 $3) 'BEGIN {
  tzero = mktime("1996 01 01 00 00 00")
  tmfg = tzero + 60 * Dell_time
  datestr = strftime("%a %b %d %R %Y", tmfg)
  print datestr
  }'
}

function show_ascii_bytes {
  printf "%s\t" "$1"
  shift
  printf "%s\n" "\"$(ascii_from_hex_n_bytes $*)\""
}

function show_ascii_vpd {
  printf "%s\t%s\n" "$1" "\"$(ascii_from_hex $(vpd_read $2 $3))\""
}

function show_dec_byte {
  declare -i number=0x$2
  printf "%s\t%d\n" "$1" $number
}

function show_dec_vpd {
  declare -i number=0x$(vpd_read $2 1)
  printf "%s\t%d\n" "$1" $number
}

function show_hex_byte {
  declare -i number=0x$2
  printf "%s\t%.2x\n" "$1" $number
}

function show_hex_vpd {
  declare -i number=0x$(vpd_read $2 1)
  printf "%s\t%.2x\n" "$1" $number
}

function show_le_dec_bytes {
  printf "%s\t" "$1"
  shift
  declare -i number=$(int_from_le $*)
  printf "%d\n" $number
}

function show_le_dec_vpd {
  declare -i number=$(int_from_le $(vpd_read $2 $3))
  printf "%s\t%d\n" "$1" $number
}

function hex {
  printf "%x" $1
}

function show_svctg {
  echo "== Service Tag element =="
  show_ascii_bytes "service tag" $*
  # assume only one tag
}

function show_cpld {
  echo "== CPLD Revision element =="
  declare -i revs=0x$1
  shift
  declare -i r
  for ((r = 0; r < revs; ++r))
  do
    printf "rev %d\t\t%s%s\n" $((r+1)) $1 $2
    shift 2
  done
}

function show_mac {
  echo "== System MAC Address element =="
  printf "address 1\t%s:%s:%s:%s:%s:%s\n" $1 $2 $3 $4 $5 $6
  printf "address 2\t%s:%s:%s:%s:%s:%s\n" $7 $8 $9 ${10} ${11} ${12}
}

function show_pcba {
  echo "== PCBA Test Record element =="
  show_dec_byte "count	" $1
}

function show_mgmt {
  echo "== Management Information element =="
  show_le_dec_bytes "max power" $1 $2
  case $3 in
  05) fabric_type="FC 4" ;;
  06) fabric_type="FC 8" ;;
  10) fabric_type="10GE-KR" ;;
  11) fabric_type="FC 16" ;;
  *)  fabric_type="?" ;;
  esac
  printf "fabric type\t%s (%s)\n" $3 "$fabric_type"
  show_dec_byte "fabric major" $4
  show_dec_byte "fabric minor" $5
  show_hex_byte "sw mgblty" $6
  show_hex_byte "hw mgblty" $7
  show_dec_byte "max boot time" $8
  show_dec_byte "link tuning" $9
}

function checksum_is_ok {
  declare -i sum=0
  for y
  do
  declare -i n=0x$y
    let "sum += n"
  done
  let "sum &= 0xff"
  return $sum
}

function show_element {
  # $1 = type, $2 $3 = address (little-endian)
  # address must be less than 256
  if [ "$3" != "00" ]
  then
    echo "element type $1: impossible element offset $3$2"
    return
  fi
  tt=$(vpd_read $2 1)
  if [ "$1" != "$tt" ]
  then
    echo "inconsistent element types $1 $tt at $2"
    return
  fi
  # element length must be in the range 8 .. 160 (not a very tight bound)
  declare -i addr=0x$2
  len_lsb=$(vpd_read $(hex $((addr+1))) 1)
  len_msb=$(vpd_read $(hex $((addr+2))) 1)
  chcksum=$(vpd_read $(hex $((addr+3))) 1)
  declare -i len=0x${len_msb}${len_lsb}
  if [ $len -gt 160 ] || [ $len -lt 8 ]
  then
    echo "element type $1 at $2: impossible element length $len"
    return
  fi
  $VERBOSE "== element type $1 starts at $2 =="
  # check the checksum
  attributes=$(vpd_read $(hex $((addr+4))) $(hex $((len-4))))
  if ! checksum_is_ok $tt $len_lsb $len_msb $chcksum $attributes
  then
    echo "element type $1 at $2: bad checksum"
  fi
  case $1 in
  04) show_svctg $attributes ;;
  0c) show_cpld $attributes ;;
  0d) show_mac $attributes ;;
  0e) show_pcba $attributes ;;
  0f) show_mgmt $attributes ;;
  *)  echo "== unrecognized element type $1 at $2 =="
      printf "\t\t%s\n" "$attributes"
      ;;
  esac
}

function show_board_serial_number {
  show_ascii_bytes "serial number" e $*
  show_ascii_bytes "  country" 2 $1 $2
  show_ascii_bytes "  manufacturer" 5 $3 $4 $5 $6 $7
  board_date=$(ascii_from_hex_n_bytes 3 $8 $9 ${10})
  board_year_byte=$(ascii_from_hex_1_byte $8)
  case $board_year_byte in
  0|1|2|3|4|5) board_year="201${board_year_byte}" ;;
  6|7|8|9)     board_year="200${board_year_byte}" ;;
  *)           board_year="?" ;;
  esac
  case $(ascii_from_hex_1_byte $9) in
  1) board_month="Jan" ;;
  2) board_month="Feb" ;;
  3) board_month="Mar" ;;
  4) board_month="Apr" ;;
  5) board_month="May" ;;
  6) board_month="Jun" ;;
  7) board_month="Jul" ;;
  8) board_month="Aug" ;;
  9) board_month="Sep" ;;
  A) board_month="Oct" ;;
  B) board_month="Nov" ;;
  C) board_month="Dec" ;;
  *) board_month="?" ;;
  esac
  declare -i board_day_hex=0x${10}
  declare -i board_day
  case $(ascii_from_hex_1_byte ${10}) in
  1|2|3|4|5|6|7|8|9)
    board_day=$((board_day_hex-0x30))
    ;;
  A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V)
    board_day=$((board_day_hex-0x41+10))
    ;;
  *)
    board_day=0 ;;
  esac
  printf "  date code\t%s (%s %s %d)\n" $board_date $board_year $board_month $board_day
  show_ascii_bytes "  sequence num" 4 ${11} ${12} ${13} ${14}
}

function show_fru {
  # common header
  if [ "$(vpd_read 0 8)" != "01 0a 00 01 00 00 00 f4" ]
  then
    echo "common header looks bad"
    show_raw
    exit 0
  fi
  # board area
  $VERBOSE "== Board Information Area starts at 08 =="
  echo "== Board Information Area =="
  if [ "$(vpd_read 8 3)" != "01 09 00" ]
  then
    echo "one or more of version, length, or language look bad"
    show_raw
    exit 0
  fi
  tstamp=$(vpd_read b 3)
  printf "mfg timestamp\t%d (%s)\n" $(int_from_timestamp $tstamp) "$(date_from_timestamp $tstamp)"
  manufacturer=$(vpd_read e 4)
  if [ "$manufacturer" = "83 64 c9 b2" ]
  then
    printf "manufacturer\tDELL\n"
  else
    echo "manufacturer	unknown"
    # we may need to add decode for 6-bit ASCII
  fi
  show_ascii_vpd "product name" 13 1e
  show_board_serial_number $(vpd_read 32 e)
  show_ascii_vpd "part number" 41 6
  show_ascii_vpd "hardware rev" 47 3
  file_id=$(vpd_read 4b 1)
  case $file_id in
  01) gen="9G" ;;
  02) gen="10G" ;;
  03) gen="11G" ;;
  04) gen="12G" ;;
  *)  gen="?" ;;
  esac
  printf "file ID\t\t%s (platform generation = %s)\n" $file_id $gen
  if ! checksum_is_ok $(vpd_read 8 48)
  then
    echo "bad checksum"
  fi
  # Internal Use Area
  $VERBOSE "== Internal Use Area starts at 50 =="
  echo "== Internal Use Area =="
  if [ "$(vpd_read 50 1)" != "01" ]
  then
    echo "IPMI version number looks bad"
    show_raw
    exit 0
  fi
  show_ascii_vpd "format	" 51 4
  show_hex_vpd "board p/n cksum" 55
  if ! checksum_is_ok $(vpd_read 41 9) $(vpd_read 55 1)
  then
    printf "\t\tbad checksum\n"
  fi
  show_dec_vpd "header rev" 58
  show_hex_vpd "feature flags" 59
  show_dec_vpd "units	" 5a
  show_le_dec_vpd "EEPROM size" 5b 2
  declare -i header_length=0x$(vpd_read 5d 1)
  # Elements
  declare -i int_use_base=0x50
  declare -i element_count=0x$(vpd_read 5f 1)
  declare -i element_base=0x60
  declare -i element_stride=3
  declare -i int_use_len=$((element_base-int_use_base+element_stride*element_count))
  let "int_use_len = 0xf8 & (int_use_len + 7)"
  if [ $element_count -gt 5 ]
  then
    printf "too many elements (%d)\n" $element_count
    exit 0
  fi
  if [ $header_length -ne $int_use_len ]
  then
    printf "inconsistent lengths (%d, %d)\n" $header_length $int_use_len
    exit 0
  fi
  if ! checksum_is_ok $(vpd_read $(hex $int_use_base) $(hex $int_use_len))
  then
    echo "bad checksum"
  fi
  printf "elements\t%d\n" $element_count
  $VERBOSE "== element list starts at 60 =="
  declare -i x
  for ((x=0; x < element_count; ++x))
  do
    element_addr=$(hex $((element_base+element_stride*x)))
    show_element $(vpd_read $element_addr $element_stride)
  done
}

SCRIPT=${0##*/}

function usage {
  printf "usage:\n"
  printf "%s -[f|r|h]\n" $SCRIPT
  printf "  -f\t\tformatted FRU data (default)\n"
  printf "  -v\t\tverbose, like -f plus addresses of blocks\n"
  printf "  -r\t\tRaw hex data\n"
  printf "  -h\t\tprint this usage message and exit\n"
  exit 1
}

if [ $# -eq 0 ]
then
  show_fru
else
  case $1 in
  -v) VERBOSE='echo -e'
      show_fru
      ;;
  -f) show_fru ;;
  -r) show_raw ;;
  -h) usage ;;
  *)  usage ;;
esac
fi
