Miscellaneous tools for IBM PC emulation development
#    Copyright 2022-2023 Daniel Balsom
#    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#    Permission is hereby granted, free of charge, to any person obtaining a
#    copy of this software and associated documentation files (the “Software”),
#    to deal in the Software without restriction, including without limitation
#    the rights to use, copy, modify, merge, publish, distribute, sublicense,
#    and/or sell copies of the Software, and to permit persons to whom the
#    Software is furnished to do so, subject to the following conditions:
#
#    The above copyright notice and this permission notice shall be included in
#    all copies or substantial portions of the Software.
#
#    THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER   
#    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
#    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
#    DEALINGS IN THE SOFTWARE.
#    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

#   count_rows.py
#   Displays the number of rows in the supplied csv file.

import pandas as pd
import sys

def count_rows(csv_filename, chunksize=10000):
    """
    Count the number of rows in a csv file.

    :param csv_filename: The name of the csv file.
    :param chunksize: The number of rows to read at a time. Default is 10000.
    :return: The total number of rows in the csv file.
    """
    row_count = 0
    # Use the 'chunksize' parameter to read the file in chunks
    for chunk in pd.read_csv(csv_filename, chunksize=chunksize):
        row_count += len(chunk)
    return row_count

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print("Usage: python count_rows.py <csv_filename>")
        sys.exit(1)

    csv_file = sys.argv[1]
    num_rows = count_rows(csv_file)
    print(f"The number of rows in {csv_file} is: {num_rows}")
bus_sniffer/count_rows.py
dbalsom/marty_tools
#    Copyright 2022-2023 Daniel Balsom
#    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#    Permission is hereby granted, free of charge, to any person obtaining a
#    copy of this software and associated documentation files (the “Software”),
#    to deal in the Software without restriction, including without limitation
#    the rights to use, copy, modify, merge, publish, distribute, sublicense,
#    and/or sell copies of the Software, and to permit persons to whom the
#    Software is furnished to do so, subject to the following conditions:
#
#    The above copyright notice and this permission notice shall be included in
#    all copies or substantial portions of the Software.
#
#    THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER   
#    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
#    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
#    DEALINGS IN THE SOFTWARE.
#    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

#   Decode.py 
#
#   Decode a CSV file produced by PulseView/DSView into a cycle trace log.
#   Before decoding, the cycles should be extracted via 'export_cycles.py'
#   
#   The input file should have the following columns:
#   Time(s),
#   AD0,AD1,AD2,AD3,AD4,AD5,AD6,AD7,
#   A8,A9,A10,A11,A12,A13,A14,A15,A16,A17,A18,A19,
#   CLK,READY,QS0,QS1,S0,S1,S2
#   
#   Command Line Arguments:
#   input_csv output_csv

import pandas as pd
import sys

from enum import Enum, auto
from iced_x86 import Decoder, Formatter, FormatterSyntax

CLOCK_DIVISOR = 3

BUS_MAPPING = {
    0: 'INTA',
    1: 'IOR',
    2: 'IOW',
    3: 'HALT',
    4: 'CODE',
    5: 'MEMR',
    6: 'MEMW',
    7: 'PASV'
}

Q_MAPPING = {
    '00': '.',
    '01': 'F',
    '10': 'E',
    '11': 'S'
}

SEG_MAPPING = {
    '00': 'ES',
    '01': 'SS',
    '10': 'CS',
    '11': 'DS'
}

ADDRESS_COLS = ['AD0', 'AD1', 'AD2', 'AD3', 'AD4', 'AD5', 'AD6', 'AD7', 'A8', 'A9', 'A10', 'A11', 'A12', 'A13', 'A14', 'A15', 'A16', 'A17', 'A18', 'A19']
DATA_COLS = ['AD0', 'AD1', 'AD2', 'AD3', 'AD4', 'AD5', 'AD6', 'AD7']

INSTR_PREFIXES = {0x26, 0x2E, 0x36, 0x3E, 0xF0, 0xF1, 0xF2, 0xF3}

class State(Enum):
    TI = auto()
    T1 = auto()
    T2 = auto()
    T3 = auto()
    Tw = auto()
    T4 = auto()

def decode_address(row):
    # Construct binary representation by joining values of AD columns
    binary_str = ''.join(map(str, [int(row[col]) for col in reversed(ADDRESS_COLS)]))

    # Convert the binary string to an integer
    address_value = int(binary_str, 2)

    # Convert the integer to hexadecimal and pad to 5 digits
    return "'" + format(address_value, '05X')

def decode_data(row):
    # Construct binary representation by joining values of AD columns
    binary_str = ''.join(map(str, [int(row[col]) for col in reversed(DATA_COLS)]))
    # Convert the binary string to an integer
    data_value = int(binary_str, 2)

    # Convert the integer to hexadecimal and pad to 5 digits
    return "'" + format(data_value, '02X')

def decode_status(row):
    # Convert 3-bit value to decimal
    return row['S2']*4 + row['S1']*2 + row['S0']

def add_ale_column(df):
    condition = (df['B'].shift(1) == 7) & (df['B'] != 7)
    df['ALE'] = '.'
    df.loc[condition, 'ALE'] = 'A'
    return df

def add_ale_and_al_columns(df):
    # Condition for ALE
    condition = (df['B'].shift(1) == 7) & (df['B'] != 7)
    
    # Default columns
    df['ALE'] = '.'
    df['AL'] = None
    
    # Set ALE where condition is true
    df.loc[condition, 'ALE'] = 'A'
    
    # Now, for AL column
    last_latched_address = None
    for index, row in df.iterrows():
        if row['ALE'] == 'A':
            last_latched_address = row['ADDR']
        
        df.at[index, 'AL'] = last_latched_address

    return df
    
def add_t_and_d_column(df):
    state = State.TI
    next_state = None 
    do_wait_state = False
    
    df['PREV_R'] = df['READY'].shift(1)
    
    for index, row in df.iterrows():
        
        data_bus_valid = False
    
        if next_state is not None:
            state = next_state
            next_state = None
        elif row['ALE'] == 'A':
            state = State.T1
        elif state == State.T1:
            state = State.T2
        elif state == State.T2:
            if row['READY'] == 1:
                data_bus_valid = True
            state = State.T3
        elif state == State.T3:
            if row['PREV_R'] == 0 or row['READY'] == 0:
                state = State.Tw
            else:
                state = State.T4
        elif state == State.Tw:
            if row['PREV_R'] == 1:
                data_bus_valid = True
                state = State.T4
            else:
                state = State.Tw
        elif state == State.T4:
            state = State.TI

        if data_bus_valid:
            df.at[index, 'D'] = decode_data(row)

        # Set the T column for the current row
        df.at[index, 'T'] = state.name

    # Drop the temporary next_QOP column after use 
    df = df.drop(columns=['PREV_R'])
        
    return df
    
def add_qop_column(df):
    # Apply the mapping for the QOP column
    df['QOP'] = df.apply(lambda row: Q_MAPPING[f"{int(row['QS1'])}{int(row['QS0'])}"], axis=1)
    
    return df    
    
def add_seg_column(df):
    
    for index, row in df.iterrows():
        # If 'ALE' signal is low, we can decode the segment status from S3-S4 (A16-A17)
        if row['ALE'] != 'A' and row['T'] != 'TI':
            # Apply the mapping for the SEG column.
            df.at[index, 'SEG'] = SEG_MAPPING[f"{int(row['A17'])}{int(row['A16'])}"]

    return df            

def add_busl_column(df):
    # Initialize a variable to hold the latched value of 'BUS'
    latched_value = ""
    # Loop through each row in the DataFrame
    for index, row in df.iterrows():
        # If 'ALE' is 'A', update the latched value
        if row['ALE'] == 'A':
            latched_value = row['BUS']
        elif row['T'] == 'TI':
            latched_value = 'PASV'
            
        # Assign the latched value to the 'BUSL' column
        df.at[index, 'BUSL'] = latched_value

    return df    
    
def add_time_delta(df):
    """Add time delta in nanoseconds to the DataFrame."""

    df['ns_d'] = df['Time(s)'].diff().fillna(0) * 1e9
    df.at[0, 'ns_d'] = 0
    df['ns_d'] = df['ns_d'].astype(int)

    return df

def calculate_d_accum(df):
    # Create a column that indicates a change in the 'CLK' value
    df['CLK_change'] = df['CLK'].diff() != 0

    # Shift the CLK_change by one row so that accumulation continues until the row where the change occurs
    df['group'] = df['CLK_change'].shift(fill_value=False).cumsum()

    # Create an accumulator which resets based on the shifted 'group' values
    df['d_accum'] = df.groupby(df['group'])['ns_d'].cumsum()

    # Drop the 'group' column as it was just an intermediate step
    df.drop('group', axis=1, inplace=True)

    return df

def filter_clock_signal(df):
    # Define the expected half-cycle time and its half value for noise detection
    expected_time = 105  # in ns
    noise_threshold = expected_time / 2

    # Identify rows where CLK_change is TRUE
    transitions = df[df['CLK'].diff() != 0]

    # Determine rows that are considered noise
    mask = transitions['d_accum'] < noise_threshold

    # Mark these transitions as noise
    noisy_transitions = transitions[mask]
    
    df['noise'] = False
    df.loc[noisy_transitions.index, 'noise'] = True

    return df

