# SPDX-License-Identifier: CERN-OHL-S-2.0
"""

Copyright (c) 2025 FPGA Ninja, LLC

Authors:
- Alex Forencich

"""

import array
import datetime
import logging
import struct
from collections import deque

import cocotb
from cocotb.queue import Queue
from cocotb.triggers import RisingEdge


# Command opcodes
CNDM_CMD_OP_NOP = 0x0000

CNDM_CMD_OP_CFG        = 0x0100

CNDM_CMD_OP_ACCESS_REG = 0x0180
CNDM_CMD_OP_PTP        = 0x0190
CNDM_CMD_OP_HWID       = 0x01A0
CNDM_CMD_OP_HWMON      = 0x01B0
CNDM_CMD_OP_PLL        = 0x01C0

CNDM_CMD_OP_CREATE_EQ  = 0x0200
CNDM_CMD_OP_MODIFY_EQ  = 0x0201
CNDM_CMD_OP_QUERY_EQ   = 0x0202
CNDM_CMD_OP_DESTROY_EQ = 0x0203

CNDM_CMD_OP_CREATE_CQ  = 0x0210
CNDM_CMD_OP_MODIFY_CQ  = 0x0211
CNDM_CMD_OP_QUERY_CQ   = 0x0212
CNDM_CMD_OP_DESTROY_CQ = 0x0213

CNDM_CMD_OP_CREATE_SQ  = 0x0220
CNDM_CMD_OP_MODIFY_SQ  = 0x0221
CNDM_CMD_OP_QUERY_SQ   = 0x0222
CNDM_CMD_OP_DESTROY_SQ = 0x0223

CNDM_CMD_OP_CREATE_RQ  = 0x0230
CNDM_CMD_OP_MODIFY_RQ  = 0x0231
CNDM_CMD_OP_QUERY_RQ   = 0x0232
CNDM_CMD_OP_DESTROY_RQ = 0x0233

CNDM_CMD_OP_CREATE_QP  = 0x0240
CNDM_CMD_OP_MODIFY_QP  = 0x0241
CNDM_CMD_OP_QUERY_QP   = 0x0242
CNDM_CMD_OP_DESTROY_QP = 0x0243

CNDM_CMD_REG_FLG_WRITE  = 0x00000001
CNDM_CMD_REG_FLG_RAW    = 0x00000100

CNDM_CMD_PTP_FLG_SET_TOD    = 0x00000001
CNDM_CMD_PTP_FLG_OFFSET_TOD = 0x00000002
CNDM_CMD_PTP_FLG_SET_REL    = 0x00000004
CNDM_CMD_PTP_FLG_OFFSET_REL = 0x00000008
CNDM_CMD_PTP_FLG_OFFSET_FNS = 0x00000010
CNDM_CMD_PTP_FLG_SET_PERIOD = 0x00000080


# Board operation commands
CNDM_CMD_BRD_OP_NOP = 0x0000

CNDM_CMD_BRD_OP_FLASH_RD  = 0x0100
CNDM_CMD_BRD_OP_FLASH_WR  = 0x0101
CNDM_CMD_BRD_OP_FLASH_CMD = 0x0108

CNDM_CMD_BRD_OP_EEPROM_RD = 0x0200
CNDM_CMD_BRD_OP_EEPROM_WR = 0x0201

CNDM_CMD_BRD_OP_OPTIC_RD = 0x0300
CNDM_CMD_BRD_OP_OPTIC_WR = 0x0301

CNDM_CMD_BRD_OP_HWID_SN_RD  = 0x0400
CNDM_CMD_BRD_OP_HWID_VPD_RD = 0x0410
CNDM_CMD_BRD_OP_HWID_MAC_RD = 0x0480

CNDM_CMD_BRD_OP_PLL_STATUS_RD   = 0x0500
CNDM_CMD_BRD_OP_PLL_TUNE_RAW_RD = 0x0502
CNDM_CMD_BRD_OP_PLL_TUNE_RAW_WR = 0x0503
CNDM_CMD_BRD_OP_PLL_TUNE_PPT_RD = 0x0504
CNDM_CMD_BRD_OP_PLL_TUNE_PPT_WR = 0x0505

CNDM_CMD_BRD_OP_I2C_RD = 0x8100
CNDM_CMD_BRD_OP_I2C_WR = 0x8101


