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

import "doc_Session.jnc"
import "io_usb.jncx"
import "io_UsbDevice.jnc"
import "io_UsbEnumerator.jnc"
import "io_base.jncx"
import "io_Serial.jnc"
import "ui_SerialUi.jnc"
import "ui_BufferPropertySet.jnc"
import "ui_StdSessionInfoSet.jnc"
import "ui_GroupBox.jnc"
import "log_ThroughputCalc.jnc"
import "log_ChecksumCalc.jnc"
import "std_Buffer.jnc"
import "SerialLog/SerialLogRecordCode.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,

	// interrupt bits

	CY_UART_CTS_BIT          = 0x01,
	CY_UART_DSR_BIT          = 0x02,
	CY_UART_BREAK_BIT        = 0x04,
	CY_UART_FRAME_ERROR_BIT  = 0x10,
	CY_UART_PARITY_ERROR_BIT = 0x20,
}

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_statusLines;

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

	void 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_statusLines = 0;

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

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

	return true;
}

void 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;
	string_t m_deviceName;
	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

	ui.SerialUi m_serialUi;

	// toolbar, status bar & menu

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

	// serial property grid

	ui.EnumProperty* m_deviceProp;
	ui.BoolProperty* m_flipProp;
	ui.BufferPropertySet m_bufferPropertySet;

	// information grid

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

	// ui reactor

	reactor m_uiReactor;

public:
	construct(doc.PluginHost* pluginHost);

	bool errorcode capture();
	void stopCapture();

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

	override bool errorcode connectSession() {
		return m_sessionConnectionState ? true : capture();
	}

	override void disconnectSession() {
		stopCapture();
	}

	override bool errorcode dispatch(
		uint_t code,
		variant_t in,
		variant_t* out
	);

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

	void onDeviceComboEnter() {
		try capture();
	}

	void onBaudRateChanged();
	void onDataBitsChanged();
	void onParityChanged();
	void onStopBitsChanged();

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

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

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

	void updateLineInfoValue(
		InfoId infoId,
		bool line
	);
}

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

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

	m_readBuffer.setSize(ui.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));
}

void SerialTapSession.createUi() {
	// toolbar

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

	m_pluginHost.m_toolBar.addLabel("Tap:");
	m_deviceCombo = m_pluginHost.m_toolBar.addComboBox(-1);
	m_deviceCombo.m_onReturnPressed += 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;

	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;

	// status bar (only one pane)

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

	// serial settings

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

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

	ui.FormLayout layout;

	ui.SerialUi.ItemId serialUiMask =
		ui.SerialUi.ItemId.BaudRate |
		ui.SerialUi.ItemId.DataBits |
		ui.SerialUi.ItemId.StopBits |
		ui.SerialUi.ItemId.Parity;

	m_serialUi.createForm(layout, serialUiMask);
	m_serialUi.createProperties(m_pluginHost.m_propertyGrid, groupProp, serialUiMask);

	ui.GroupBox groupBox("Serial tap settings", layout);
	m_pluginHost.createForm(groupBox);

	m_serialUi.m_baudRateCombo.m_onReturnPressed += onBaudRateChanged;
	bindingof(m_serialUi.m_baudRateCombo.m_currentIndex) += onBaudRateChanged;
	bindingof(m_serialUi.m_dataBitsCombo.m_currentIndex) += onDataBitsChanged;
	bindingof(m_serialUi.m_parityCombo.m_currentIndex) += onParityChanged;
	bindingof(m_serialUi.m_stopBitsCombo.m_currentIndex) += onStopBitsChanged;

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

	// property grid

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

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

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

	// information grid

	ui.InformationGroup* infoGroup = m_pluginHost.m_informationGrid.createGroup("Serial Tap");

	m_stdSessionInfoSet.create(m_pluginHost.m_txRxStats, 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_propertyGrid.resizeNameColumnToContents();
	m_pluginHost.m_informationGrid.resizeNameColumnToContents();

	enumerateDevices();

	m_uiReactor.start();
}

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

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

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

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

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

		string_t 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);
}

