//..............................................................................
//
// Serial tap cable session
//

import "io_usb.jncx"
import "io_base.jncx"
import "io_Serial.jnc"
import "io_SerialUi.jnc"
import "io_BufferUi.jnc"
import "io_UsbDb.jnc"
import "std_Buffer.jnc"
import "doc_Session.jnc"
import "gui_StdSessionInfo.jnc"
import "log_ThroughputCalc.jnc"
import "log_ChecksumCalc.jnc"

namespace cy {

// excerpts from Cypress USBS236 SDK

//..............................................................................

enum
{
	// request types

	CY_VENDOR_REQUEST_DEVICE_TO_HOST = 0xC0,
	CY_VENDOR_REQUEST_HOST_TO_DEVICE = 0x40,

	// request ids

	CY_UART_GET_CONFIG_CMD = 0xC0,
	CY_UART_SET_CONFIG_CMD = 0xC1,
	CY_GPIO_GET_VALUE_CMD  = 0xDA,
	CY_GPIO_SET_VALUE_CMD  = 0xDB,

	// misc constants

	CY_SCB_INDEX_POS = 15,
}

struct CyUartConfig
{
	uint32_t baudRate;
	uint8_t pinType;
	uint8_t dataWidth;
	uint8_t stopBits;
	uint8_t mode;
	uint8_t parity;
	uint8_t isMsbFirst;
	uint8_t txRetry;
	uint8_t rxInvertPolarity;
	uint8_t rxIgnoreError;
	uint8_t isFlowControl;
	uint8_t isLoopBack;
	uint8_t flags;
}

//..............................................................................

} // namespace cy

enum UartGpioPin
{
	Dtr = 2,
	Rts = 4,
	Dsr = 14,
	Cts = 13,
}


bool getGpioPin (
	io.UsbDevice* device,
	uint_t pin
	)
{
	uint16_t value = -1;

	try device.controlTransfer (
		cy.CY_VENDOR_REQUEST_DEVICE_TO_HOST,
		cy.CY_GPIO_GET_VALUE_CMD,
		pin,
		0,
		&value,
		sizeof (uint16_t)
		);

	return !value;
}

//..............................................................................

class SerialIface
{
	io.UsbInterface* m_interface;
	io.UsbEndpoint* m_bulkEndpoint;
	io.UsbEndpoint* m_interruptEndpoint;
	uint_t m_bulkSyncId;
	uint_t m_interruptSyncId;
	io.SerialStatusLines bindable m_serialStatusLines;

	bool errorcode claim (
		io.UsbDevice* device,
		uint_t ifaceId
		);

	release ();
}

// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

bool errorcode SerialIface.claim (
	io.UsbDevice* device,
	uint_t ifaceId
	)
{
	m_interface = device.claimInterface (ifaceId);
	m_bulkEndpoint = m_interface.openEndpoint (ifaceId ? 0x85 : 0x82);
	m_interruptEndpoint = m_interface.openEndpoint (ifaceId ? 0x86 : 0x83);

	bool dsr = getGpioPin (device, ifaceId ? UartGpioPin.Dsr : UartGpioPin.Dtr);
	bool cts = getGpioPin (device, ifaceId ? UartGpioPin.Cts : UartGpioPin.Rts);

	m_serialStatusLines = 0;

	if (dsr)
		m_serialStatusLines = io.SerialStatusLines.Dsr;

	if (cts)
		m_serialStatusLines = io.SerialStatusLines.Cts;

	return true;
}

SerialIface.release ()
{
	if (m_bulkEndpoint)
	{
		m_bulkEndpoint.close ();
		m_bulkEndpoint = null;
		m_bulkSyncId++;
	}

	if (m_interruptEndpoint)
	{
		m_interruptEndpoint.close ();
		m_interruptEndpoint = null;
		m_interruptSyncId++;
	}

	if (m_interface)
	{
		m_interface.release ();
		m_interface = null;
	}
}

//..............................................................................

class SerialTapSession: doc.Session
{
protected:
	enum State
	{
		Closed,
		Opened,
	}

	enum ActionId
	{
		Refresh,
		Capture,
		Flip,
		_Count,
	}

	enum IconId
	{
		Capture,
		StopCapture,
		_Count,
	}