class Eq:
    def __init__(self, driver, port):
        self.driver = driver
        self.log = driver.log

        self.port = port

        self.log_size = 0
        self.size = 0
        self.size_mask = 0
        self.stride = 0
        self.eqn = None
        self.enabled = False

        self.buf_size = 0
        self.buf_region = None
        self.buf_dma = 0
        self.buf = None

        self.irqn = None

        self.cq_table = {}

        self.cons_ptr = None

        self.db_offset = None
        self.hw_regs = self.driver.hw_regs

    async def open(self, irqn, size):
        if self.eqn is not None:
            raise Exception("Already open")

        self.log_size = size.bit_length() - 1
        self.size = 2**self.log_size
        self.size_mask = self.size-1
        self.stride = 16

        self.buf_size = self.size*self.stride
        self.buf_region = self.driver.pool.alloc_region(self.buf_size)
        self.buf_dma = self.buf_region.get_absolute_address(0)
        self.buf = self.buf_region.mem

        self.buf[0:self.buf_size] = b'\x00'*self.buf_size

        self.cons_ptr = 0

        self.irqn = irqn

        self.cq_table = {}

        rsp = await self.driver.exec_cmd(struct.pack("<HHLLLLLLLQQLLLL",
            0, # rsvd
            CNDM_CMD_OP_CREATE_EQ, # opcode
            0x00000000, # flags
            self.port.index, # port
            0, # eqn
            self.irqn, # irqn
            0, # pd
            self.log_size, # size
            0, # dboffs
            self.buf_dma, # base addr
            0, # ptr2
            0, # prod_ptr
            0, # cons_ptr
            0, # rsvd
            0, # rsvd
        ))

        rsp_unpacked = struct.unpack("<HHLLLLLLLQQLLLL", rsp)
        print(rsp_unpacked)
        self.eqn = rsp_unpacked[4]
        self.db_offset = rsp_unpacked[8]

        if self.db_offset == 0:
            self.eqn = None
            self.db_offset = None
            self.log.error("Failed to allocate EQ")
            return

        await self.write_cons_ptr_arm()

        self.log.info("Opened EQ %d", self.eqn)
        self.log.info("Using doorbell at offset 0x%08x", self.db_offset)

        self.enabled = True

    async def close(self):
        if self.eqn is None:
            return

        self.enabled = False

        rsp = await self.driver.exec_cmd(struct.pack("<HHLLLLLLLQQLLLL",
            0, # rsvd
            CNDM_CMD_OP_DESTROY_EQ, # opcode
            0x00000000, # flags
            self.port.index, # port
            self.eqn, # eqn
            0, # irqn
            0, # pd
            0, # size
            0, # dboffs
            0, # base addr
            0, # ptr2
            0, # prod_ptr
            0, # cons_ptr
            0, # rsvd
            0, # rsvd
        ))

        self.eqn = None

        # TODO free buffer

    def attach_cq(self, cq):
        self.cq_table[cq.cqn] = cq

    def detach_cq(self, cq):
        del self.cq_table[cq.cqn]

    async def write_cons_ptr(self):
        await self.hw_regs.write_dword(self.db_offset, self.cons_ptr & 0xffff)

    async def write_cons_ptr_arm(self):
        await self.hw_regs.write_dword(self.db_offset, (self.cons_ptr & 0xffff) | 0x80000000)

    async def process_eq(self):
        self.log.info("Process EQ")

        eq_cons_ptr = self.cons_ptr
        eq_index = eq_cons_ptr & self.size_mask

        while True:
            # event_data = struct.unpack_from("<HHLLLLLLL", self.buf, eq_index*self.stride)
            event_data = struct.unpack_from("<HHLLL", self.buf, eq_index*self.stride)

            self.log.info("EQ %d index %d data: %s", self.eqn, eq_index, repr(event_data))

            if bool(event_data[-1] & 0x80000000) == bool(eq_cons_ptr & self.size):
                self.log.info("EQ %d empty", self.eqn)
                break

            if event_data[1] == 0x0000:
                # completion
                self.log.info("Event from CQ %d", event_data[2])
                cq = self.cq_table[event_data[2]]
                await cq.handler(cq)

            eq_cons_ptr += 1
            eq_index = eq_cons_ptr & self.size_mask

        self.cons_ptr = eq_cons_ptr
        await self.write_cons_ptr_arm()