void 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_serialUi.m_baudRate;
	uartConfig.dataWidth = m_serialUi.m_dataBits;
	uartConfig.stopBits = m_serialUi.m_stopBits ? 2 : 1;
	uartConfig.parity = m_serialUi.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.UsbDeviceDescriptor const* deviceDesc = m_device.m_deviceDescriptor;

	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_serialUi.m_baudRate;
	m_dataBits = m_serialUi.m_dataBits;
	m_stopBits = m_serialUi.m_stopBits;
	m_parity = m_serialUi.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_statusLines & io.SerialStatusLines.Dsr;
	params.m_rts = m_dceIface.m_statusLines & io.SerialStatusLines.Cts;
	params.m_statusLines = m_dceIface.m_statusLines;
	params.m_options = 0;
	params.m_osKind = sys.g_systemInfo.m_osKind;

	m_deviceName = m_deviceCombo.m_currentText;

	m_logWriter.write(
		SerialLogRecordCode.PortOpened,
		&params,
		sizeof(params),
		m_deviceName.m_p,
		m_deviceName.m_length
	);

	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:
	m_logWriter.writeLastError(SerialLogRecordCode.PortOpenError);
	m_dteIface.release();
	m_dceIface.release();

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

	return false;
}

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

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

void 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
	);
}

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

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

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

	if ((triggeredEvents & io.UsbEndpointEvents.ReadBufferFull) &&
		m_bufferPropertySet.m_notifyReadBufferFullProp.m_value)
		m_logWriter.write(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;
			m_logWriter.write(recordCode, m_readBuffer.m_p, result);
		}
	}

	waitBulkEndpointEvents(iface);
}

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

	if (triggeredEvents & io.UsbEndpointEvents.IoError) {
		m_logWriter.write(log.StdRecordCode.Error, iface.m_interruptEndpoint.m_ioError);
		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)) {
			char bits = m_readBuffer[8];

			io.SerialStatusLines statusLines;
			io.SerialLineErrors lineErrors;

			if (bits & cy.CY_UART_DSR_BIT)
				statusLines |= io.SerialStatusLines.Dsr;

			if (bits & cy.CY_UART_CTS_BIT)
				statusLines |= io.SerialStatusLines.Cts;

			if (bits & cy.CY_UART_FRAME_ERROR_BIT)
				lineErrors |= io.SerialLineErrors.FramingError;

			if (bits & cy.CY_UART_PARITY_ERROR_BIT)
				lineErrors |= io.SerialLineErrors.ParityError;

			if (bits & cy.CY_UART_BREAK_BIT)
				lineErrors |= io.SerialLineErrors.BreakError;

			io.SerialStatusLines statusLineMask = statusLines ^ iface.m_statusLines;

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

				SerialStatusLineChangedParams params = { statusLines, statusLineMask }
				m_logWriter.write(recordCode, params, sizeof(params));

				iface.m_statusLines = statusLines;
			}

			if (lineErrors)
				m_logWriter.write(SerialLogRecordCode.LineError, &lineErrors, sizeof(lineErrors));
		}
	}

	waitInterruptEndpointEvents(iface);
}