	enum InfoId
	{
		Dtr,
		Rts,
		Dsr,
		Cts,
		_Count,
	}

	enum UsbId
	{
		VendorId  = 0x16D0,
		ProductId = 0x0E26,
	}

protected:
	// two USB interfaces for serial ports (DTE & DCE)

	State bindable m_state;
	io.UsbDevice* m_device;
	SerialIface* m_dteIface; // pointers are easier to flip
	SerialIface* m_dceIface;
	uint_t m_baudRate;
	uint_t m_dataBits;
	io.SerialStopBits m_stopBits;
	io.SerialParity m_parity;
	std.Buffer m_readBuffer;

	// common serial ui

	io.SerialSettingUi m_serialSettingUi;

	// toolbar, status bar & menu

	gui.Action* m_actionTable [ActionId._Count];
	gui.Icon* m_iconTable [IconId._Count];
	gui.StatusPane* m_statusPane;
	gui.ComboBox* m_deviceCombo;

	// serial property grid

	gui.EnumProperty* m_deviceProp;
	gui.BoolProperty* m_flipProp;
	io.BufferPropertySet m_bufferPropertySet;

	// information grid

	gui.StdSessionInfoSet m_stdSessionInfoSet;
	log.ThroughputInfoSet m_throughputInfoSet;
	log.ChecksumInfoSet m_checksumInfoSet;
	gui.InformationValue* m_infoTable [InfoId._Count];

	// ui reactor

	reactor m_uiReactor;

public:
	construct (doc.PluginHost* pluginHost);

	bool errorcode capture ();
	stopCapture ();

	override void load (doc.Storage* storage);
	override void save (doc.Storage* storage);
	override void updateProperties ();
	override bool errorcode applyProperties ();
	override void restoreDefaultProperties ();

protected:
	onCapture ()
	{
		m_state ? stopCapture () : try capture ();
	}

	onDeviceComboEnter ()
	{
		try capture ();
	}

	onBaudRateChanged ();

	onBulkEndpointEvent (
		SerialIface* iface,
		uint_t syncId,
		io.UsbEndpointEvents triggeredEvents
		);

	onInterruptEndpointEvent (
		SerialIface* iface,
		uint_t syncId,
		io.UsbEndpointEvents triggeredEvents
		);

	createUi ();
	enumerateDevices ();
	flipDteDce ();
	waitBulkEndpointEvents (SerialIface* iface);
	waitInterruptEndpointEvents (SerialIface* iface);
	bool errorcode configureSerialIface (uint_t ifaceId);
	bool getGpio (uint_t pin);

	updateLineInfoValue (
		InfoId infoId,
		bool line
		);
}

// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

SerialTapSession.construct (doc.PluginHost* pluginHost)
{
	basetype.construct (pluginHost);

	m_readBuffer.setSize (io.BufferPropertySet.Defaults.ReadBufferSize);

	m_dteIface = new SerialIface;
	m_dceIface = new SerialIface;

	createUi ();

	pluginHost.m_log.addSelectionProcessor (log.ThroughputCalcRangeProcessor.factory ~(m_throughputInfoSet));
	pluginHost.m_log.addSelectionProcessor (log.ChecksumCalcRangeProcessor.factory ~(m_checksumInfoSet));
	m_stdSessionInfoSet.addLogListener (pluginHost.m_log);
}