def remove_noisy_rows_and_recalculate(df):
    # 1. Remove rows with noise
    df_cleaned = df[df['noise'] == False].copy()

    # 2. Recalculate 'ns_d' column
    df_cleaned['ns_d'] = (df_cleaned['Time(s)'] - df_cleaned['Time(s)'].shift(fill_value=0)) * 1e9  # converting to nanoseconds

    # 3. Recalculate 'd_accum' column using the previous `calculate_d_accum` function
    df_cleaned = calculate_d_accum(df_cleaned)
    
    return df_cleaned

def insert_column_after(df, col_after, new_col, new_col_data):
    # Get the index of the column after which the new column will be inserted
    col_after_index = df.columns.get_loc(col_after)
    
    # Insert the new column after the specified existing column
    df.insert(col_after_index + 1, new_col, new_col_data)

    return df

def partial_reorder_columns(df, order_list):
    # Filter out the columns from the DataFrame that are not in the order_list
    other_columns = [col for col in df.columns if col not in order_list]
    
    # Combine the ordered columns with the other columns
    new_order = order_list + other_columns
    
    # Reorder the DataFrame
    df = df[new_order]
    
    return df

def update_queue(df):
    df['Q0'] = None
    df['Q1'] = None
    df['Q2'] = None
    df['Q3'] = None
    df['QL'] = 0  # Queue Length, initialize to 0

    # Shift the QOP column up by one row for the next row value
    df['next_QOP'] = df['QOP'].shift(-1)

    for index, row in df.iterrows():
        if index > 0:  # Copy the Q and QL values from the previous row
            for col in ['Q0', 'Q1', 'Q2', 'Q3', 'QL']:
                df.at[index, col] = df.at[index - 1, col]

        if pd.notna(row['D']) and row['BUSL'] == 'CODE':
            ql = df.at[index, 'QL']
            if ql < 4:  # If queue is not full
                df.at[index, f'Q{ql}'] = row['D']  # Add the value to the first empty Q column
                df.at[index, 'QL'] += 1  # Increase the queue length
            else:
                print(f"Queue overflow at index {index}")

        # Use next_QOP for the queue operation
        if row['next_QOP'] in ['F', 'S']:
            if df.at[index, 'Q0'] is None:
                print(f"Queue underflow at index {index}")
            else:        
                # Store the value from Q0 and shift the values from Q1, Q2, Q3
                df.at[index, 'QB'] = df.at[index, 'Q0']
                
                df.at[index, 'Q0'] = df.at[index, 'Q1']
                df.at[index, 'Q1'] = df.at[index, 'Q2']
                df.at[index, 'Q2'] = df.at[index, 'Q3']
                df.at[index, 'Q3'] = None
            # Decrease the queue length if it is not already 0
            if df.at[index, 'QL'] > 0:
                df.at[index, 'QL'] -= 1 
        elif row['next_QOP'] == 'E':
            # Empty all Q columns and set QL to 0
            df.at[index, 'Q0'] = None
            df.at[index, 'Q1'] = None
            df.at[index, 'Q2'] = None
            df.at[index, 'Q3'] = None
            df.at[index, 'QL'] = 0

    # Drop the temporary next_QOP column after use 
    df = df.drop(columns=['next_QOP'])

    return df

def update_video(df):

    df['FRAME'] = 0
    df['R_X'] = 0
    df['R_Y'] = 0

    cur_frame = 0
    cur_r_y = 0 
    cur_r_x = 0

    # Iterate over the DataFrame rows
    for index, row in df.iterrows():
        if index > 0:  # Skip the first row to avoid index error

            prev_hs = df.at[index - 1, 'HS']
            prev_vs = df.at[index - 1, 'VS']

            current_hs = row['HS']                            
            current_vs = row['VS']

            if prev_vs == 1 and current_vs == 0:                
                # VSYNC is transitioning from high to low, so we have a new frame.
                # Increment the frame counter, and reset the current scanline counter.
                cur_r_y = 0
                cur_frame += 1  # Increment the current frame value

            if prev_hs == 1 and current_hs == 0:
                # HSYNC is transitioning from high to low, so we have a new scanline.
                # Increment the scanline counter and reset the x counter.
                cur_r_x = 0
                cur_r_y += 1

        df.at[index, 'FRAME'] = cur_frame
        df.at[index, 'R_Y'] = cur_r_y
        df.at[index, 'R_X'] = cur_r_x

        # Scan to next pixel.
        cur_r_x += CLOCK_DIVISOR

    return df

def fetch(df):
    """ fetch instruction bytes from the queue and assemble instruction bytes
    
    This function creates the following columns:
    IS    - Instruction status. This is used to track whether we have read in a prefix or 
            a non-prefix 'first' instruction byte. 
    INST  - Instruction; this is a hexadecimal representation of all the instruction bytes
            read from the queue thus far.
    INSTF - Instruction, final. This is the complete hexadecimal representation of the 
            instruction; this is processed the disassembly phase.
    IDX   - Instruction index. The assembly of the instruction is delayed until the end
            of the instruction where the complete instruction representation is guaranteed,
            therefore the index of the beginning of the instruction is cached here, so that
            the disassembly of the instruction can be placed at the index. 
    """

    # Ensure the 'IS', 'INST', and 'IDX' columns exist
    if 'IS' not in df.columns:
        df['IS'] = ''
    if 'INST' not in df.columns:
        df['INST'] = ''
    if 'INSTF' not in df.columns:
        df['INSTF'] = ''        
    if 'IDX' not in df.columns:
        df['IDX'] = 0
        
    # Set some null, default values.
    current_IS = ''
    current_INST = ''
    current_IDX = 0        

    for index, row in df.iterrows():
        
        # Replicate the IS, INST and IDX columns, ongoing.
        df.at[index, 'IS'] = current_IS
        df.at[index, 'INST'] = current_INST
        df.at[index, 'IDX'] = current_IDX
    
        # Was a byte read from the queue?
        value = str(row['QB']).strip("'")
        
        # If so, process it...
        if value: 
            if pd.isna(row['QB']):
                #df.at[index, 'C'] = "NA??"
                continue        
            try:
                int_value = int(value, 16)
            except ValueError:
                print(f"Error converting '{value}' to an integer at index {index}")
                continue

            new_inst = False
            
            if df.at[index + 1, 'QOP'] == 'F':
                # We've read a 'F' "First Instruction Byte" from the queue.
                
                # Is this byte an instruction prefix?
                if int_value in INSTR_PREFIXES:
                    # If so, enter the 'P' instruction state.
                    df.at[index, 'IS'] = 'P'
                    # If we've read a prefix, we're starting a new instruction.
                    new_inst = True                    
                else:                    
                    # Not a prefix, and 'F' first instruction byte.                  
                    if df.at[index, 'IS'] == 'P':
                        # We were in 'read prefix' state, so we don't need to start a 
                        # new instruction - we did so when the prefix was read. Just 
                        # move to 'I' state.
                        df.at[index, 'IS'] = 'I'
                    else:
                        # We weren't in a prefix state, so this represents the start
                        # of a new, non-prefixed instruction.
                        df.at[index, 'IS'] = 'I'
                        new_inst = True            
                
            if new_inst:
                df.at[index, 'INSTF'] = "'" + df.at[index, 'INST'].strip("'")
                df.at[index, 'INST'] = ''
                df.at[index, 'IDX'] = index
            
            old_inst = df.at[index, 'INST'].strip("'")
            df.at[index, 'INST'] = f"'{old_inst}{value}"
                
        current_IS = df.at[index, 'IS']
        current_IDX = df.at[index, 'IDX']
        current_INST = df.at[index, 'INST']

    return df    

def disassemble(df):

    bitness = 16  # 16-bit for 8088

    for index, row in df.iterrows():
        inst_hex = row['INSTF'].strip("'")
        if inst_hex and not pd.isna(inst_hex):
            try:
                inst_bytes = bytes.fromhex(inst_hex)

                decoder = Decoder(bitness, inst_bytes)
                for instr in decoder:
                    # Create a formatter and get the disassembled instruction
                    formatter = Formatter(FormatterSyntax.NASM)
                    disassembled = formatter.format(instr)
                    
                    # Get the instruction start index
                    instr_idx = int(df.at[index - 1, 'IDX'])
                    
                    # Assign the disassembled instruction to a new column in the DataFrame
                    df.at[instr_idx + 1, 'DISASM'] = disassembled
            except Exception as e:
                print(f"Error disassembling instruction at index {index}: {e}")
                continue

    return df
    
def add_index(df):

    for index, row in df.iterrows():
        df.at[index, 'N'] = index
        
    return df