void SerialTapSession.load(doc.Storage* storage) {
	m_serialUi.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_serialUi.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_serialUi.updateProperties();

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

bool errorcode SerialTapSession.applyProperties() {
	m_serialUi.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_serialUi.m_baudRate;
	uint_t dataBits = m_serialUi.m_dataBits;
	io.SerialStopBits stopBits = m_serialUi.m_stopBits;
	io.SerialParity parity = m_serialUi.m_parity;

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

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

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

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

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

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

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

bool errorcode SerialTapSession.dispatch(
	uint_t code,
	variant_t in,
	variant_t* out
) {
	switch (code) {
	case SerialTapDispatchCode.GetBaudRate:
		*out = m_serialUi.m_baudRate;
		break;

	case SerialTapDispatchCode.SetBaudRate:
		m_serialUi.m_baudRate = in;
		onBaudRateChanged();
		break;

	case SerialTapDispatchCode.GetDataBits:
		*out = m_serialUi.m_dataBits;
		break;

	case SerialTapDispatchCode.SetDataBits:
		uint_t dataBits = in;
		m_serialUi.m_dataBits = dataBits;
		onDataBitsChanged();
		break;

	case SerialTapDispatchCode.GetParity:
		*out = m_serialUi.m_parity;
		break;

	case SerialTapDispatchCode.SetParity:
		io.SerialParity parity = in;
		m_serialUi.m_parity = parity;
		onParityChanged();
		break;

	case SerialTapDispatchCode.GetStopBits:
		*out = m_serialUi.m_stopBits;
		break;

	case SerialTapDispatchCode.SetStopBits:
		io.SerialStopBits stopBits = in;
		m_serialUi.m_stopBits = stopBits;
		onStopBitsChanged();
		break;

	case SerialTapDispatchCode.GetStatusLines:
		*out = m_dteIface.m_statusLines;
		break;

	case SerialTapDispatchCode.GetDtr:
		*out = (m_dceIface.m_statusLines & io.SerialStatusLines.Dsr) != 0;
		break;

	case SerialTapDispatchCode.GetRts:
		*out = (m_dceIface.m_statusLines & io.SerialStatusLines.Cts) != 0;
		break;

	case SerialTapDispatchCode.Capture:
		return capture();

	case SerialTapDispatchCode.StopCapture:
		stopCapture();
		break;

	default:
		std.setError("unsupported dispatch code");
		return false;
	}

	return true;
}

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

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

	configureSerialIface(0);
	configureSerialIface(1);
	m_logWriter.write(SerialLogRecordCode.BaudRateChanged, &baudRate, sizeof(baudRate));
	m_baudRate = baudRate;

catch:
	m_logWriter.writeLastError(log.StdRecordCode.Error);
}

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

	uint_t dataBits = m_serialUi.m_dataBits;
	if (dataBits == m_dataBits)
		return;

	configureSerialIface(0);
	configureSerialIface(1);
	m_logWriter.write(SerialLogRecordCode.DataBitsChanged, &dataBits, sizeof(dataBits));
	m_dataBits = dataBits;

catch:
	m_logWriter.writeLastError(log.StdRecordCode.Error);
}

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

	io.SerialParity parity = m_serialUi.m_parity;
	if (parity == m_parity)
		return;

	configureSerialIface(0);
	configureSerialIface(1);
	m_logWriter.write(SerialLogRecordCode.ParityChanged, &parity, sizeof(parity));
	m_parity = parity;

catch:
	m_logWriter.writeLastError(log.StdRecordCode.Error);
}

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

	io.SerialStopBits stopBits = m_serialUi.m_stopBits;
	if (stopBits == m_stopBits)
		return;

	configureSerialIface(0);
	configureSerialIface(1);
	m_logWriter.write(SerialLogRecordCode.StopBitsChanged, &stopBits, sizeof(stopBits));
	m_stopBits = stopBits;

catch:
	m_logWriter.writeLastError(log.StdRecordCode.Error);
}

void SerialTapSession.updateLineInfoValue(
	InfoId infoId,
	bool line
) {
	m_infoTable[infoId].m_value = line ? "on" : "off";
	m_infoTable[infoId].m_valueColor = line ? ui.StdColor.BrightRed : ui.StdColor.BrightBlack;
}

reactor SerialTapSession.m_uiReactor {
	m_title = m_state ?
		$"Serial Tap $(m_deviceName)" :
		$"Serial Tap $(m_deviceCombo.m_currentText)";

	m_sessionConnectionState = m_state == State.Opened ?
		SessionConnectionState.Connected :
		SessionConnectionState.Disconnected;

	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_statusLines & io.SerialStatusLines.Dsr);
	updateLineInfoValue(InfoId.Rts, m_dceIface.m_statusLines & io.SerialStatusLines.Cts);
	updateLineInfoValue(InfoId.Dsr, m_dteIface.m_statusLines & io.SerialStatusLines.Dsr);
	updateLineInfoValue(InfoId.Cts, m_dteIface.m_statusLines & io.SerialStatusLines.Cts);
}

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