SerialTapSession.createUi ()
{
	// toolbar

	m_iconTable [IconId.Capture] = new gui.Icon ("images/capture.png");
	m_iconTable [IconId.StopCapture] = new gui.Icon ("images/pause.png");

	m_pluginHost.m_toolBar.addLabel ("Tap:");
	m_deviceCombo = m_pluginHost.m_toolBar.addComboBox (-1);
	m_deviceCombo.m_onEnter += onDeviceComboEnter;

	m_actionTable [ActionId.Refresh] = createAction ("Re-enumerate devices", "images/refresh.png");
	m_actionTable [ActionId.Refresh].m_onTriggered += enumerateDevices;

	m_actionTable [ActionId.Capture] = createAction ("Capture", m_iconTable [IconId.Capture]);
	m_actionTable [ActionId.Capture].m_onTriggered += onCapture;

	// status bar (only one pane)

	m_statusPane = m_pluginHost.m_statusBar.addPane (, 80);

	// serial settings

	gui.GroupProperty* group = m_pluginHost.m_propertyGrid.createGroupProperty (
		"Connection",
		"USB-specific connection settings"
		);

	m_deviceProp = m_pluginHost.m_propertyGrid.createEnumProperty (
		group,,
		"Tap",
		"Target USB-connected tap device",
		null, 0
		);

	m_serialSettingUi.create (
		m_pluginHost.m_toolBar,
		m_pluginHost.m_propertyGrid,
		group,
		io.SerialSettingUi.SettingId.BaudRate,
		io.SerialSettingUi.SettingId.BaudRate |
		io.SerialSettingUi.SettingId.DataBits |
		io.SerialSettingUi.SettingId.StopBits |
		io.SerialSettingUi.SettingId.Parity
		);

	m_serialSettingUi.m_baudRateCombo.m_onEnter += onBaudRateChanged;
	bindingof (m_serialSettingUi.m_baudRateCombo.m_currentIndex) += onBaudRateChanged;

	m_pluginHost.m_menu.addSeparator ();

	m_actionTable [ActionId.Flip] = createAction ("Flip DTE/DCE", "images/flip.png");
	m_actionTable [ActionId.Flip].m_isCheckable = true;
	m_actionTable [ActionId.Flip].m_onTriggered = flipDteDce;

	m_flipProp = m_pluginHost.m_propertyGrid.createBoolProperty (
		group,,
		"Flip DTE/DCE",
		"Flip logical sides of the tap cable"
		);

	// property grid

	group = m_pluginHost.m_propertyGrid.createGroupProperty (
		"Buffering & compatibility",
		"Advanced options controlling performance, throughtput and compatibility"
		);

	io.BufferPropertySet.PropertyId mask = io.BufferPropertySet.PropertyId.All;
	mask &= ~(
		io.BufferPropertySet.PropertyId.WriteBufferSize |
		io.BufferPropertySet.PropertyId.KeepWriteBlockSize
		);

	m_bufferPropertySet.createProperties (m_pluginHost.m_propertyGrid, group,, mask);

	// information grid

	gui.InformationGroup* infoGroup = m_pluginHost.m_informationGrid.createGroup ("Serial tap");

	m_stdSessionInfoSet.createInformationValues (m_pluginHost.m_informationGrid, infoGroup);

	m_infoTable [InfoId.Dtr] = m_pluginHost.m_informationGrid.createValue (infoGroup,,, "DTR");
	m_infoTable [InfoId.Rts] = m_pluginHost.m_informationGrid.createValue (infoGroup,,, "RTS");
	m_infoTable [InfoId.Dsr] = m_pluginHost.m_informationGrid.createValue (infoGroup,,, "DSR");
	m_infoTable [InfoId.Cts] = m_pluginHost.m_informationGrid.createValue (infoGroup,,, "CTS");
	m_throughputInfoSet.createInformationValuesGrouped (m_pluginHost.m_informationGrid);
	m_checksumInfoSet.createInformationValuesGrouped (m_pluginHost.m_informationGrid);

	m_pluginHost.m_informationGrid.resizeNameColumnToContents ();

	enumerateDevices ();

	m_uiReactor.start ();
}

SerialTapSession.enumerateDevices ()
{
	m_deviceCombo.clear ();

	io.UsbDevice* const* deviceArray = io.createUsbDeviceArray ();
	size_t totalCount = dynamic countof (deviceArray);
	size_t actualCount = 0;

	gui.EnumPropertyOption* optionTable = new gui.EnumPropertyOption [totalCount];

	for (size_t i = 0; i < totalCount; i++)
	{
		io.UsbDevice* device = deviceArray [i];
		io.UsbDeviceDesc const* deviceDesc = device.m_deviceDesc;

		if (deviceDesc.m_vendorId != UsbId.VendorId ||
			deviceDesc.m_productId != UsbId.ProductId)
			continue;

		char const* text = $"@%(1;03d):%(2;03d) IO Ninja Serial Tap" (
			device.m_bus,
			device.m_address
			);

		m_deviceCombo.addItem (text, device);

		optionTable [actualCount].m_text = text;
		optionTable [actualCount].m_value = device;
		actualCount++;
	}

	m_deviceProp.setOptions (optionTable, actualCount);
}