class Cq:
    def __init__(self, driver, port):
        self.driver = driver
        self.log = driver.log

        self.port = port

        self.log_size = 0
        self.size = 0
        self.size_mask = 0
        self.stride = 0
        self.cqn = None
        self.enabled = False

        self.buf_size = 0
        self.buf_region = None
        self.buf_dma = 0
        self.buf = None

        self.eq = None
        self.irqn = None

        self.src_ring = None
        self.handler = None

        self.cons_ptr = None

        self.db_offset = None
        self.hw_regs = self.driver.hw_regs

    async def open(self, eq, size):
        if self.cqn is not None:
            raise Exception("Already open")

        self.log_size = size.bit_length() - 1
        self.size = 2**self.log_size
        self.size_mask = self.size-1
        self.stride = 16

        self.buf_size = self.size*self.stride
        self.buf_region = self.driver.pool.alloc_region(self.buf_size)
        self.buf_dma = self.buf_region.get_absolute_address(0)
        self.buf = self.buf_region.mem

        self.buf[0:self.buf_size] = b'\x00'*self.buf_size

        self.cons_ptr = 0

        if isinstance(eq, Eq):
            self.eq = eq
            dqn = eq.eqn
        else:
            self.irqn = eq
            dqn = eq | 0x80000000

        rsp = await self.driver.exec_cmd(struct.pack("<HHLLLLLLLQQLLLL",
            0, # rsvd
            CNDM_CMD_OP_CREATE_CQ, # opcode
            0x00000000, # flags
            self.port.index, # port
            0, # cqn
            dqn, # eqn
            0, # pd
            self.log_size, # size
            0, # dboffs
            self.buf_dma, # base addr
            0, # ptr2
            0, # prod_ptr
            0, # cons_ptr
            0, # rsvd
            0, # rsvd
        ))

        rsp_unpacked = struct.unpack("<HHLLLLLLLQQLLLL", rsp)
        print(rsp_unpacked)
        self.cqn = rsp_unpacked[4]
        self.db_offset = rsp_unpacked[8]

        if self.db_offset == 0:
            self.cqn = None
            self.db_offset = None
            self.log.error("Failed to allocate CQ")
            return

        if self.eq:
            self.eq.attach_cq(self)

        await self.write_cons_ptr_arm()

        self.log.info("Opened CQ %d", self.cqn)
        self.log.info("Using doorbell at offset 0x%08x", self.db_offset)

        self.enabled = True

    async def close(self):
        if self.cqn is None:
            return

        self.enabled = False

        rsp = await self.driver.exec_cmd(struct.pack("<HHLLLLLLLQQLLLL",
            0, # rsvd
            CNDM_CMD_OP_DESTROY_CQ, # opcode
            0x00000000, # flags
            self.port.index, # port
            self.cqn, # cqn
            0, # eqn
            0, # pd
            0, # size
            0, # dboffs
            0, # base addr
            0, # ptr2
            0, # prod_ptr
            0, # cons_ptr
            0, # rsvd
            0, # rsvd
        ))

        self.cqn = None

        # TODO free buffer

    async def write_cons_ptr(self):
        await self.hw_regs.write_dword(self.db_offset, self.cons_ptr & 0xffff)

    async def write_cons_ptr_arm(self):
        await self.hw_regs.write_dword(self.db_offset, (self.cons_ptr & 0xffff) | 0x80000000)