def main(input_csv, output_csv):
    # Read the input CSV file
    df = pd.read_csv(input_csv, comment=';')

    # Trim whitespace from column names
    df.columns = df.columns.str.strip()
    
    print("Decoding address lines...")
    df['ADDR'] = df.apply(decode_address, axis=1)
    

    # Add time delta to DataFrame
    df = add_time_delta(df)

    # fix up clock signal
    df = calculate_d_accum(df)

    # Filter clock signal
    #df = filter_clock_signal(df)
    
    # Remove noise and recalculate deltas
    #df = remove_noisy_rows_and_recalculate(df)
    
    # Filter clock signal again
    #df = filter_clock_signal(df)    

    # Add a new column 'B' that contains the decimal value calculated from columns 'S2', 'S1', and 'S0'
    print("Decoding bus status...")
    df['B'] = df.apply(decode_status, axis=1)

    # Add ALE signal using value of B (ALE is active when changing from PASV to any other bus state)
    # When ALE is detected, update the value of AL.

    #df = add_ale_column(df)
    print("Calculating ALE and latching addresses...")
    df = add_ale_and_al_columns(df)

    # Generate the 'BUS' column using the bus value to string mapping defined above.
    df['BUS'] = df['B'].map(BUS_MAPPING)

    print("Adding T-states and decoding data bus...")
    df = add_t_and_d_column(df)

    print("Decoding segment status...")
    df = add_seg_column(df)

    print("Decoding queue operations...")
    df = add_qop_column(df)

    print("Latching bus status...")
    df = add_busl_column(df)

    print("Calculating queue contents...")
    df = update_queue(df)

    print("Fetching instructions...")
    df = fetch(df)
    
    print("Disassembling instructions...")
    df = disassemble(df)
    
    if 'VS' in df.columns and 'HS' in df.columns:
        # If both columns are present, call the add_frame function
        print("Calculating frames and scanlines...")        
        df = update_video(df)

    # Reorder our new columns.
    print("Adding index...")
    df = add_index(df)
    
    df = partial_reorder_columns(df, ['N', 'ALE', 'AL', 'SEG', 'BUSL', 'READY', 'T', 'D', 'QOP', 'QB','IS', 'INST', 'INSTF', 'DISASM', 'QL', 'Q0', 'Q1', 'Q2', 'Q3'])

    # Write the updated DataFrame to the output CSV file
    df.to_csv(output_csv, index=False)

if __name__ == '__main__':
    # Ensure correct number of command line arguments
    if len(sys.argv) != 3:
        print("Usage: python decode01.py input_csv output_csv")
        sys.exit(1)

    input_csv = sys.argv[1]
    output_csv = sys.argv[2]

    main(input_csv, output_csv)
bus_sniffer/decode.py
dbalsom/marty_tools
#    Copyright 2022-2023 Daniel Balsom
#    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#    Permission is hereby granted, free of charge, to any person obtaining a
#    copy of this software and associated documentation files (the “Software”),
#    to deal in the Software without restriction, including without limitation
#    the rights to use, copy, modify, merge, publish, distribute, sublicense,
#    and/or sell copies of the Software, and to permit persons to whom the
#    Software is furnished to do so, subject to the following conditions:
#
#    The above copyright notice and this permission notice shall be included in
#    all copies or substantial portions of the Software.
#
#    THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER   
#    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
#    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
#    DEALINGS IN THE SOFTWARE.
#    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

#   excelify.py
#
#   Convert the specified input csv cycle log to Excel format using openpyxl.
#   Excel features such as cell borders and fill are used to make the output
#   pretty.
#
#   Command Line Arguments:
#   input_csv output_csv
import csv
import sys
import json
from openpyxl import Workbook
from openpyxl.styles import Border, Side, PatternFill, Font, NamedStyle
from openpyxl.utils import get_column_letter

from collections import deque


PASTEL_PINK = 'FFD1DC'      # Pastel Pink
PASTEL_ORANGE = 'FFC3A0'    # Pastel Orange
PASTEL_YELLOW = 'FFF5A2'    # Pastel Yellow
PASTEL_GREEN = 'D3FFA3'     # Pastel Green
PASTEL_MINT = 'A2F9E6'      # Pastel Mint
PASTEL_SKY_BLUE = 'A2D5F9'  # Pastel Sky Blue
PASTEL_PURPLE = 'CCA2F9'    # Pastel Purple
PASTEL_VIOLET = 'FCA2F9'    # Pastel Violet
LIGHT_PINK = 'FFB6C1'       # Light Pink
LIGHT_BLUE = 'ADD8E6'       # Light Blue
PALE_GREEN = '98FB98'       # Pale Green
LIGHT_GOLDEN = 'FFD700'     # Light Golden
WHEAT = 'F5DEB3'            # Wheat
LIGHT_CYAN = 'E0FFFF'       # Light Cyan
THISTLE = 'D8BFD8'          # Thistle
SANDY_BROWN = 'F4A460'      # Sandy Brown
LIGHT_GRAY = 'D3D3D3'  # Light Gray
NORMAL_BLUE = '0000FF'

FILL_COLORS = ["ADD8E6", "FFB6C1", "FFDAB9", "E0FFFF", "FFF0F5", "D8BFD8", "FAEBD7"]
COLOR_MAP = {
    'SEG': {
        #'CS': LIGHT_BLUE,
        'DS': PASTEL_GREEN,
        'SS': PASTEL_PINK,
        'ES': PASTEL_YELLOW
    },
    'T': {
        'TI': LIGHT_GRAY,
        'Tw': LIGHT_BLUE
    },
    'QOP': {
        'F': PASTEL_MINT,
        'S': PASTEL_YELLOW,
        'E': PASTEL_PINK,
    }
}

def apply_fill(wb):
    """
    Iterates through the worksheet and applies fill color based on column header and cell value.

    :param ws: The worksheet object.
    :param color_map: The color mapping.
    """

    print("\nApplying cell fills...")

    ws = wb.active
    headers = {}
    for col_num, cell in enumerate(ws[1], 1):  # assuming headers are in row 1
        headers[col_num] = cell.value

    for row_num, row in enumerate(ws.iter_rows(min_row=2, values_only=True), start=2):  # starting from row 2
        for col_num, cell_value in enumerate(row, 1):
            col_header = headers[col_num]
            if col_header in COLOR_MAP and cell_value in COLOR_MAP[col_header]:
                color_hex = COLOR_MAP[col_header][cell_value]
                fill = PatternFill(start_color=color_hex, end_color=color_hex, fill_type="solid")
                ws.cell(row=row_num, column=col_num).fill = fill

def set_clk_borders(ws, curr, prev, nxt, row, col_num, fill_color):

    fill_style = PatternFill(start_color=fill_color, end_color=fill_color, fill_type="solid")
    borders = {"left": None, "right": None, "top": None, "bottom": None}
    do_fill = None

    if curr != prev:
        borders["top"] = Side(style='thin')
    if curr == 0:
        borders["left"] = Side(style='thin')
    if curr == 1:
        borders["right"] = Side(style='thin')
        do_fill = fill_style
    if curr != nxt:
        borders["bottom"] = Side(style='thin')

    ws.cell(row=row, column=col_num + 2).border = Border(
        left=borders["left"],
        right=borders["right"],
        top=borders["top"],
        bottom=borders["bottom"]
    )

    if do_fill:
        ws.cell(row=row, column=col_num + 2).fill = do_fill

def adjust_column_width(ws, col_num, skinny=True):

    col_letter = get_column_letter(col_num)
    default_height = 15.0
    row_height_points = ws.row_dimensions[1].height or default_height

    print(f"Adjusting width for column {col_letter}")

    if skinny:
        ws.column_dimensions[col_letter].width = row_height_points / 10
    else:
        ws.column_dimensions[col_letter].width = row_height_points / 5


def read_csv(csv_filename):

    with open(csv_filename, mode='r') as csv_file:
        reader = csv.reader(csv_file)
        headers = next(reader)
        data = [row for row in reader]

    return headers, data

def csv_to_excel(csv_filename):
    
    wb = Workbook()
    ws = wb.active
    instructions = deque()

    with open(csv_filename, mode='r') as csv_file:
        csv_reader = csv.DictReader(csv_file)

        print("Creating excel worksheet...")

        for idx, header in enumerate(csv_reader.fieldnames, start=1):
            ws.cell(row=1, column=idx, value=header)

        for i, row in enumerate(csv_reader):
            if i % 1000 == 0:
                sys.stdout.write(f'\rProcessed row: {i}')
                sys.stdout.flush()
            ws.append(list(row.values()))

            if row['DISASM']:
                #print(f"Found instruction at: {i}")
                instructions.append(i + 3)

    return wb, instructions

def find_disasm_rows(headers, data):

    disasm_rows = []
    try:
        disasm_index = headers.index('DISASM')
    except ValueError:
        raise ValueError("No 'DISASM' column found in the CSV file.")
    
    for i, row in enumerate(data):
        if row[disasm_index]:
            disasm_rows.append(i)

    return disasm_rows

def set_border(cell, row_num, disasm_rows):

    if (row_num - 2) in disasm_rows:
        thick_border_side = Side(style='thick')
        cell.border = Border(top=thick_border_side)

