|  | # Copyright lowRISC contributors. | 
|  | # Licensed under the Apache License, Version 2.0, see LICENSE for details. | 
|  | # SPDX-License-Identifier: Apache-2.0 | 
|  | # | 
|  | # Convert I2C host format to SVG | 
|  |  | 
|  | import logging as log | 
|  | from collections import namedtuple | 
|  |  | 
|  | import i2csvg.i2csvg_data as sdata | 
|  |  | 
|  |  | 
|  | # validating version of int(x, 0) | 
|  | # returns int value, error flag | 
|  | # if error flag is True value will be zero | 
|  | def check_int(x, err_prefix): | 
|  | if isinstance(x, int): | 
|  | return x, False | 
|  | if len(x) == 0: | 
|  | log.error(err_prefix + " Zero length string") | 
|  | return 0, True | 
|  | if x[0] == '0' and len(x) > 2: | 
|  | if x[1] in 'bB': | 
|  | validch = '01' | 
|  | elif x[1] in 'oO': | 
|  | validch = '01234567' | 
|  | elif x[1] in 'xX': | 
|  | validch = '0123456789abcdefABCDEF' | 
|  | else: | 
|  | log.error(err_prefix + | 
|  | ": int must start digit, 0b, 0B, 0o, 0O, 0x or 0X") | 
|  | return 0, True | 
|  | for c in x[2:]: | 
|  | if not c in validch: | 
|  | log.error(err_prefix + ": Bad character " + c + " in " + x) | 
|  | return 0, True | 
|  | else: | 
|  | if not x.isdecimal(): | 
|  | log.error(err_prefix + ": Number not valid int " + x) | 
|  | return 0, 1 | 
|  | return int(x, 0), False | 
|  |  | 
|  |  | 
|  | def check_single(line, char, fullline, err): | 
|  | ''' Check for character char in input line | 
|  | Return True if there is one (or more) | 
|  | Propagate err or force to err True if more than one char | 
|  | ''' | 
|  | res = False | 
|  | if char in line: | 
|  | res = True | 
|  | if line.count(char) > 1: | 
|  | log.warning('Multiple ' + char + ' in line ' + fullline) | 
|  | err = True | 
|  | return res, err | 
|  |  | 
|  |  | 
|  | I2cOp = namedtuple('I2cOp', | 
|  | 'read rcont start stop nackok mvalue adr size fbyte tag') | 
|  |  | 
|  |  | 
|  | def check_and_size(iic, line): | 
|  | ''' Check I2C Op for validity and return size in bits | 
|  | ''' | 
|  | err = False | 
|  | if iic.start and iic.read: | 
|  | log.error('Start and Read found in ' + line) | 
|  | err = True | 
|  | if iic.rcont and not iic.read: | 
|  | log.error('RCont without Read found in ' + line) | 
|  | err = True | 
|  |  | 
|  | # documentation says R+C and P is not permitted, but I think it | 
|  | # is needed for protocols where the last read data is ACKed? | 
|  | # (these don't match I2C or SMBus spec but exist in the wild) | 
|  |  | 
|  | size = 0 | 
|  | if iic.start: | 
|  | size += 1 | 
|  | if iic.stop: | 
|  | size += 1 | 
|  | if iic.read: | 
|  | # multi is D0, D1, ..., Dn-1 so 3 byte/acks and a 1 bit squiggle | 
|  | # regular read is one byte/ack per | 
|  | size += 9 * 3 + 1 if iic.mvalue else 9 * iic.fbyte | 
|  | else: | 
|  | # write data is one byte/ack | 
|  | size += 9 | 
|  | # rcont, nackok just affect the polarity of the final ack bit | 
|  | # adr just affects how the write data is drawn | 
|  | return size, err | 
|  |  | 
|  |  | 
|  | def parse_i2c_fifodata(line): | 
|  | ''' Parse input line of I2C FDATA fifo and convert to internal type | 
|  | Line is usually 0x + the hex value written to the register | 
|  | But could be binary (0b), octal (0o) or decimal | 
|  | ''' | 
|  | fifodata, err = check_int(line, 'FIFO value') | 
|  | # bit values here must match the register definition! | 
|  | ress = (fifodata & 0x0100) != 0 | 
|  | resp = (fifodata & 0x0200) != 0 | 
|  | resr = (fifodata & 0x0400) != 0 | 
|  | resc = (fifodata & 0x0800) != 0 | 
|  | resn = (fifodata & 0x1000) != 0 | 
|  | resb = fifodata & 0xff | 
|  | resm = False  # only used in descriptive case | 
|  | resa = False  # only used in descriptive case | 
|  |  | 
|  | tmpr = I2cOp(resr, resc, ress, resp, resn, resm, resa, 0, resb, None) | 
|  | size, serr = check_and_size(tmpr, line) | 
|  | if serr: | 
|  | err = True | 
|  | return I2cOp(resr, resc, ress, resp, resn, resm, resa, size, resb, | 
|  | None), err | 
|  |  | 
|  |  | 
|  | def parse_i2c_code(line): | 
|  | ''' Parse input line of I2C FDATA fifo and convert to internal type | 
|  | Line is coded with flags and an 8-bit data value | 
|  | S - Start flag, P - stop flag, | 
|  | R - read flag, C - continue read flag, N - NackOk flag | 
|  | followed by the data byte | 
|  | Special cases: | 
|  | M - indicates multiple bytes instead of data byte | 
|  | A - followed by 0 or 1 address/direction or 2 address/data | 
|  | Data value in quotes is a tag | 
|  | ''' | 
|  | resr, resc, ress, resp, resn = False, False, False, False, False | 
|  | resm, resa, resb = False, False, 0 | 
|  |  | 
|  | err = False | 
|  | firstval = 0 | 
|  | for i in line: | 
|  | if i.isdigit() or i == "'": | 
|  | break | 
|  | firstval += 1 | 
|  | # Will only check the flags section, so no concern about hex digits | 
|  | ress, err = check_single(line[:firstval], 'S', line, err) | 
|  | resp, err = check_single(line[:firstval], 'P', line, err) | 
|  | resr, err = check_single(line[:firstval], 'R', line, err) | 
|  | resc, err = check_single(line[:firstval], 'C', line, err) | 
|  | resn, err = check_single(line[:firstval], 'N', line, err) | 
|  | # these two are formally part of the value but parse like flags | 
|  | resm, err = check_single(line[:firstval], 'M', line, err) | 
|  | resa, err = check_single(line[:firstval], 'A', line, err) | 
|  |  | 
|  | if firstval == len(line): | 
|  | if not resm: | 
|  | err = True | 
|  | log.error('No value found in ' + line) | 
|  | rest = None | 
|  | else: | 
|  | if resm: | 
|  | err = True | 
|  | log.error('Found M and value in ' + line) | 
|  | rest = None | 
|  | resb = 0 | 
|  | elif line[firstval] == "'": | 
|  | rest = line[firstval + 1:-1] | 
|  | resb = 0 | 
|  | else: | 
|  | rest = None | 
|  | resb, verr = check_int(line[firstval:], | 
|  | 'Value in ' + line + ' ' + str(firstval)) | 
|  | if verr: | 
|  | err = True | 
|  | if resb < 0 or resb > 255 or (resa and resb > 2): | 
|  | log.error('Value out of range in ' + line) | 
|  | resb = 0 | 
|  | err = True | 
|  |  | 
|  | tmpr = I2cOp(resr, resc, ress, resp, resn, resm, resa, 0, resb, rest) | 
|  | size, serr = check_and_size(tmpr, line) | 
|  | if serr: | 
|  | err = True | 
|  | return I2cOp(resr, resc, ress, resp, resn, resm, resa, size, resb, | 
|  | rest), err | 
|  |  | 
|  |  | 
|  | def parse_file(infile, fifodata=False, prefix=None): | 
|  | ''' Parse a file of I2C data | 
|  | fifodata indicates if the data is a dump from writes to FDATA fifo | 
|  | prefix is a prefix on valid lines and will be stripped | 
|  | lines without the prefix are ignored | 
|  | Returns list of I2cOps or str (for titles) | 
|  | ''' | 
|  | transaction = [] | 
|  | errors = 0 | 
|  | firstline = True | 
|  |  | 
|  | for line in infile: | 
|  | if prefix: | 
|  | if not line.startswith(prefix): | 
|  | continue | 
|  | line = line[len(prefix):] | 
|  |  | 
|  | if len(line) == 0 or line.isspace() or line[0] == '#': | 
|  | continue | 
|  | line = line.lstrip().rstrip() | 
|  | if line[0] == 'T': | 
|  | transaction.append(line[1:].lstrip()) | 
|  | continue | 
|  | schar = ',' | 
|  | if fifodata and not ',' in line: | 
|  | # fifodata could also be whitespace spearated | 
|  | schar = None | 
|  |  | 
|  | for sline in line.split(sep=schar): | 
|  | if fifodata: | 
|  | t, err = parse_i2c_fifodata(sline) | 
|  | else: | 
|  | t, err = parse_i2c_code(sline) | 
|  | if err: | 
|  | errors += 1 | 
|  | else: | 
|  | transaction.append(t) | 
|  | if errors > 0: | 
|  | log.error('Found ' + str(errors) + ' errors in input') | 
|  | return transaction | 
|  |  | 
|  |  | 
|  | def output_debug(outfile, t, term): | 
|  | for tr in t: | 
|  | outfile.write(str(tr) + term) | 
|  |  | 
|  |  | 
|  | def text_element(tr, term, titles): | 
|  | if isinstance(tr, str): | 
|  | if titles: | 
|  | return 'T ' + tr + term | 
|  | return '' | 
|  | flags = 'S' if tr.start else '.' | 
|  | flags += 'P' if tr.stop else '.' | 
|  | flags += 'R' if tr.read else '.' | 
|  | flags += 'C' if tr.rcont else '.' | 
|  | flags += 'N' if tr.nackok else '.' | 
|  |  | 
|  | # mvalue and adr are only for drawing, but can propagate in value | 
|  | if tr.adr: | 
|  | val = 'A' + str(tr.fbyte) | 
|  | else: | 
|  | if tr.tag: | 
|  | val = "'" + tr.tag + "'" | 
|  | else: | 
|  | val = 'M' if tr.mvalue else hex(tr.fbyte) | 
|  | return flags + ' ' + val + term | 
|  |  | 
|  |  | 
|  | def output_text(outfile, transactions, term, titles=True): | 
|  | for tr in transactions: | 
|  | text = text_element(tr, term, titles) | 
|  | if text: | 
|  | outfile.write(text) | 
|  |  | 
|  |  | 
|  | # use will place a defined group at the given x,y | 
|  | def svg_use(item, x, y): | 
|  | return '    <use href="#' + item + '" x="' + str(x) + \ | 
|  | '" y="' + str(y) + '" />\n' | 
|  |  | 
|  |  | 
|  | # a byte write is a byte of data from the host and an ack from the device | 
|  | def svg_wrbyte(xpos, ypos, nok, label): | 
|  | rtext = svg_use('hbyte', xpos, ypos) | 
|  | rtext += '    <text x="' + str(xpos + (sdata.bytew / 2)) | 
|  | rtext += '" y="' + str(ypos + sdata.txty) + '">\n' | 
|  | rtext += label | 
|  | rtext += '</text>\n' | 
|  | xpos += sdata.bytew | 
|  | if nok: | 
|  | rtext += svg_use('norackd', xpos, ypos) | 
|  | else: | 
|  | rtext += svg_use('ackd', xpos, ypos) | 
|  | xpos += sdata.bitw | 
|  | return rtext, xpos | 
|  |  | 
|  |  | 
|  | # a byte read is a byte of data from the device and an ack/nack from the host | 
|  | def svg_rdbyte(xpos, ypos, ack, label): | 
|  | rtext = svg_use('dbyte', xpos, ypos) | 
|  | rtext += '    <text x="' + str(xpos + (sdata.bytew / 2)) | 
|  | rtext += '" y="' + str(ypos + sdata.txty) + '">\n' | 
|  | rtext += label | 
|  | rtext += '</text>\n' | 
|  | xpos += sdata.bytew | 
|  | rtext += svg_use(ack, xpos, ypos) | 
|  | xpos += sdata.bitw | 
|  | return rtext, xpos | 
|  |  | 
|  |  | 
|  | def svg_element(tr, xpos, ypos): | 
|  | etext = '' | 
|  | if tr.start: | 
|  | etext += svg_use('start', xpos, ypos) | 
|  | xpos += sdata.bitw | 
|  |  | 
|  | if tr.read and not tr.mvalue: | 
|  | for n in range(0, 1 if tr.tag else tr.fbyte): | 
|  | acktype = 'ackh' if (n < tr.fbyte - 1) or tr.rcont else 'nackh' | 
|  | t, xpos = svg_rdbyte(xpos, ypos, acktype, | 
|  | tr.tag if tr.tag else 'D' + str(n + 1)) | 
|  | etext += t | 
|  | if xpos > sdata.wrap and (n < tr.fbyte - 1): | 
|  | xpos = sdata.cindent | 
|  | ypos += sdata.linesep | 
|  | elif tr.read and tr.mvalue: | 
|  | # need space to draw three byte+ack and a break squiggle | 
|  | if (xpos + (sdata.bytew + sdata.bitw) * 3 + sdata.bitw) > sdata.wrap: | 
|  | xpos = sdata.cindent | 
|  | ypos += sdata.linesep | 
|  | t, xpos = svg_rdbyte(xpos, ypos, 'ackh', 'Data1') | 
|  | etext += t | 
|  | t, xpos = svg_rdbyte(xpos, ypos, 'ackh', 'Data2') | 
|  | etext += t | 
|  | etext += svg_use('skip', xpos, ypos) | 
|  | xpos += sdata.bitw | 
|  | t, xpos = svg_rdbyte(xpos, ypos, 'nackh', 'DataN') | 
|  | etext += t | 
|  |  | 
|  | elif tr.adr: | 
|  | etext += svg_use('adr' + str(tr.fbyte), xpos, ypos) | 
|  | xpos += sdata.bytew | 
|  | etext += svg_use('ackd', xpos, ypos) | 
|  | xpos += sdata.bitw | 
|  | elif tr.mvalue: | 
|  | # need space to draw three byte+ack and a break squiggle | 
|  | if (xpos + (sdata.bytew + sdata.bitw) * 3 + sdata.bitw) > sdata.wrap: | 
|  | xpos = sdata.cindent | 
|  | ypos += sdata.linesep | 
|  | t, xpos = svg_wrbyte(xpos, ypos, tr.nackok, 'Data1') | 
|  | etext += t | 
|  | t, xpos = svg_wrbyte(xpos, ypos, tr.nackok, 'Data2') | 
|  | etext += t | 
|  | etext += svg_use('skip', xpos, ypos) | 
|  | xpos += sdata.bitw | 
|  | t, xpos = svg_wrbyte(xpos, ypos, tr.nackok, 'DataN') | 
|  | etext += t | 
|  |  | 
|  | elif tr.start:  # and not tr.adr by position in elif | 
|  | etext += svg_use('adr' + str(tr.fbyte & 1), xpos, ypos) | 
|  | etext += '    <text x="' + str(xpos + 115) | 
|  | etext += '" y="' + str(ypos + sdata.txty) + '">' + hex(tr.fbyte >> 1) | 
|  | etext += '</text>\n' | 
|  | xpos += sdata.bytew | 
|  | etext += svg_use('ackd', xpos, ypos) | 
|  | xpos += sdata.bitw | 
|  |  | 
|  | else: | 
|  | t, xpos = svg_wrbyte(xpos, ypos, tr.nackok, | 
|  | tr.tag if tr.tag else hex(tr.fbyte)) | 
|  | etext += t | 
|  |  | 
|  | if tr.stop: | 
|  | etext += svg_use('pstop', xpos, ypos) | 
|  | xpos += sdata.bitw | 
|  |  | 
|  | return etext, xpos, ypos | 
|  |  | 
|  |  | 
|  | # since they are referenced by href name the style and defs only | 
|  | # go in the first svg in a file | 
|  | first_svg = True | 
|  |  | 
|  |  | 
|  | def out_svg(outfile, svg, ypos, svgtext): | 
|  | global first_svg | 
|  | outfile.write('<svg\n' + sdata.svgtag_consts) | 
|  | outfile.write('viewBox="0 0 ' + str(sdata.svgw) + ' ' + | 
|  | str(ypos + sdata.linesep + 8) + '">\n') | 
|  | if (first_svg): | 
|  | outfile.write(sdata.svgstyle + sdata.svg_defs) | 
|  | first_svg = False | 
|  | outfile.write(svg) | 
|  | if svgtext: | 
|  | outfile.write('<text x="10" y="' + str(ypos + sdata.linesep + 3)) | 
|  | outfile.write('" class="tt">' + svgtext[:-2] + '</text>\n') | 
|  | outfile.write('</svg>\n') | 
|  |  | 
|  |  | 
|  | def output_svg(outfile, transactions, title): | 
|  | xpos = 0 | 
|  | ypos = 0 | 
|  | svg = '' | 
|  | svgtext = '' | 
|  | for tr in transactions: | 
|  | if isinstance(tr, str): | 
|  | if svg: | 
|  | out_svg(outfile, svg, ypos, svgtext) | 
|  | if title: | 
|  | outfile.write('<h2>' + tr + '</h2>\n') | 
|  | xpos = 0 | 
|  | ypos = 0 | 
|  | svg = '' | 
|  | svgtext = '' | 
|  | continue | 
|  | if xpos > sdata.wrap: | 
|  | xpos = sdata.cindent | 
|  | ypos += sdata.linesep | 
|  | trsvg, xpos, ypos = svg_element(tr, xpos, ypos) | 
|  | svgtext += text_element(tr, ', ', False) | 
|  | svg += trsvg | 
|  |  | 
|  | out_svg(outfile, svg, ypos, svgtext) |