class Sq:
    def __init__(self, driver, port):
        self.driver = driver
        self.log = driver.log

        self.port = port

        self.log_size = 0
        self.size = 0
        self.size_mask = 0
        self.full_size = 0
        self.stride = 0
        self.sqn = None
        self.enabled = False

        self.buf_size = 0
        self.buf_region = None
        self.buf_dma = 0
        self.buf = None

        self.cq = None

        self.prod_ptr = None
        self.cons_ptr = None

        self.packets = 0
        self.bytes = 0

        self.db_offset = None
        self.hw_regs = self.driver.hw_regs

    async def open(self, cq, size):
        if self.sqn is not None:
            raise Exception("Already open")

        self.log_size = size.bit_length() - 1
        self.size = 2**self.log_size
        self.size_mask = self.size-1
        self.stride = 16

        self.tx_info = [None]*self.size

        self.buf_size = self.size*self.stride
        self.buf_region = self.driver.pool.alloc_region(self.buf_size)
        self.buf_dma = self.buf_region.get_absolute_address(0)
        self.buf = self.buf_region.mem

        self.buf[0:self.buf_size] = b'\x00'*self.buf_size

        self.prod_ptr = 0
        self.cons_ptr = 0

        self.cq = cq
        self.cq.src_ring = self
        self.cq.handler = Sq.process_tx_cq

        rsp = await self.driver.exec_cmd(struct.pack("<HHLLLLLLLQQLLLL",
            0, # rsvd
            CNDM_CMD_OP_CREATE_SQ, # opcode
            0x00000000, # flags
            self.port.index, # port
            0, # sqn
            self.cq.cqn, # cqn
            0, # pd
            self.log_size, # size
            0, # dboffs
            self.buf_dma, # base addr
            0, # ptr2
            0, # prod_ptr
            0, # cons_ptr
            0, # rsvd
            0, # rsvd
        ))

        rsp_unpacked = struct.unpack("<HHLLLLLLLQQLLLL", rsp)
        print(rsp_unpacked)
        self.sqn = rsp_unpacked[4]
        self.db_offset = rsp_unpacked[8]

        if self.db_offset == 0:
            self.sqn = None
            self.db_offset = None
            self.log.error("Failed to allocate SQ")
            return

        self.log.info("Opened SQ %d (CQ %d)", self.sqn, cq.cqn)
        self.log.info("Using doorbell at offset 0x%08x", self.db_offset)

        self.enabled = True

    async def close(self):
        if self.sqn is None:
            return

        self.enabled = False

        rsp = await self.driver.exec_cmd(struct.pack("<HHLLLLLLLQQLLLL",
            0, # rsvd
            CNDM_CMD_OP_DESTROY_SQ, # opcode
            0x00000000, # flags
            self.port.index, # port
            self.sqn, # sqn
            0, # eqn
            0, # pd
            0, # size
            0, # dboffs
            0, # base addr
            0, # ptr2
            0, # prod_ptr
            0, # cons_ptr
            0, # rsvd
            0, # rsvd
        ))

        self.sqn = None

        # TODO free buffer

    def is_ring_empty(self):
        return self.prod_ptr == self.cons_ptr

    def is_ring_full(self):
        return ((self.prod_ptr - self.cons_ptr) & 0xffffffff) > self.size

    async def write_prod_ptr(self):
        await self.hw_regs.write_dword(self.db_offset, self.prod_ptr & 0xffff)

    async def start_xmit(self, data):
        headroom = 10
        tx_buf = self.driver.alloc_pkt()
        await tx_buf.write(headroom, data)
        index = self.prod_ptr & self.size_mask
        ptr = tx_buf.get_absolute_address(0)
        struct.pack_into('<xxxxLQ', self.buf, 16*index, len(data), ptr+headroom)
        self.tx_info[index] = tx_buf
        self.prod_ptr += 1
        await self.write_prod_ptr()

    def free_tx_desc(self, index):
        pkt = self.tx_info[index]
        self.driver.free_pkt(pkt)
        self.tx_info[index] = None

    def free_tx_buf(self):
        while not self.is_ring_empty():
            index = self.cons_ptr & self.size_mask
            self.free_tx_desc(index)
            self.cons_ptr += 1

    @staticmethod
    async def process_tx_cq(cq):
        sq = cq.src_ring

        cq.log.info("Process CQ %d for SQ %d", cq.cqn, sq.sqn)

        cq_cons_ptr = cq.cons_ptr
        cons_ptr = sq.cons_ptr

        while True:
            cq_index = cq_cons_ptr & cq.size_mask
            index = cons_ptr & sq.size_mask

            cpl_data = struct.unpack_from("<LLLL", cq.buf, cq_index*16)

            cq.log.info("TX CQ index %d data %s", cq_index, cpl_data)

            if bool(cpl_data[-1] & 0x80000000) == bool(cq_cons_ptr & cq.size):
                cq.log.info("CQ empty")
                break

            pkt = sq.tx_info[index]

            sq.free_tx_desc(index)

            cq_cons_ptr += 1
            cons_ptr += 1

        cq.cons_ptr = cq_cons_ptr
        sq.cons_ptr = cons_ptr

        await cq.write_cons_ptr_arm()