def set_instr_border(cell):
    thick_border = Side(style='thick')

    left = cell.border.left
    right = cell.border.right
    bottom = cell.border.bottom

    cell.border = Border(left=left, right=right, top=thick_border, bottom=bottom)

def set_fill(cell, value):

    blue_fill = PatternFill(start_color="CCDDFF", end_color="CCDDFF", fill_type="solid")
    grey_fill = PatternFill(start_color="DDDDDD", end_color="DDDDDD", fill_type="solid")

    if value == 'Tw':
        cell.fill = blue_fill
    elif value == 'TI':
        cell.fill = grey_fill

def add_instruction_borders(wb, instructions):

    print("\nAdding instruction borders...")

    ws = wb.active

    have_disasm = False

    for cell in ws[1]:  # assuming headers are in the first row
        if cell.value == 'DISASM':
            disasm_index = cell.column  # get the column number
            have_disasm = True
            break

    if not have_disasm:
        return
    
    next_instr_idx = instructions.popleft()

    # Mark instructions with horizontal borders.
    for i, row in enumerate(ws.iter_rows(), start=2): 

        if i % 1000 == 0:
            sys.stdout.write(f'\rProcessed row: {i}')
            sys.stdout.flush()
        
        if i == next_instr_idx:
            #print(f"\nMarking instruction: {i}")
            for col_num, cell in enumerate(row, start=1):
                set_instr_border(cell)
            try:
                next_instr_idx = instructions.popleft()
            except:
                return
                
                
def draw_clocks(wb, columns):

    color_index = 0

    print("\nDrawing clocks...")
    ws = wb.active
    
    # Iterate over column headers to find clk columns
    col_num = 1

    # Add columns to represent clock art
    for cell in ws[1]:
        cell_value = cell.value
        # If the header value matches our list
        if str(cell_value).upper() in columns:
            # Insert two new columns to the right of the clock data column
            ws.insert_cols(col_num + 1, 2)
            
            ws.cell(row=1, column=col_num + 1, value=str(cell_value).upper() + "1")
            ws.cell(row=1, column=col_num + 2, value=str(cell_value).upper() + "2")

            adjust_column_width(ws, col_num + 1)
            adjust_column_width(ws, col_num + 2, False)
            
            # Move to the next column and increment color index
            col_num += 3
        else:
            col_num += 1

    # Reset column number and actually draw them
    col_num = 0

    for col_num, cell in enumerate(ws[1]):

        cell_value = cell.value
        #print(f"Evaluating column: {str(cell_value).upper()}")

        # If the header value matches our list
        if str(cell_value).upper() in columns:

            print(f"Drawing clock column: {cell_value}")

            start_row = 2  # Assuming headers are present
            clk_data = [
                value
                for value in ws.iter_cols(
                    min_col=col_num + 1,
                    max_col=col_num + 1,
                    min_row=start_row,
                    values_only=True,
                )
            ][0]
            
            # Insert two new columns to the right of the clock data column
            #ws.insert_cols(col_num + 1, 2)

            # Set width for the first inserted column
            
            #ws.column_dimensions[chr(65 + col_num)].width = 1.0  # Static small width
            #adjust_column_width(ws, col_num)

            for index, value in enumerate(clk_data, start_row):
                if not value:
                    print(f"No value at index: {index}")
                    break

                value = int(value)
                prev_value = (
                    int(clk_data[index - start_row - 1]) if index > start_row else value
                )
                next_value = (
                    int(clk_data[index - start_row + 1])
                    if index - start_row < len(clk_data) - 1
                    else value
                )
                
                # Only set fill color if the value is 0 or 1
                if value in [0, 1]:

                    ws.cell(row=index, column=col_num + 2).fill = PatternFill(
                        start_color=FILL_COLORS[color_index],
                        end_color=FILL_COLORS[color_index],
                        fill_type="solid",
                    )
                    set_clk_borders(
                        ws,
                        value,
                        prev_value,
                        next_value,
                        index,
                        col_num + 1,
                        FILL_COLORS[color_index],
                    )

            # Adjust column width after drawing the clock
            #adjust_column_width(ws, col_num + 1)
            
            # Move to the next column and increment color index
            col_num += 3
            color_index = (color_index + 1) % len(FILL_COLORS)

def convert_columns_to_text(wb, col_headers):
    """
    Converts specified columns to text and removes any "`" character from the values.

    :param ws: The worksheet object.
    :param col_headers: List of column header names to be converted.
    """

    ws = wb.active
    # First, identify the column numbers for the headers.
    header_col_nums = {}
    for col_num, cell in enumerate(ws[1], 1):  # assuming headers are in row 1
        if cell.value in col_headers:
            header_col_nums[cell.value] = col_num
    
    # Next, iterate over the cells in the specified columns.
    for col_header, col_num in header_col_nums.items():
        for row_num, row in enumerate(ws.iter_rows(min_row=2, min_col=col_num, max_col=col_num), start=2):  # starting from row 2
            cell = ws.cell(row=row_num, column=col_num)
            # If cell contains a value, convert it to text and remove any "`" character.
            if cell.value is not None:
                cell.value = str(cell.value).replace("'", "")

def adjust_column_widths(wb, ignore_cols, padding=2):
    """
    Iterate through columns in the worksheet and set the column width based on the
    length of the longest string in each column.

    :param ws: The worksheet object.
    :param padding: An optional padding to add to the column width.
    """

    ws = wb.active

    min_width = 1

    skip_cols = []

    # Add clock columns
    for item in ignore_cols:
        skip_cols.append(item)
        skip_cols.append(item + '1')
        skip_cols.append(item + '2')

    for col_idx, col in enumerate(ws.columns, 1):  # starting enumeration from 1
        max_length = 0
        # If the header of the column is in skip_cols, skip the column
        
        if col[0].value in skip_cols:
            print(f"skipping resize of column: {col[0].value}")
            continue

        for cell in col[1:]:  # skip the header cell
            try:
                if len(str(cell.value)) > max_length:
                    max_length = len(cell.value)
            except:
                pass

        if max_length > 0:  # Only adjust column width if a nonzero length string was found.
            adjusted_width = max_length + padding
            ws.column_dimensions[get_column_letter(col_idx)].width = adjusted_width

            print(f"resizing column: {col[0].value} to width: {adjusted_width}")
        else:
            ws.column_dimensions[get_column_letter(col_idx)].width = min_width

def remove_columns_by_header(wb, headers):
    """
    Removes columns from the worksheet based on the header names.

    :param workbook: The loaded workbook object.
    :param headers: A list of header names to be removed.
    """
    ws = wb.active  # assuming we are working on the active sheet
    header_row = [cell.value for cell in ws[1]]  # assuming headers are in the first row
    
    # Get column indices for headers to be removed
    col_indices = [header_row.index(header) + 1 for header in headers if header in header_row]

    # Sort indices in descending order and remove columns
    for col_idx in sorted(col_indices, reverse=True):
        ws.delete_cols(col_idx)

def keep_only_columns(wb, headers_to_keep):
    """
    Keeps only the columns from the worksheet based on the header names provided and removes all other columns.

    :param workbook: The loaded workbook object.
    :param headers_to_keep: A list of header names to be kept.
    """
    ws = wb.active  # assuming we are working on the active sheet
    header_row = [cell.value for cell in ws[1]]  # assuming headers are in the first row

    # Find headers to remove: all headers that are not in headers_to_keep
    headers_to_remove = [header for header in header_row if header not in headers_to_keep]

    # Call the remove_columns_by_header function to remove these headers
    remove_columns_by_header(wb, headers_to_remove)

def print_cols(wb):

    ws = wb.active
    for cell in ws[1]:
        print(f"column name: {cell.value}")

def add_instructions_sheet(wb):

    print("Adding instruction index...")
    # Assuming the first sheet is the main sheet
    ws = wb.active

    # Create 'Instructions' sheet if not exist
    if 'Instructions' in wb.sheetnames:
        instructions_sheet = wb['Instructions']
    else:
        instructions_sheet = wb.create_sheet(title='Instructions')

    # Find the index of the "DISASM" column in the main sheet
    disasm_col = None
    for col in ws.iter_cols(1, ws.max_column):
        if col[0].value == 'DISASM':
            disasm_col = col
            break
    
    if disasm_col is None:
        raise Exception("DISASM column not found in the main sheet")
    
    # Fixed-width font style and hyperlink font style
    fixed_font = Font(name='Courier New')
    #hyperlink_font = Font(color='0000FF', underline='single')
    hyperlink = NamedStyle(name="hyperlink", font=Font(color="0563C1", underline="single"))

    # Variables for tracking cycles
    last_row_num = 0
    row_num = 2  # Start from the second row
    first_cycle = True

    # Adding headers
    instructions_sheet['A1'] = 'DISASM'
    instructions_sheet['B1'] = 'CYCLES'  # Swapped column
    instructions_sheet['C1'] = 'LINK'    # Swapped column

    # Iterate through cells in "DISASM" column in the main sheet starting from row 2
    for index, cell in enumerate(disasm_col[1:], start=2):
        # If cell has a value, copy it to "Instructions" sheet and create a link
        if cell.value:
            instructions_sheet[f'A{row_num}'].value = cell.value
            instructions_sheet[f'A{row_num}'].font = fixed_font

            instructions_sheet[f'C{row_num}'].hyperlink = f"#{ws.title}!A{cell.row}"
            instructions_sheet[f'C{row_num}'].style = hyperlink
            #instructions_sheet[f'C{row_num}'] = f'=HYPERLINK("#\'{ws.title}\'!{cell.coordinate}", "Link to {cell.coordinate}")'
            #instructions_sheet[f'C{row_num}'].font = hyperlink_font
            
            # Calculate the number of cycles (rows between instructions)
            if last_row_num != 1:
                cycles = index - last_row_num
                if first_cycle:
                    first_cycle = False
                else:
                    instructions_sheet[f'B{row_num - 1}'] = cycles
            last_row_num = index
            row_num += 1