SerialTapSession.flipDteDce ()
{
	if (!m_state)
		return;

	SerialIface* tmp = m_dteIface;
	m_dteIface = m_dceIface;
	m_dceIface = tmp;

	m_uiReactor.restart (); // need to re-bind DTE/DCE
}

bool errorcode SerialTapSession.configureSerialIface (uint_t ifaceId)
{
	cy.CyUartConfig uartConfig;
	uartConfig.baudRate = m_serialSettingUi.m_baudRate;
	uartConfig.dataWidth = m_serialSettingUi.m_dataBits;
	uartConfig.stopBits = m_serialSettingUi.m_stopBits ? 2 : 1;
	uartConfig.parity = m_serialSettingUi.m_parity;

	size_t result = m_device.controlTransfer (
		cy.CY_VENDOR_REQUEST_HOST_TO_DEVICE,
		cy.CY_UART_SET_CONFIG_CMD,
		ifaceId << cy.CY_SCB_INDEX_POS,
		0,
		&uartConfig,
		sizeof (uartConfig)
		);

	return true;
}

bool errorcode SerialTapSession.capture ()
{
	if (m_deviceCombo.m_currentIndex == -1)
		return false;

	stopCapture ();
	startSession ();

	m_device = m_deviceCombo.m_currentData;
	io.UsbDeviceDesc const* deviceDesc = m_device.m_deviceDesc;

	m_device.open ();

	configureSerialIface (0);
	configureSerialIface (1);

	// have to claim iface #0 before iface #1 (LIBUSB_ERROR_ACCESS otherwise)

	if (m_actionTable [ActionId.Flip].m_isChecked)
	{
		m_dceIface.claim (m_device, 0);
		m_dteIface.claim (m_device, 1);
	}
	else
	{
		m_dteIface.claim (m_device, 0);
		m_dceIface.claim (m_device, 1);
	}

	m_baudRate = m_serialSettingUi.m_baudRate;
	m_dataBits = m_serialSettingUi.m_dataBits;
	m_stopBits = m_serialSettingUi.m_stopBits;
	m_parity = m_serialSettingUi.m_parity;

	SerialOpenParams params;
	params.m_baudRate = m_baudRate;
	params.m_dataBits = m_dataBits;
	params.m_stopBits = m_stopBits;
	params.m_parity = m_parity;
	params.m_flowControl = io.SerialFlowControl.None;
	params.m_readInterval = 0;
	params.m_dtr = m_dceIface.m_serialStatusLines & io.SerialStatusLines.Dsr;
	params.m_rts = m_dceIface.m_serialStatusLines & io.SerialStatusLines.Cts;
	params.m_statusLines = m_dceIface.m_serialStatusLines;
	params.m_options = 0;
	params.m_osKind = sys.g_systemInfo.m_osKind;

	char const* p = $"$(params; B)$(m_deviceCombo.m_currentText)";
	writeLog (SerialLogRecordCode.PortOpened, p, dynamic sizeof (p));

	m_state = State.Opened;

	waitBulkEndpointEvents (m_dteIface);
	waitBulkEndpointEvents (m_dceIface);
	waitInterruptEndpointEvents (m_dteIface);
	waitInterruptEndpointEvents (m_dceIface);

	m_stdSessionInfoSet.startSession (m_pluginHost.m_mainThreadScheduler);
	return true;

catch:
	std.Error const* error = std.getLastError ();

	writeLog (
		SerialLogRecordCode.PortOpenError,
		error,
		error.m_size
		);

	m_dteIface.release ();
	m_dceIface.release ();

	if (m_device)
	{
		m_device.close ();
		m_device = null;
	}

	return false;
}

SerialTapSession.stopCapture ()
{
	if (m_state != State.Opened)
		return;

	writeLog (SerialLogRecordCode.PortClosed);
	m_stdSessionInfoSet.endSession ();
	m_dteIface.release ();
	m_dceIface.release ();
	m_device.close ();
	m_device = null;
	m_state = State.Closed;
}

SerialTapSession.waitBulkEndpointEvents (SerialIface* iface)
{
	iface.m_bulkEndpoint.wait (
		io.UsbEndpointEvents.IoError |
		io.UsbEndpointEvents.ReadBufferFull |
		io.UsbEndpointEvents.IncomingData,
		onBulkEndpointEvent ~(
			iface,
			++iface.m_bulkSyncId
			) @ m_pluginHost.m_mainThreadScheduler
		);
}