class Rq:
    def __init__(self, driver, port):
        self.driver = driver
        self.log = driver.log

        self.port = port

        self.log_size = 0
        self.size = 0
        self.size_mask = 0
        self.full_size = 0
        self.stride = 0
        self.rqn = None
        self.enabled = False

        self.buf_size = 0
        self.buf_region = None
        self.buf_dma = 0
        self.buf = None

        self.cq = None

        self.prod_ptr = None
        self.cons_ptr = None

        self.packets = 0
        self.bytes = 0

        self.db_offset = None
        self.hw_regs = self.driver.hw_regs

    async def open(self, cq, size):
        if self.rqn is not None:
            raise Exception("Already open")

        self.log_size = size.bit_length() - 1
        self.size = 2**self.log_size
        self.size_mask = self.size-1
        self.stride = 16

        self.rx_info = [None]*self.size

        self.buf_size = self.size*self.stride
        self.buf_region = self.driver.pool.alloc_region(self.buf_size)
        self.buf_dma = self.buf_region.get_absolute_address(0)
        self.buf = self.buf_region.mem

        self.buf[0:self.buf_size] = b'\x00'*self.buf_size

        self.prod_ptr = 0
        self.cons_ptr = 0

        self.cq = cq
        self.cq.src_ring = self
        self.cq.handler = Rq.process_rx_cq

        rsp = await self.driver.exec_cmd(struct.pack("<HHLLLLLLLQQLLLL",
            0, # rsvd
            CNDM_CMD_OP_CREATE_RQ, # opcode
            0x00000000, # flags
            self.port.index, # port
            0, # rqn
            self.cq.cqn, # cqn
            0, # pd
            self.log_size, # size
            0, # dboffs
            self.buf_dma, # base addr
            0, # ptr2
            0, # prod_ptr
            0, # cons_ptr
            0, # rsvd
            0, # rsvd
        ))

        rsp_unpacked = struct.unpack("<HHLLLLLLLQQLLLL", rsp)
        print(rsp_unpacked)
        self.rqn = rsp_unpacked[4]
        self.db_offset = rsp_unpacked[8]

        if self.db_offset == 0:
            self.rqn = None
            self.db_offset = None
            self.log.error("Failed to allocate RQ")
            return

        self.log.info("Opened RQ %d (CQ %d)", self.rqn, cq.cqn)
        self.log.info("Using doorbell at offset 0x%08x", self.db_offset)

        self.enabled = True

        await self.refill_rx_buffers()

    async def close(self):
        if self.rqn is None:
            return

        self.enabled = False

        rsp = await self.driver.exec_cmd(struct.pack("<HHLLLLLLLQQLLLL",
            0, # rsvd
            CNDM_CMD_OP_DESTROY_RQ, # opcode
            0x00000000, # flags
            self.port.index, # port
            self.rqn, # rqn
            0, # eqn
            0, # pd
            0, # size
            0, # dboffs
            0, # base addr
            0, # ptr2
            0, # prod_ptr
            0, # cons_ptr
            0, # rsvd
            0, # rsvd
        ))

        self.rqn = None

        # TODO free buffer

    def is_ring_empty(self):
        return self.prod_ptr == self.cons_ptr

    def is_ring_full(self):
        return ((self.prod_ptr - self.cons_ptr) & 0xffffffff) > self.size

    async def write_prod_ptr(self):
        await self.hw_regs.write_dword(self.db_offset, self.prod_ptr & 0xffff)

    def free_rx_desc(self, index):
        pkt = self.rx_info[index]
        self.driver.free_pkt(pkt)
        self.rx_info[index] = None

    def free_rx_buf(self):
        while not self.is_ring_empty():
            index = self.cons_ptr & self.size_mask
            self.free_rx_desc(index)
            self.cons_ptr += 1

    def prepare_rx_desc(self, index):
        pkt = self.driver.alloc_pkt()
        self.rx_info[index] = pkt

        length = pkt.size
        ptr = pkt.get_absolute_address(0)

        struct.pack_into('<xxxxLQ', self.buf, 16*index, length, ptr)

    async def refill_rx_buffers(self):
        missing = self.size - (self.prod_ptr - self.cons_ptr)

        if missing < 8:
            return

        for k in range(missing):
            self.prepare_rx_desc(self.prod_ptr & self.size_mask)
            self.prod_ptr += 1

        await self.write_prod_ptr()

    @staticmethod
    async def process_rx_cq(cq):
        rq = cq.src_ring

        cq.log.info("Process CQ %d for RQ %d", cq.cqn, rq.rqn)

        cq_cons_ptr = cq.cons_ptr
        cons_ptr = rq.cons_ptr

        while True:
            cq_index = cq_cons_ptr & cq.size_mask
            index = cons_ptr & rq.size_mask

            cpl_data = struct.unpack_from("<LLLL", cq.buf, cq_index*16)

            rq.log.info("RX CQ index %d data %s", cq_index, cpl_data)

            if bool(cpl_data[-1] & 0x80000000) == bool(cq_cons_ptr & cq.size):
                rq.log.info("CQ empty")
                break

            pkt = rq.rx_info[index]
            length = cpl_data[1]

            data = pkt[:length]

            rq.log.info("Packet: %s", data)

            rq.port.rx_queue.put_nowait(data)

            rq.free_rx_desc(index)

            cq_cons_ptr += 1
            cons_ptr += 1

        cq.cons_ptr = cq_cons_ptr
        rq.cons_ptr = cons_ptr

        await rq.refill_rx_buffers()
        await cq.write_cons_ptr_arm()