def add_io_sheet(wb):
    print("Adding IO index...")

    try:
        # Try to open and load the ports.json file
        with open('ports.json', 'r') as file:
            ports = json.load(file)
    except FileNotFoundError:
        print("Error: The ports.json file could not be found.")
        ports = {}  # Set ports to an empty dictionary
    except json.JSONDecodeError:
        print("Error: Unable to decode JSON data in ports.json file.")
        ports = {}  # Set ports to an empty dictionary

    main_sheet = wb.active

    # Create 'IO' sheet if not exist
    if 'IO' in wb.sheetnames:
        io_sheet = wb['IO']
    else:
        io_sheet = wb.create_sheet(title='IO')

    # Adding headers
    io_sheet['A1'] = 'ADDR'
    io_sheet['B1'] = 'OP'
    io_sheet['C1'] = 'DATA'
    io_sheet['D1'] = 'LINK'  # 'LINK' column header is now D
    io_sheet['E1'] = 'DESC'  # 'DESC' column header is now E

    # Initialize row_num for IO sheet
    row_num = 2

    hyperlink = NamedStyle(name="hyperlink2", font=Font(color="0563C1", underline="single"))

    # Resolve the column headers to column numbers
    busl_col, al_col, d_col = None, None, None
    for i, col in enumerate(main_sheet.iter_cols(1, main_sheet.max_column)):
        if col[0].value == 'BUSL':
            busl_col = i + 1
        elif col[0].value == 'AL':
            al_col = i + 1
        elif col[0].value == 'D':
            d_col = i + 1

    if not busl_col or not al_col or not d_col:
        raise Exception("Required columns not found in the main sheet")            

    # Iterate through cells in 'D' column in the main sheet starting from row 2
    for row in main_sheet.iter_rows(min_row=2, min_col=d_col, max_col=d_col, max_row=main_sheet.max_row):
        for cell in row:
            if cell.value:  # If cell in 'D' column has a value
                busl_value = main_sheet.cell(row=cell.row, column=busl_col).value  # BUSL value in the same row
                al_value = main_sheet.cell(row=cell.row, column=al_col).value  # AL value in the same row
                
                if busl_value == 'IOR' or busl_value == 'IOW':
                    io_sheet[f'A{row_num}'] = al_value
                    io_sheet[f'B{row_num}'] = 'R' if busl_value == 'IOR' else 'W'
                    io_sheet[f'C{row_num}'] = cell.value

                    # Construct the key and look up in the ports.json
                    addr = al_value[-4:]
                    op = io_sheet[f'B{row_num}'].value
                    key = f"{addr}{op.lower()}"
                    desc = ports.get(key, '')  # If not found, default to an empty string
                    io_sheet[f'E{row_num}'] = desc  # 'DESC' is now column E

                    link = f"#{main_sheet.title}!A{cell.row}"
                    io_sheet[f'D{row_num}'].hyperlink = link  # 'LINK' is now column D
                    io_sheet[f'D{row_num}'].style = hyperlink

                    row_num += 1

    # Set the auto_filter to include all data in the IO sheet.
    io_sheet.auto_filter.ref = io_sheet.dimensions

def main():

    clk_columns = ["READY", "CLK0", "INTR", "DR0", "VS", "HS"]
    text_columns = ["AL", "INSTF", "D", "QB", "Q0", "Q1", "Q2", "Q3", "ADDR"]
    keep_columns = [
        'N', 'ALE', 'AL', 'SEG', 'BUSL', 'READY', 'READY1', 'READY2', 'T', 'D', 'QOP', 'QB', 'INSTF', 'DISASM', 
        'QL', 'Q0', 'Q1', 'Q2', 'Q3',
        'CLK0', 'CLK01', 'CLK02', 'INTR', 'INTR1', 'INTR2', 'DR0', 'DR01', 'DR02', 'VS', 'VS1', 'VS2', 'HS', 'HS1', 'HS2',
        "FRAME", "R_X", "R_Y"
    ]
    
    if len(sys.argv) != 3:
        print("Usage: python excelify.py <input.csv> <output.xlsx>")
        sys.exit(1)
    
    input_csv = sys.argv[1]
    output_xlsx = sys.argv[2]
    
    #headers, data = read_csv(input_csv)

    wb, instructions = csv_to_excel(input_csv)

    print(f"\nFound {len(instructions)} instructions...")
    
    keep_only_columns(wb, keep_columns)
    draw_clocks(wb, clk_columns)
    adjust_column_widths(wb, clk_columns, padding=4)
    add_instruction_borders(wb, instructions)
    
    convert_columns_to_text(wb, text_columns)
    apply_fill(wb)
    
    # Freeze the top row
    wb.active.freeze_panes = 'A2'

    add_instructions_sheet(wb)
    add_io_sheet(wb)

    print("\nSaving xlsx...")
    wb.save(output_xlsx)
    #disasm_rows = find_disasm_rows(headers, data)
    #write_to_xlsx(headers, data, output_xlsx, disasm_rows)
    #wb.save(output_xlsx)

if __name__ == "__main__":
    main()

bus_sniffer/excelify.py
dbalsom/marty_tools
#    Copyright 2022-2023 Daniel Balsom
#    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#    Permission is hereby granted, free of charge, to any person obtaining a
#    copy of this software and associated documentation files (the “Software”),
#    to deal in the Software without restriction, including without limitation
#    the rights to use, copy, modify, merge, publish, distribute, sublicense,
#    and/or sell copies of the Software, and to permit persons to whom the
#    Software is furnished to do so, subject to the following conditions:
#
#    The above copyright notice and this permission notice shall be included in
#    all copies or substantial portions of the Software.
#
#    THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER   
#    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
#    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
#    DEALINGS IN THE SOFTWARE.
#    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

#   export_cycles.py
#
#   Export only the rows representing a low to high transition of the 'CLK' 
#   column from a CSV file exported from PulseView/DSView.
#
#   Command Line Arguments:
#   input_csv output_csv

import pandas as pd
import sys

def process_chunk(chunk, prev_clk):
    chunk.columns = chunk.columns.str.strip()
    prev_clk_values = chunk['CLK'].shift(fill_value=prev_clk).astype(int)
    result = chunk[(prev_clk_values == 0) & (chunk['CLK'] == 1)]
    return result, chunk['CLK'].iat[-1] 

def process_csv(input_csv, output_csv):
    prev_clk = 0
    chunk_number = 0
    
    with open(output_csv, 'w', newline='') as outfile:
        for chunk in pd.read_csv(input_csv, chunksize=10000, comment=';'):
            try:
                chunk_number += 1
                result_chunk, prev_clk = process_chunk(chunk, prev_clk)
                
                if chunk_number == 1:
                    result_chunk.to_csv(outfile, index=False)
                else:
                    result_chunk.to_csv(outfile, index=False, header=False, line_terminator='\n', mode='a')
                
                sys.stdout.write(f'\rProcessing chunk number {chunk_number}...')
                sys.stdout.flush()
            except Exception as e:
                print(f"An error occurred while processing chunk number {chunk_number}: {e}")
        
        print()

if __name__ == '__main__':
    if len(sys.argv) != 3:
        print("Usage: python export_cycles.py <input_csv> <output_csv>")
        sys.exit(1)

    input_csv, output_csv = sys.argv[1], sys.argv[2]
    process_csv(input_csv, output_csv)
bus_sniffer/export_cycles.py
dbalsom/marty_tools
#    Copyright 2022-2023 Daniel Balsom
#    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#    Permission is hereby granted, free of charge, to any person obtaining a
#    copy of this software and associated documentation files (the “Software”),
#    to deal in the Software without restriction, including without limitation
#    the rights to use, copy, modify, merge, publish, distribute, sublicense,
#    and/or sell copies of the Software, and to permit persons to whom the
#    Software is furnished to do so, subject to the following conditions:
#
#    The above copyright notice and this permission notice shall be included in
#    all copies or substantial portions of the Software.
#
#    THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER   
#    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
#    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
#    DEALINGS IN THE SOFTWARE.
#    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

#   head.py
#
#   Export the specified number of rows from the front of the file to the 
#   destination file. 
#   If a time offset is provided, rows will be scanned forthe 'Time(s)' column
#   to exceed the provided offset before the specified number of rows are 
#   exported.