SerialTapSession.waitInterruptEndpointEvents (SerialIface* iface)
{
	iface.m_interruptEndpoint.wait (
		io.UsbEndpointEvents.IoError |
		io.UsbEndpointEvents.IncomingData,
		onInterruptEndpointEvent ~(
			iface,
			++iface.m_interruptSyncId
			) @ m_pluginHost.m_mainThreadScheduler
		);
}

SerialTapSession.onBulkEndpointEvent (
	SerialIface* iface,
	uint_t syncId,
	io.UsbEndpointEvents triggeredEvents
	)
{
	if (syncId != iface.m_bulkSyncId)
		return;

	if (triggeredEvents & io.UsbEndpointEvents.IoError)
	{
		writeLog (log.StdRecordCode.Error, iface.m_bulkEndpoint.m_ioError, iface.m_bulkEndpoint.m_ioError.m_size);
		stopCapture ();
		return;
	}

	if ((triggeredEvents & io.UsbEndpointEvents.ReadBufferFull) &&
		m_bufferPropertySet.m_notifyReadBufferFullProp.m_value)
		writeLog (log.StdRecordCode.RxBufferFull);

	if (triggeredEvents & io.UsbEndpointEvents.IncomingData)
	{
		size_t result = iface.m_bulkEndpoint.read (m_readBuffer.m_p, m_readBuffer.m_size);
		if (result)
		{
			uint64_t recordCode = iface == m_dteIface ? log.StdRecordCode.Rx : log.StdRecordCode.Tx;
			writeLog (recordCode, m_readBuffer.m_p, result);
		}
	}

	waitBulkEndpointEvents (iface);
}

SerialTapSession.onInterruptEndpointEvent (
	SerialIface* iface,
	uint_t syncId,
	io.UsbEndpointEvents triggeredEvents
	)
{
	if (syncId != iface.m_interruptSyncId)
		return;

	if (triggeredEvents & io.UsbEndpointEvents.IoError)
	{
		writeLog (log.StdRecordCode.Error, iface.m_interruptEndpoint.m_ioError, iface.m_interruptEndpoint.m_ioError.m_size);
		stopCapture ();
		return;
	}

	if (triggeredEvents & io.UsbEndpointEvents.IncomingData)
	{
		size_t result = iface.m_interruptEndpoint.read (m_readBuffer.m_p, m_readBuffer.m_size);
		if (result >= 10 && (m_readBuffer [6] & 0x02))
		{
			io.SerialStatusLines statusLines;
			if (m_readBuffer [8] & 0x02)
				statusLines |= io.SerialStatusLines.Dsr;

			if (m_readBuffer [8] & 0x01)
				statusLines |= io.SerialStatusLines.Cts;

			io.SerialStatusLines statusLineMask = statusLines ^ iface.m_serialStatusLines;

			if (statusLineMask)
			{
				uint64_t recordCode = iface == m_dteIface ?
					SerialLogRecordCode.StatusLineChanged :
					SerialLogRecordCode.ControlLineChanged;

				SerialStatusLineChangedParams params = { statusLines, statusLineMask }
				writeLog (recordCode, params, sizeof (params));

				iface.m_serialStatusLines = statusLines;
			}
		}
	}

	waitInterruptEndpointEvents (iface);
}

void SerialTapSession.load (doc.Storage* storage)
{
	m_serialSettingUi.load (storage);

	m_deviceCombo.m_currentIndex  = storage.readInt ("deviceIdx");
	m_actionTable [ActionId.Flip].m_isChecked = storage.readBool ("flipDteDce");

	m_bufferPropertySet.load (storage);
	m_readBuffer.setSize (m_bufferPropertySet.m_readBufferSizeProp.m_value);
}

void SerialTapSession.save (doc.Storage* storage)
{
	m_serialSettingUi.save (storage);

	storage.writeInt ("deviceIdx", (int) m_deviceCombo.m_currentIndex);
	storage.writeBool ("flipDteDce", m_actionTable [ActionId.Flip].m_isChecked);

	m_bufferPropertySet.save (storage);
}