class Port:
    def __init__(self, driver, index):
        self.driver = driver
        self.log = driver.log
        self.index = index
        self.hw_regs = driver.hw_regs

        self.eq_count = 1
        self.eq = []

        self.rxq_count = 1
        self.rxq = []

        self.txq_count = 1
        self.txq = []

        self.rx_queue = Queue()

    async def init(self):
        for k in range(self.eq_count):
            eq = Eq(self.driver, self)
            await eq.open(self.index, 256)
            self.eq.append(eq)

        await self.open()

    async def open(self):
        for k in range(self.rxq_count):
            cq = Cq(self.driver, self)
            await cq.open(self.eq[0], 256)

            q = Rq(self.driver, self)
            await q.open(cq, 256)

            self.rxq.append(q)

        for k in range(self.txq_count):
            cq = Cq(self.driver, self)
            await cq.open(self.eq[0], 256)

            q = Sq(self.driver, self)
            await q.open(cq, 256)

            self.txq.append(q)

    async def start_xmit(self, data, tx_ring=0):
        await self.txq[tx_ring].start_xmit(data)

    async def recv(self):
        return await self.rx_queue.get()

    async def recv_nowait(self):
        return self.rx_queue.get_nowait()


class Interrupt:
    def __init__(self, index, handler=None):
        self.index = index
        self.queue = Queue()
        self.handler = handler
        self.signal = None

        cocotb.start_soon(self._run())

    @classmethod
    def from_edge(cls, index, signal, handler=None):
        obj = cls(index, handler)
        obj.signal = signal
        cocotb.start_soon(obj._run_edge())
        return obj

    async def interrupt(self):
        self.queue.put_nowait(None)

    async def _run(self):
        while True:
            await self.queue.get()
            if self.handler:
                await self.handler(self.index)

    async def _run_edge(self):
        while True:
            await RisingEdge(self.signal)
            await self.interrupt()