#   Command Line Arguments:
#   num_rows time_offset input_csv output_csv

import pandas as pd
import sys
import time

def display_status(chunk_count, current_time, start_time, rows_processed):
    elapsed_time = time.time() - start_time
    rows_per_second = rows_processed / elapsed_time
    sys.stdout.write(f"\rProcessing chunk: {chunk_count}, Time(s): {current_time}, Rows/sec: {rows_per_second:.2f}")
    sys.stdout.flush()

def dump_rows(n, time_offset, source_file, destination_file):
    CHUNK_SIZE = 10000
    chunk_count = 0
    rows_processed = 0
    start_time = time.time()

    reader = pd.read_csv(source_file, sep=',', comment=';', chunksize=CHUNK_SIZE)

    first_chunk = True
    rows_to_write = n
    for chunk in reader:
        chunk_count += 1
        rows_processed += len(chunk)
        
        # Not writing the last row of the chunk, as it might be incomplete
        chunk = chunk.iloc[:-1]

        # Filtering based on the Time(s) condition
        filtered_chunk = chunk[chunk['Time(s)'] > time_offset]
        
        if not filtered_chunk.empty:
            # If we find rows greater than the time_offset
            if first_chunk:
                filtered_chunk.head(rows_to_write).to_csv(destination_file, mode='w', index=False)
                first_chunk = False
                rows_to_write -= len(filtered_chunk.head(rows_to_write))
            else:
                filtered_chunk.head(rows_to_write).to_csv(destination_file, mode='a', header=False, index=False)
                rows_to_write -= len(filtered_chunk.head(rows_to_write))

            if rows_to_write <= 0:
                break

        current_time = chunk['Time(s)'].max()
        display_status(chunk_count, current_time, start_time, rows_processed)
        
    if rows_to_write > 0:
        print(f"\nCould only extract {n - rows_to_write} rows after the time offset.")

if __name__ == '__main__':
    # Example usage: script_name.py 100 12.34 source.csv dest.csv
    n = int(sys.argv[1])
    time_offset = float(sys.argv[2])
    source_file = sys.argv[3]
    destination_file = sys.argv[4]

    dump_rows(n, time_offset, source_file, destination_file)
bus_sniffer/head.py
dbalsom/marty_tools
##
## This file is part of the libsigrokdecode project.
##
## Copyright (C) 2013 Uwe Hermann <uwe@hermann-uwe.de>
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program; if not, see <http://www.gnu.org/licenses/>.
##

'''
This protocol decoder can decode the CPU signals produced by the Intel
8088 CPU in Maximum Mode paired with an Intel 8288 Bus controller.

To properly set the address latch, an ALE signal must be provided.

'''

from .pd import Decoder

sigrok_decoders/i8088/__init__.py
dbalsom/marty_tools


class Disassembler:
    def __init__(self):
        self.mnemonics = [
            ("ADD", 0),
            ("PUSH", 0),
            ("POP", 0),
            ("OR", 0),
            ("ADC", 0),
            ("SBB", 0),
            ("AND", 0),
            ("ES", 0),
            ("DAA", 0),
            ("SUB", 0),
            ("CS", 0),
            ("DAS", 0),
            ("XOR", 0),
            ("SS", 0),
            ("AAA", 0),
            ("CMP", 0),
            ("DS", 0),
            ("AAS", 0),
            ("INC", 0),
            ("DEC", 0),
            ("JO", 0),
            ("JNO", 0),
            ("JB", 0),
            ("JNB", 0),
            ("JZ", 0),
            ("JNZ", 0),
            ("JBE", 0),
            ("JNBE", 0),
            ("JS", 0),
            ("JNS", 0),
            ("JP", 0),
            ("JNP", 0),
            ("JL", 0),
            ("JNL", 0),
            ("JLE", 0),
            ("JNLE", 0),
            ("TEST", 0),
            ("XCHG", 0),
            ("MOV", 0),
            ("LEA", 0),
            ("CBW", 0),
            ("CWD", 0),
            ("CALLF", 0),
            ("PUSHF", 0),
            ("POPF", 0),
            ("SAHF", 0),
            ("LAHF", 0),
            ("MOVSB", 0),
            ("MOVSW", 0),
            ("CMPSB", 0),
            ("CMPSW", 0),
            ("STOSB", 0),
            ("STOSW", 0),
            ("LODSB", 0),
            ("LODSW", 0),
            ("SCASB", 0),
            ("SCASW", 0),
            ("RETN", 0),
            ("LES", 0),
            ("LDS", 0),
            ("RETF", 0),
            ("INT", 0),
            ("INTO", 0),
            ("IRET", 0),
            ("ROL", 0),
            ("ROR", 0),
            ("RCL", 0),
            ("RCR", 0),
            ("SHL", 0),
            ("SHR", 0),
            ("SAR", 0),
            ("AAM", 0),
            ("AMX", 0),
            ("AAD", 0),
            ("ADX", 0),
            ("XLAT", 0),
            ("LOOPNE", 0),
            ("LOOPE", 0),
            ("LOOP", 0),
            ("JCXZ", 0),
            ("IN", 0),
            ("OUT", 0),
            ("CALL", 0),
            ("JMP", 0),
            ("JMPF", 0),
            ("LOCK", 0),
            ("REPNZ", 0),
            ("REP", 0),
            ("REPZ", 0),
            ("HLT", 0),
            ("CMC", 0),
            ("NOT", 0),
            ("NEG", 0),
            ("MUL", 0),
            ("IMUL", 0),
            ("DIV", 0),
            ("IDIV", 0),
            ("CLC", 0),
            ("STC", 0),
            ("CLI", 0),
            ("STI", 0),
            ("CLD", 0),
            ("STD", 0),
            ("WAIT", 0),
            ("INVAL", 0),
            ("GRP1", 1),
            ("GRP2A", 2),
            ("GRP3", 3),
            ("GRP4", 4),
            ("GRP5", 5),
            ("GRP2B", 6),
            ("NOP", 0),
        ]
        self.grp_mnemonics = [
            [
                "ADD",
                "OR",
                "ADC",
                "SBB",
                "AND",
                "SUB",
                "XOR",
                "CMP"
            ],[ 
                "ROL",
                "ROR",
                "RCL",
                "RCR",  
                "SHL",
                "SHR",
                "SETMO",
                "SAR"
            ],[
                "ROL",
                "ROR",
                "RCL",
                "RCR",
                "SHL",
                "SHR",
                "SETMOC",
                "SAR"
            ],[
              "TEST",
              "TEST",
              "NOT",
              "NEG",
              "MUL",
              "IMUL",
              "DIV",
              "IDIV",
            ],[
                "INC",
                "DEC",
                "INVAL",
                "INVAL",
                "INVAL",
                "INVAL",
                "INVAL",
                "INVAL"
            ],[
                "INC",
                "DEC",
                "CALL",
                "CALLF",
                "JMP",
                "JMPF",
                "PUSH",
                "INVAL"
            ]
        ]

        self.opcode_refs = [
            0, 0, 0, 0, 0, 0, 1, 2, 3, 3, 3, 3, 3, 3, 1, 2,
            4, 4, 4, 4, 4, 4, 1, 2, 5, 5, 5, 5, 5, 5, 1, 2,
            6, 6, 6, 6, 6, 6, 7, 8, 9, 9, 9, 9, 9, 9, 10, 11, 
            12, 12, 12, 12, 12, 12, 13, 14, 15, 15, 15, 15, 15, 15, 16, 17,
            18, 18, 18, 18, 18, 18, 18, 18, 19, 19, 19, 19, 19, 19, 19, 19,
            1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2,
            20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35,
            20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35,
            105, 105, 105, 105, 36, 36, 37, 37, 38, 38, 38, 38, 38, 39, 38, 2,
            111, 37, 37, 37, 37, 37, 37, 37, 40, 41, 42, 103, 43, 44, 45, 46,
            38, 38, 38, 38, 47, 48, 49, 50, 36, 36, 51, 52, 53, 54, 55, 56,
            38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38, 38,
            57, 57, 57, 57, 58, 59, 38, 38, 60, 60, 60, 60, 61, 61, 62, 63,
            106, 106, 110, 110, 71, 73, 104, 75, 104, 104, 104, 104, 104, 104, 104, 104,
            76, 77, 78, 79, 80, 80, 81, 81, 82, 83, 84, 83, 80, 80, 81, 81,
            85, 104, 86, 87, 89, 90, 107, 107, 97, 98, 99, 100, 101, 102, 108, 109 
        ]

    def lookup_opcode(self, byte, modrm=None):
        if not (isinstance(byte, int) and 0 <= byte <= 255):
            raise ValueError("Expected a byte (0-255), got: {}".format(byte))

        op_tuple = self.mnemonics[self.opcode_refs[byte]]

        if op_tuple[1] > 0:
            # Group opcode.
            if modrm is None:
                raise ValueError("Group opcode with no modrm")
            
            op_ext = (modrm >> 3) & 0x07
            return self.grp_mnemonics[op_tuple[1]-1][op_ext]    
        else:
            return op_tuple[0]
        
    def disassemble(self, code_bytes):
        if len(code_bytes) == 0:
            return "nul"
        elif len(code_bytes) == 1:
            return self.lookup_opcode(code_bytes[0])
        else:
            return self.lookup_opcode(code_bytes[0], code_bytes[1])