void SerialTapSession.updateProperties ()
{
	m_serialSettingUi.updateProperties ();

	m_deviceProp.m_currentIndex = m_deviceCombo.m_currentIndex;
	m_flipProp.m_value = m_actionTable [ActionId.Flip].m_isChecked;
}

bool errorcode SerialTapSession.applyProperties ()
{
	m_serialSettingUi.applyProperties ();

	m_deviceCombo.m_currentIndex = m_deviceProp.m_currentIndex;
	m_actionTable [ActionId.Flip].m_isChecked = m_flipProp.m_value;
	m_readBuffer.setSize (m_bufferPropertySet.m_readBufferSizeProp.m_value);

	if (m_state != State.Opened)
		return true;

	uint_t baudRate = m_serialSettingUi.m_baudRate;
	uint_t dataBits = m_serialSettingUi.m_dataBits;
	io.SerialStopBits stopBits = m_serialSettingUi.m_stopBits;
	io.SerialParity parity = m_serialSettingUi.m_parity;

	if (m_baudRate != baudRate)
	{
		writeLog (SerialLogRecordCode.BaudRateChanged, &baudRate, sizeof (baudRate));
		m_baudRate = baudRate;
	}

	if (m_dataBits != dataBits)
	{
		writeLog (SerialLogRecordCode.DataBitsChanged, &dataBits, sizeof (dataBits));
		m_dataBits = dataBits;
	}

	if (m_stopBits != stopBits)
	{
		writeLog (SerialLogRecordCode.StopBitsChanged, &stopBits, sizeof (stopBits));
		m_stopBits = stopBits;
	}

	if (m_parity != parity)
	{
		writeLog (SerialLogRecordCode.ParityChanged, &parity, sizeof (parity));
		m_parity = parity;
	}

	configureSerialIface (0);
	configureSerialIface (1);
	return true;

catch:
	writeLogLastError (log.StdRecordCode.Error);
	return false;
}

void SerialTapSession.restoreDefaultProperties ()
{
	m_serialSettingUi.restoreDefaultProperties ();
	m_flipProp.m_value = false;
	m_bufferPropertySet.restoreDefaults ();
}

SerialTapSession.onBaudRateChanged ()
{
	if (m_state != State.Opened)
		return;

	uint_t baudRate = m_serialSettingUi.m_baudRate;
	if (baudRate == m_baudRate)
		return;

	configureSerialIface (0);
	configureSerialIface (1);

	writeLog (SerialLogRecordCode.BaudRateChanged, &baudRate, sizeof (baudRate));
	m_baudRate = baudRate;

catch:
	writeLogLastError (log.StdRecordCode.Error);
}

SerialTapSession.updateLineInfoValue (
	InfoId infoId,
	bool line
	)
{
	m_infoTable [infoId].m_value = line ? "on" : "off";
	m_infoTable [infoId].m_valueColor = line ? gui.StdColor.LightRed : gui.StdColor.Gray;
}

reactor SerialTapSession.m_uiReactor
{
	m_title = $"Tap $(m_deviceCombo.m_currentText)";
	m_actionTable [ActionId.Capture].m_text = m_state ? "Stop capture" : "Capture";
	m_actionTable [ActionId.Capture].m_icon = m_iconTable [m_state ? IconId.StopCapture : IconId.Capture];
	m_statusPane.m_text = m_state ? "Capturing" : "Closed";
	m_infoTable [InfoId.Dtr].m_isEnabled = m_state;
	m_infoTable [InfoId.Rts].m_isEnabled = m_state;
	m_infoTable [InfoId.Dsr].m_isEnabled = m_state;
	m_infoTable [InfoId.Cts].m_isEnabled = m_state;

	updateLineInfoValue (InfoId.Dtr, m_dceIface.m_serialStatusLines & io.SerialStatusLines.Dsr);
	updateLineInfoValue (InfoId.Rts, m_dceIface.m_serialStatusLines & io.SerialStatusLines.Cts);
	updateLineInfoValue (InfoId.Dsr, m_dteIface.m_serialStatusLines & io.SerialStatusLines.Dsr);
	updateLineInfoValue (InfoId.Cts, m_dteIface.m_serialStatusLines & io.SerialStatusLines.Cts);
}

//..............................................................................