class Driver:
    def __init__(self):
        self.log = logging.getLogger("cocotb.cndm")

        self.dev = None
        self.pool = None
        self.hw_regs = None

        self.irq_list = []

        self.port_count = None

        self.ports = []

        self.free_packets = deque()
        self.allocated_packets = []

        # config
        self.cfg_page_max = None
        self.cmd_ver = None

        # FW ID
        self.fpga_id = None
        self.fw_id = None
        self.fw_ver = None
        self.board_id = None
        self.board_ver = None
        self.build_date = None
        self.git_hash = None
        self.release_info = None

        # HW config
        self.sys_clk_per_ns_num = None
        self.sys_clk_per_ns_den = None
        self.ptp_clk_per_ns_num = None
        self.ptp_clk_per_ns_den = None

        # Resources
        self.log_max_eq = None
        self.log_max_eq_sz = None
        self.eq_pool = None
        self.eqe_ver = None
        self.log_max_cq = None
        self.log_max_cq_sz = None
        self.cq_pool = None
        self.cqe_ver = None
        self.log_max_sq = None
        self.log_max_sq_sz = None
        self.sq_pool = None
        self.sqe_ver = None
        self.log_max_rq = None
        self.log_max_rq_sz = None
        self.rq_pool = None
        self.rqe_ver = None

    async def init_pcie_dev(self, dev):
        self.dev = dev
        self.pool = dev.rc.mem_pool

        await dev.enable_device()
        await dev.set_master()
        await dev.alloc_irq_vectors(32, 32)

        self.hw_regs = dev.bar_window[0]

        # set up MSI
        for index in range(32):
            irq = Interrupt(index, self.interrupt_handler)
            self.dev.request_irq(index, irq.interrupt)
            self.irq_list.append(irq)

        await self.init_common()

    async def init_common(self):

        # Get config information
        rsp = await self.exec_cmd(struct.pack("<HHLHHLLLLLLLLLLLLL",
            0, # rsvd
            CNDM_CMD_OP_CFG, # opcode
            0x00000000, # flags
            0, # cfg_page
            0, # num_cfg_pages
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
        ))

        rsp_unpacked = struct.unpack("<HHLHHLLLLLLLLLLLLL", rsp)
        print(rsp_unpacked)

        self.cfg_page_max = rsp_unpacked[4]
        self.cmd_ver = rsp_unpacked[5]

        self.log.info("Config pages: %d", self.cfg_page_max+1)
        self.log.info("Command version: %d.%d.%d", self.cmd_ver >> 20, (self.cmd_ver >> 12) & 0xff, self.cmd_ver & 0xfff)

        self.fpga_id = rsp_unpacked[10]
        self.fw_id = rsp_unpacked[11]
        self.fw_ver = rsp_unpacked[12]
        self.board_id = rsp_unpacked[13]
        self.board_ver = rsp_unpacked[14]
        self.build_date = rsp_unpacked[15]
        self.git_hash = rsp_unpacked[16]
        self.release_info = rsp_unpacked[17]

        self.log.info("FPGA JTAG ID: 0x%08x", self.fpga_id)
        self.log.info("FW ID: 0x%08x", self.fw_id)
        self.log.info("FW version: %d.%d.%d", self.fw_ver >> 20, (self.fw_ver >> 12) & 0xff, self.fw_ver & 0xfff)
        self.log.info("Board ID: 0x%08x", self.board_id)
        self.log.info("Board version: %d.%d.%d", self.board_ver >> 20, (self.board_ver >> 12) & 0xff, self.board_ver & 0xfff)
        self.log.info("Build date: %s UTC (raw: 0x%08x)", datetime.datetime.fromtimestamp(self.build_date, datetime.timezone.utc).isoformat(' '), self.build_date)
        self.log.info("Git hash: %08x", self.git_hash)
        self.log.info("Release info: %08x", self.release_info)

        # Get config information
        rsp = await self.exec_cmd(struct.pack("<HHLHHLLLLLLLLLLLLL",
            0, # rsvd
            CNDM_CMD_OP_CFG, # opcode
            0x00000000, # flags
            1, # cfg_page
            0, # num_cfg_pages
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
        ))

        rsp_unpacked = struct.unpack("<HHLHHLLLLLLLLLLLLL", rsp)
        print(rsp_unpacked)

        self.port_count = rsp_unpacked[10] & 0xff
        self.sys_clk_per_ns_den = rsp_unpacked[14] & 0xffff
        self.sys_clk_per_ns_num = rsp_unpacked[14] >> 16
        self.ptp_clk_per_ns_den = rsp_unpacked[15] & 0xffff
        self.ptp_clk_per_ns_num = rsp_unpacked[15] >> 16

        self.log.info("Port count: %d", self.port_count)

        self.log.info("Sys clock period: %f MHz (raw %d/%d ns)",
            1000/(self.sys_clk_per_ns_num/self.sys_clk_per_ns_den),
            self.sys_clk_per_ns_num, self.sys_clk_per_ns_den)
        self.log.info("PTP clock period: %f MHz (raw %d/%d ns)",
            1000/(self.ptp_clk_per_ns_num/self.ptp_clk_per_ns_den),
            self.ptp_clk_per_ns_num, self.ptp_clk_per_ns_den)

        # Get config information
        rsp = await self.exec_cmd(struct.pack("<HHLHHLLLLLLLLLLLLL",
            0, # rsvd
            CNDM_CMD_OP_CFG, # opcode
            0x00000000, # flags
            2, # cfg_page
            0, # num_cfg_pages
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
            0,
        ))

        rsp_unpacked = struct.unpack("<HHLHHLLLLLLLLLLLLL", rsp)
        print(rsp_unpacked)

        # Resources
        self.log_max_eq = rsp_unpacked[10] & 0xff
        self.log_max_eq_sz = (rsp_unpacked[10] >> 8) & 0xff
        self.eq_pool = (rsp_unpacked[10] >> 16) & 0xff
        self.eqe_ver = (rsp_unpacked[10] >> 24) & 0xff
        self.log_max_cq = rsp_unpacked[11] & 0xff
        self.log_max_cq_sz = (rsp_unpacked[11] >> 8) & 0xff
        self.cq_pool = (rsp_unpacked[11] >> 16) & 0xff
        self.cqe_ver = (rsp_unpacked[11] >> 24) & 0xff
        self.log_max_sq = rsp_unpacked[12] & 0xff
        self.log_max_sq_sz = (rsp_unpacked[12] >> 8) & 0xff
        self.sq_pool = (rsp_unpacked[12] >> 16) & 0xff
        self.sqe_ver = (rsp_unpacked[12] >> 24) & 0xff
        self.log_max_rq = rsp_unpacked[13] & 0xff
        self.log_max_rq_sz = (rsp_unpacked[13] >> 8) & 0xff
        self.rq_pool = (rsp_unpacked[13] >> 16) & 0xff
        self.rqe_ver = (rsp_unpacked[13] >> 24) & 0xff

        self.log.info("Max EQ count: %d (log %d)", 2**self.log_max_eq, self.log_max_eq)
        self.log.info("Max EQ size: %d (log %d)", 2**self.log_max_eq_sz, self.log_max_eq_sz)
        self.log.info("EQ pool: %d", self.eq_pool)
        self.log.info("EQE version: %d", self.eqe_ver)
        self.log.info("Max CQ count: %d (log %d)", 2**self.log_max_cq, self.log_max_cq)
        self.log.info("Max CQ size: %d (log %d)", 2**self.log_max_cq_sz, self.log_max_cq_sz)
        self.log.info("CQ pool: %d", self.cq_pool)
        self.log.info("CQE version: %d", self.cqe_ver)
        self.log.info("Max SQ count: %d (log %d)", 2**self.log_max_sq, self.log_max_sq)
        self.log.info("Max SQ size: %d (log %d)", 2**self.log_max_sq_sz, self.log_max_sq_sz)
        self.log.info("SQ pool: %d", self.sq_pool)
        self.log.info("SQE version: %d", self.sqe_ver)
        self.log.info("Max RQ count: %d (log %d)", 2**self.log_max_rq, self.log_max_rq)
        self.log.info("Max RQ size: %d (log %d)", 2**self.log_max_rq_sz, self.log_max_rq_sz)
        self.log.info("RQ pool: %d", self.rq_pool)
        self.log.info("RQE version: %d", self.rqe_ver)

        # Get PTP information
        rsp = await self.exec_cmd(struct.pack("<HHLLLQQQQQLL",
            0, # rsvd
            CNDM_CMD_OP_PTP, # opcode
            0x00000000, # flags
            0, # fns
            0, # tod_ns
            0, # tod_sec
            0, # rel_ns
            0, # ptm
            0, # nom_period
            0, # period
            0, # rsvd
            0, # rsvd
        ))

        rsp_unpacked = struct.unpack("<HHLLLQQQQQLL", rsp)
        print(rsp_unpacked)

        nom_period = rsp_unpacked[8]
        self.log.info("PHC nominal period: %.09f ns (raw 0x%x)", nom_period / 2**32, nom_period)

        # Test setting PTP time
        rsp = await self.exec_cmd(struct.pack("<HHLLLQQQQQLL",
            0, # rsvd
            CNDM_CMD_OP_PTP, # opcode
            CNDM_CMD_PTP_FLG_SET_TOD | CNDM_CMD_PTP_FLG_SET_REL | CNDM_CMD_PTP_FLG_SET_PERIOD, # flags
            0, # fns
            0x12345678, # tod_ns
            0x123456654321, # tod_sec
            0x112233445566, # rel_ns
            0, # ptm
            0, # nom_period
            nom_period, # period
            0, # rsvd
            0, # rsvd
        ))

        rsp_unpacked = struct.unpack("<HHLLLQQQQQLL", rsp)
        print(rsp_unpacked)

        for k in range(self.port_count):
            port = Port(self, k)
            await port.init()

            self.ports.append(port)

    async def access_reg(self, reg, raw, write=False, data=0):
        flags = 0
        if raw:
            flags |= CNDM_CMD_REG_FLG_RAW
        if write:
            flags |= CNDM_CMD_REG_FLG_WRITE

        rsp = await self.exec_cmd(struct.pack("<HHLLLLLLLQQLLLL",
            0, # rsvd
            CNDM_CMD_OP_ACCESS_REG, # opcode
            flags, # flags
            0, # rsvd
            0, # rsvd
            0, # rsvd
            0, # rsvd
            0, # rsvd
            reg, # reg
            data, # write data
            0, # read data
            0, # rsvd
            0, # rsvd
            0, # rsvd
            0, # rsvd
        ))

        return struct.unpack_from("<Q", rsp, 10*4)[0]

    async def exec_cmd(self, cmd):
        return await self.exec_mbox_cmd(cmd)

    async def exec_mbox_cmd(self, cmd):
        cmd = bytes(cmd)
        cmd = cmd.ljust(64, b'\x00')

        if len(cmd) != 64:
            raise ValueError("Invalid command length")

        # write command to mailbox
        a = array.array("I")
        a.frombytes(cmd)
        for k, dw in enumerate(a):
            await self.hw_regs.write_dword(0x10000+k*4, dw)

        # execute it
        await self.hw_regs.write_dword(0x0200, 0x00000001)

        # wait for completion
        while await self.hw_regs.read_dword(0x0200) & 0x00000001:
            pass

        # read response from mailbox
        for k in range(16):
            a[k] = await self.hw_regs.read_dword(0x10040+k*4)
        return a.tobytes()

    async def interrupt_handler(self, irqn):
        self.log.info("Interrupt handler start (IRQ %d)", irqn)
        for p in self.ports:
            if p.eq:
                # using EQs
                for eq in p.eq:
                    if eq.irqn == irqn:
                        await eq.process_eq()
            else:
                # using IRQs directly from CQs
                for q in p.rxq:
                    if q.cq.irqn == irqn:
                        await q.cq.handler(q.cq)
                for q in p.txq:
                    if q.cq.irqn == irqn:
                        await q.cq.handler(q.cq)
        self.log.info("Interrupt handler end (IRQ %d)", irqn)

    def alloc_pkt(self):
        if self.free_packets:
            return self.free_packets.popleft()

        pkt = self.pool.alloc_region(4096)
        self.allocated_packets.append(pkt)
        return pkt

    def free_pkt(self, pkt):
        assert pkt is not None
        assert pkt in self.allocated_packets
        self.free_packets.append(pkt)