sigrok_decoders/i8088/disasm.py
dbalsom/marty_tools
##
## This file is part of the libsigrokdecode project.
##
## Copyright (C) 2013-2016 Uwe Hermann <uwe@hermann-uwe.de>
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program; if not, see <http://www.gnu.org/licenses/>.
##

import sigrokdecode as srd
from functools import reduce
from common.srdhelper import bitpack
from collections import deque
from .disasm import Disassembler

'''
OUTPUT_PYTHON format:

Packet:
[<ptype>, <pdata>]

<ptype>, <pdata>
 - 'ITEM', [<item>, <itembitsize>]
 - 'WORD', [<word>, <wordbitsize>, <worditemcount>]

<item>:
 - A single item (a number). It can be of arbitrary size. The max. number
   of bits in this item is specified in <itembitsize>.

<itembitsize>:
 - The size of an item (in bits). For a 4-bit parallel bus this is 4,
   for a 16-bit parallel bus this is 16, and so on.

<word>:
 - A single word (a number). It can be of arbitrary size. The max. number
   of bits in this word is specified in <wordbitsize>. The (exact) number
   of items in this word is specified in <worditemcount>.

<wordbitsize>:
 - The size of a word (in bits). For a 2-item word with 8-bit items
   <wordbitsize> is 16, for a 3-item word with 4-bit items <wordbitsize>
   is 12, and so on.

<worditemcount>:
 - The size of a word (in number of items). For a 4-item word (no matter
   how many bits each item consists of) <worditemcount> is 4, for a 7-item
   word <worditemcount> is 7, and so on.
'''

class ChannelError(Exception):
    pass

ADDRESS_LINES = ['ad0', 'ad1', 'ad2', 'ad3', 'ad4', 'ad5', 'ad6', 'ad7', 'a8', 'a9', 'a10', 'a11', 'a12', 'a13', 'a14', 'a15', 'a16', 'a17', 'a18', 'a19']

ADDR_LEN = 20
PINS_LEN = 27

class Pin:
    AD0, AD1, AD2, AD3, AD4, AD5, AD6, AD7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, S0, S1, S2, QS0, QS1, RDY, CLK = range(PINS_LEN)

class Addr:
    AD0, AD1, AD2, AD3, AD4, AD5, AD6, AD7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19 = range(ADDR_LEN)

class Status:
    S0, S1, S2 = 20, 21, 22

class TState:
    TI, T1, T2, T3, TW, T4 = range(6)

class Annot:
    ALE,AddrLatch,BusStatus,BusStatusL,QsF,QsS,QsE,TI,T1,T2,T3,TW,T4,D,F,Mm,Piq,Ib,Dbg,Err = range(20)

class BusStatus:
    INTA,IOR,IOW,HALT,CODE,MEMR,MEMW,PASV = range(8)

class QueueOp:
    Idle, First, Empty, Subs = range(4)

QUEUE_ANNOTS = [
    Annot.QsF,
    Annot.QsF,
    Annot.QsE,
    Annot.QsS,
]

BUS_STATES = [
    'INTA',
    'IOR',
    'IOW',
    'HALT',
    'CODE',
    'MEMR',
    'MEMW',
    'PASV',
]

QUEUE_STATES = [
    '.',
    'F',
    'E',
    'S',
]

T_ANNOTS = [
    Annot.TI, 
    Annot.T1, 
    Annot.T2, 
    Annot.T3, 
    Annot.TW, 
    Annot.T4
]

T_STATES = [
    'Ti',
    'T1',
    'T2',
    'T3',
    'Tw',
    'T4',
]

def reduce_bus(bus):
    if 0xFF in bus:
        return None # unassigned bus channels
    else:
        return reduce(lambda a, b: (a << 1) | b, reversed(bus))
    
class TrackedValue:
    def __init__(self, initial_value=None):
        self.prev = None
        self.cur = initial_value

    def update(self, new_value):
        self.prev = self.cur
        self.cur = new_value
        return self

    def changed(self):
        return self.prev != self.cur

class Decoder(srd.Decoder):
    api_version = 3
    id = 'i8088'
    name = 'i8088'
    longname = 'Intel 8088 CPU'
    desc = 'Decoder for the Intel 8088 CPU'
    license = 'gplv2+'
    inputs = ['logic']
    outputs  = []
    tags = ['Retrocomputing']
    channels = (
        {'id': 'ad0', 'name': 'AD0', 'desc': 'Address/Data Line 0'},
        {'id': 'ad1', 'name': 'AD1', 'desc': 'Address/Data Line 1'},
        {'id': 'ad2', 'name': 'AD2', 'desc': 'Address/Data Line 2'},
        {'id': 'ad3', 'name': 'AD3', 'desc': 'Address/Data Line 3'},
        {'id': 'ad4', 'name': 'AD4', 'desc': 'Address/Data Line 4'},
        {'id': 'ad5', 'name': 'AD5', 'desc': 'Address/Data Line 5'},
        {'id': 'ad6', 'name': 'AD6', 'desc': 'Address/Data Line 6'},
        {'id': 'ad7', 'name': 'AD7', 'desc': 'Address/Data Line 7'},
        {'id': 'a8',  'name': 'A8',  'desc': 'Address Line 8'},
        {'id': 'a9',  'name': 'A9',  'desc': 'Address Line 9'},
        {'id': 'a10', 'name': 'A10', 'desc': 'Address Line 10'},
        {'id': 'a11', 'name': 'A11', 'desc': 'Address Line 11'},
        {'id': 'a12', 'name': 'A12', 'desc': 'Address Line 12'},
        {'id': 'a13', 'name': 'A13', 'desc': 'Address Line 13'},
        {'id': 'a14', 'name': 'A14', 'desc': 'Address Line 14'},
        {'id': 'a15', 'name': 'A15', 'desc': 'Address Line 15'},
        {'id': 'a16', 'name': 'A16', 'desc': 'Address Line 16'},
        {'id': 'a17', 'name': 'A17', 'desc': 'Address Line 17'},
        {'id': 'a18', 'name': 'A18', 'desc': 'Address Line 18'},
        {'id': 'a19', 'name': 'A19', 'desc': 'Address Line 19'},
        {'id': 's0',  'name': 'S0',  'desc': 'Status Line 0'},
        {'id': 's1',  'name': 'S1',  'desc': 'Status Line 1'},
        {'id': 's2',  'name': 'S2',  'desc': 'Status Line 2'},
        {'id': 'qs0', 'name': 'QS0', 'desc': 'Queue Status Line 0'},
        {'id': 'qs1', 'name': 'QS1', 'desc': 'Queue Status Line 1'},    
        {'id': 'rdy', 'name': 'RDY', 'desc': 'READY input'},
        {'id': 'clk', 'name': 'CLK', 'desc': 'CPU Clock'},       
    )
    options = ()
    annotations = (
        ('ale', 'Address Latch Enable'),
        ('address-latch', 'Address Latch'),
        ('bus-status', 'Bus Status'),
        ('bus-status-latch', 'Bus Status Latch'),
        ('qs-f', 'Queue Status: F'),
        ('qs-e', 'Queue Status: S'),
        ('qs-s', 'Queue Status: E'),
        ('ti', 'Ti'),
        ('t1', 'T1'),
        ('t2', 'T2'),
        ('t3', 'T3'),
        ('tw', 'Tw'),
        ('t4', 'T4'),
        ('d', 'Data Bus'),
        ('cf', 'Code Fetch'),
        ('mm', 'Mnemonic'),
        ('piq', 'Processor Instruction Queue'),
        ('ib', 'Instruction Bytes'),
        ('dbg', 'Debug Msg'),
        ('err', 'Error Msg'),
    )
    annotation_rows = (
        ('ALE', 'Address Latch Enable', (Annot.ALE,)),
        ('AL', 'Address Latch', (Annot.AddrLatch,)),
        ('BUS', 'Bus Status', (Annot.BusStatus,)),
        ('BUSL', 'Bus Status Latch', (Annot.BusStatusL,)),
        ('QOP', 'Queue Status', (Annot.QsF, Annot.QsS, Annot.QsE,)),
        ('TCYC', 'CPU T-cycle', (Annot.TI, Annot.T1, Annot.T2, Annot.T3, Annot.TW, Annot.T4,)),
        ('DATA', 'Data Bus', (Annot.D,)),
        ('FETCH', 'Code Fetch', (Annot.F,)),
        ('PIQ', 'Instruction Queue', (Annot.Piq,)),
        ('IB', 'Instruction Bytes', (Annot.Ib,)),
        ('INST', 'Instruction', (Annot.Mm,)),
        ('DBG', 'Debug', (Annot.Dbg,Annot.Err,)),
    )

    def __init__(self):
        self.reset()

        self.disasm = Disassembler()

    def reset(self):
        self.items = []
        self.saved_item = None
        self.ss_item = self.es_item = None
        self.saved_word = None
        self.ss_word = self.es_word = None
        self.first = True

    def start(self):
        self.out_python = self.register(srd.OUTPUT_PYTHON)
        self.out_ann = self.register(srd.OUTPUT_ANN)
        
        # 8088 specific state
        self.last_pins = [0] * PINS_LEN 

        self.ale = False            # Address Latch Enable
        self.al = None              # Address Latch
        self.al_ss = TrackedValue()
        self.al_annotation = TrackedValue('')

        self.cycle_sample = TrackedValue(0)

        # Bus status
        self.bus_status = TrackedValue()         # Bus Status
        self.bus_status_ss = TrackedValue()      # Bus cycle sample number
        self.bus_status_latch = TrackedValue()   # Bus Status Latch
        
        self.t_state = TState.TI    # CPU T-state
        self.prev_queue_ss = 0
        self.queue_status = TrackedValue(0)

        self.data_valid = False
        self.data_bus = 0

        self.queue = deque()
        self.opcode = TrackedValue(0)
        self.instr_ss = TrackedValue()
        self.instr = deque()
        self.mnemonic = TrackedValue('')

    def putpb(self, data):
        self.put(self.ss_item, self.es_item, self.out_python, data)

    def putb(self, data):
        self.put(self.ss_item, self.es_item, self.out_ann, data)

    def putpw(self, data):
        self.put(self.ss_word, self.es_word, self.out_python, data)

    def putw(self, data):
        self.put(self.ss_word, self.es_word, self.out_ann, data)

    def save_pins(self, pins):
        for i in range(PINS_LEN):
            self.last_pins[i] = pins[i] 

    def cycle_annot(self, annot, text):
        self.put(
            self.cycle_sample.prev, 
            self.samplenum, 
            self.out_ann, 
            [annot, ["{}".format(text)]]
        )

    def debug_annot(self, text):
        self.put(
            self.cycle_sample.prev, 
            self.samplenum, 
            self.out_ann, 
            [Annot.Dbg, ["{}".format(text)]]
        )

    def error_annot(self, text):
        self.put(
            self.cycle_sample.prev, 
            self.samplenum, 
            self.out_ann, 
            [Annot.Err, ["{}".format(text)]]
        )

    def decode_status(self, pins):
        
        # Decode bus status pins S0-S2 to BUS status.
        if (
            self.last_pins[Status.S0] != pins[Status.S0]
            or self.last_pins[Status.S1] != pins[Status.S1]
            or self.last_pins[Status.S2] != pins[Status.S2]
        ):
            # Any one of the three status lines has changed...

            # Emit annotation for the previous bus state.
            if self.bus_status_ss.cur is not None and self.bus_status.prev is not None:
                self.put(
                    self.bus_status_ss.cur,
                    self.samplenum,
                    self.out_ann,
                    [Annot.BusStatus, [BUS_STATES[self.bus_status.cur]]],
                )
    
            # Decode the bus status pins to an integer.
            self.bus_status.update(reduce_bus(pins[Status.S0 : Status.S2 + 1]))
            self.bus_status_ss.update(self.samplenum)
            
            if self.bus_status.prev is not None:
                if self.bus_status.prev == 7 and self.bus_status.cur != 7:
                    # New bus cycle is beginning, indicating the ALE signal should go high.
                    self.ale = True
                    self.al_ss.update(self.samplenum)
                    # Latch the bus status. The instantaneous bus status goes LOW on T3, but
                    # we want to remember it to detect type of reads and writes.
                    self.bus_status_latch.update(self.bus_status.cur)

    def display_queue(self):
        if len(self.queue) > 0:
            hex_string = ''.join(format(byte, '02X') for byte in self.queue)
            self.put(
                self.cycle_sample.prev, 
                self.samplenum, 
                self.out_ann, 
                [Annot.Piq, [hex_string]],
            )
        else:
            self.put(
                self.cycle_sample.prev, 
                self.samplenum, 
                self.out_ann, 
                [Annot.Piq, ['-']],
            )

    def instr_string(self):
        return ''.join(format(byte, '02X') for byte in self.instr)

    def display_instr(self):
        if len(self.instr) > 0:
            hex_string = self.instr_string()
            self.put(
                self.cycle_sample.prev, 
                self.samplenum, 
                self.out_ann, 
                [Annot.Ib, [hex_string]],
            )
        else:
            self.put(
                self.cycle_sample.prev, 
                self.samplenum, 
                self.out_ann, 
                [Annot.Ib, ['-']],
            )            

    def queue_push(self, byte):
        if len(self.queue) > 3:
            raise IndexError("This is a custom error message")
        else:
            self.queue.append(byte)

    def queue_pop(self):
        return self.queue.popleft()

    def instr_push(self, byte):
        if len(self.instr) < 8:
            self.instr.append(byte)

    def decode_queue(self, pins):
        if self.queue_status.update(reduce_bus(pins[Pin.QS0:Pin.QS1+1])).changed():
            if self.queue_status.prev != 0:
                self.cycle_annot(
                    QUEUE_ANNOTS[self.queue_status.prev],
                    QUEUE_STATES[self.queue_status.prev]    
                )

            if self.queue_status.prev == QueueOp.Empty:
                self.queue.clear()
                self.debug_annot("q_e")
                self.display_queue()

            try:
                if self.queue_status.cur == QueueOp.First:
                    # Update disassembly and clear instruction deque
                    self.instr_ss.update(self.samplenum)
                    self.opcode.update(self.instr[0])
                    try:
                        self.mnemonic.update(self.disasm.disassemble(list(self.instr)))
                    except:
                        self.mnemonic.update("inval")
                        self.error_annot("e:{}".format(e))
                    self.put(
                        self.instr_ss.prev,
                        self.cycle_sample.cur,
                        self.out_ann,
                        [Annot.Mm, ["{:02X}:{}".format(self.opcode.cur, self.mnemonic.cur)]],
                    )
                    self.instr.clear()
            except Exception as e:
                self.error_annot("e: {}".format(e))

            if self.queue_status.prev == QueueOp.First or self.queue_status.prev == QueueOp.Subs:
                try:
                    qb = self.queue_pop()
                    self.opcode.update(qb)
                    self.instr_push(qb)
                    self.display_queue()
                    self.display_instr()
                    self.debug_annot("q_rd:{:02X}".format(qb))
                except:
                    self.error_annot("q_err_uf")

    def advance_t_state(self, pins):
        # Check if we should transition to T1.
        if self.ale == True and self.t_state != TState.T1:
            self.t_state = TState.T1
        else:
            if self.t_state == TState.T1:
                # Clear the ALE state - it should only be active for one cycle.
                self.ale = False
                self.cycle_annot(Annot.ALE, "ALE")
                self.t_state = TState.T2
            elif self.t_state == TState.T2:
                self.t_state = TState.T3
                if pins[Pin.RDY] == 1:
                    self.data_valid = True
            elif self.t_state == TState.T3:
                if pins[Pin.RDY] == 1:
                    self.t_state = TState.T4
                else:
                    self.t_state = TState.TW
            elif self.t_state == TState.TW:
                if self.last_pins[Pin.RDY] == 0:
                    if pins[Pin.RDY] == 1:
                        self.data_valid = True
                else:
                    self.t_state = TState.T4
            elif self.t_state == TState.T4:
                self.t_state = TState.TI

    def set_address_latch(self, pins):
        if self.ale:
            #self.debug_annot(Annot.ALE, "ALE")
            if self.al_ss.prev is not None:

                # Decode entire address bus and save sample 
                self.al_annotation.update('%05X' % reduce_bus(pins[Addr.AD0:Addr.A19+1]))

                self.put(
                    self.al_ss.prev, 
                    self.samplenum, 
                    self.out_ann, 
                    [Annot.AddrLatch, [self.al_annotation.prev]]
                )

    def decode_data(self, pins):
        self.data_bus = reduce_bus(pins[Pin.AD0:Pin.AD7+1])
        self.cycle_annot(Annot.D, '%02X' % self.data_bus)

    def fetch(self):
        self.cycle_annot(Annot.F, '%02X' % self.data_bus)
        try:
            self.queue_push(self.data_bus)
        except:
            self.error_annot('q_err_of')

        self.display_queue()
        #inst = self.disasm.disassemble([self.data_bus])
        #self.cycle_annot(Annot.Mm, inst)

    def decode(self):
        while True:
            # Wait for rising edge on the CPU clock.
            pins = self.wait({Pin.CLK: 'r'})
            
            self.cycle_sample.update(self.samplenum)

            self.decode_status(pins)

            self.cycle_annot(T_ANNOTS[self.t_state], T_STATES[self.t_state])

            if self.data_valid:
                self.decode_data(pins)
                if self.bus_status_latch.cur == BusStatus.CODE:
                    # This was a code fetch. Process it.
                    self.fetch()

                self.data_valid = False

            self.decode_queue(pins)
            self.advance_t_state(pins)
            self.set_address_latch(pins)
            self.save_pins(pins)

sigrok_decoders/i8088/pd.py
dbalsom/marty_tools