//..............................................................................
//
// Modbus RTU/TCP protocol constants and structs
//

import "ui_Color.jnc"
import "ias_FieldCodec.jnc"
import "std_String.jnc"
import "crc8.jnc"
import "crc16.jnc"
import "hexEncoding.jnc"

namespace io {

//! \addtogroup proto-modbus
//! @{

pragma(Alignment, 1);

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

enum ModbusProtocol: uint8_t {
	Rtu,
	Ascii,
	Tcp,
}

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

enum ModbusRole: uint8_t {
	Master,
	Slave,
}

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

enum ModbusChecksumKind {
	Crc, // used in Modbus RTU
	Lrc, // used in Modbus ASCII
}

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

enum ModbusFunc: uint8_t {
	[ displayName = "Read coils" ]
	ReadCoils                  = 1,

	[ displayName = "Read discrete inputs" ]
	ReadDiscreteInputs         = 2,

	[ displayName = "Read holding registers" ]
	ReadHoldingRegisters       = 3,

	[ displayName = "Read input registers" ]
	ReadInputRegisters         = 4,

	[ displayName = "Write single coil" ]
	WriteSingleCoil            = 5,

	[ displayName = "Write single register" ]
	WriteSingleRegister        = 6,

	[ displayName = "Read exception status" ]
	ReadExceptionStatus	       = 7,

	[ displayName = "Diagnostics" ]
	Diagnostics                = 8,

	[ displayName = "Get comm event counter" ]
	GetCommEventCounter        = 11,

	[ displayName = "Get comm event log" ]
	GetCommEventLog            = 12,

	[ displayName = "Write multiple coils" ]
	WriteMultipleCoils         = 15,

	[ displayName = "Write multiple registers" ]
	WriteMultipleRegisters     = 16,

	[ displayName = "Report server ID" ]
	ReportServerId             = 17,

	[ displayName = "Read file record" ]
	ReadFileRecord             = 20,

	[ displayName = "Write file record" ]
	WriteFileRecord            = 21,

	[ displayName = "Mask write register" ]
	MaskWriteRegister	       = 22,

	[ displayName = "Read/write multiple registers" ]
	ReadWriteMultipleRegisters = 23,

	[ displayName = "Read FIFO queue" ]
	ReadFifoQueue              = 24,

	[ displayName = "Encapsulated interface" ]
	EncapsulatedInterface      = 43,

	[ displayName = "Exception" ]
	ExceptionFlag              = 0x80,
}

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

// exception codes

enum ModbusException: uint8_t {
	[ displayName = "Illegal function" ]
	IllegalFunction         = 1,

	[ displayName = "Illegal data address" ]
	IllegalDataAddress      = 2,

	[ displayName = "Illegal data value" ]
	IllegalDataValue        = 3,

	[ displayName = "Server device failure" ]
	ServerDeviceFailure     = 4,

	[ displayName = "Acknowledge" ]
	Acknowledge             = 5,

	[ displayName = "Server device busy" ]
	ServerDeviceBusy        = 6,

	[ displayName = "Memory parity error" ]
	MemoryParityError       = 8,

	[ displayName = "Gateway path unavailable" ]
	GatewayPathUnavailable  = 10,

	[ displayName = "Gateway device no response" ]
	GatewayDeviceNoResponse = 11,
}

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

// diagnostic sub-functions

enum ModbusDiagnosticSubFunc: uint16_t {
	[ displayName = "Return query data" ]
	ReturnQueryData                     = 0,

	[ displayName = "Restart communications option" ]
	RestartCommunicationsOption         = 1,

	[ displayName = "Return diagnostic register" ]
	ReturnDiagnosticRegister            = 2,

	[ displayName = "Change ascii input delimiter" ]
	ChangeAsciiInputDelimiter           = 3,

	[ displayName = "Force listen only mode" ]
	ForceListenOnlyMode                 = 4,

	[ displayName = "Clear counters and diagnostic register" ]
	ClearCountersAndDiagnosticRegister  = 10,

	[ displayName = "Return bus message count" ]
	ReturnBusMessageCount               = 11,

	[ displayName = "Return bus communication error count" ]
	ReturnBusCommunicationErrorCount    = 12,

	[ displayName = "Return bus exception error count" ]
	ReturnBusExceptionErrorCount        = 13,

	[ displayName = "Return server message count" ]
	ReturnServerMessageCount            = 14,

	[ displayName = "Return server no response count" ]
	ReturnServerNoResponseCount         = 15,

	[ displayName = "Return server nak count" ]
	ReturnServerNakCount                = 16,

	[ displayName = "Return server busy count" ]
	ReturnServerBusyCount               = 17,

	[ displayName = "Return bus character overrun count" ]
	ReturnBusCharacterOverrunCount      = 18,

	[ displayName = "Clear overrun counter and flag" ]
	ClearOverrunCounterAndFlag          = 20,
}

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

// particular function code PDUs

struct ModbusPduHdr {
	[ displayName = "Function" ]
	ModbusFunc m_func;
}

struct ModbusReplyPduHdr {
	[
		displayName = "Function",
		displayType = typeof(ModbusFunc)
	]
	uint8_t m_func: 7;

	[
		displayName = "Exception flag",
		displayType = typeof(bool)
	]
	uint8_t m_exceptionFlag: 1;
}

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

// ModbusFunc.Exception (ModbusPduHdr.m_func has ModbusFunc.ExceptionFlag set)

struct ModbusExceptionParams {
	[ displayName = "Exception code" ]
	ModbusException m_exception;
}

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

// ModbusFunc.ReadCoils / ReadDiscreteInputs / ReadHoldingRegisters / ReadInputRegisters

struct ModbusReadParams {
	[ displayName = "Starting address" ]
	bigendian uint16_t m_address;

	[ displayName = "Quantity" ]
	bigendian uint16_t m_count;
}

struct ModbusReadReplyParams {
	[ displayName = "Byte count" ]
	uint8_t m_size;

	// followed by read data
}

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

// ModbusFunc.WriteSingleCoil/WriteSingleRegister

struct ModbusWriteSingleParams {
	[ displayName = "Address" ]
	bigendian uint16_t m_address;

	[ displayName = "Value" ]
	bigendian uint16_t m_value;
}

typedef ModbusWriteSingleParams ModbusWriteSingleReplyParams;

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

// ModbusFunc.ReadExceptionStatus

struct ModbusReadExceptionStatusReplyParams {
	[ displayName = "Output data" ]
	uint8_t m_status;
}

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

// ModbusFunc.Diagnostics

struct ModbusDiagnosticsParams {
	bigendian ModbusDiagnosticSubFunc m_subFunc;

	[ displayName = "Data" ]
	bigendian uint16_t m_data;
}

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

// ModbusFunc.GetCommEventCounter

struct ModbusGetCommEventCounterReplyParams {
	[ displayName = "Status" ]
	bigendian uint16_t m_status;

	[ displayName = "Event count" ]
	bigendian uint16_t m_eventCount;
}

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

// ModbusFunc.GetCommEventLog

struct ModbusGetCommEventLogReplyParams: ModbusReadReplyParams {
	[ displayName = "Status" ]
	bigendian uint16_t m_status;

	[ displayName = "Event count" ]
	bigendian uint16_t m_eventCount;

	[ displayName = "Message count" ]
	bigendian uint16_t m_messageCount;

	// followed by event log data
}

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

// ModbusFunc.WriteMultipleCoils / WriteMultipleRegisters

struct ModbusWriteMultipleParams {
	[ displayName = "Starting address" ]
	bigendian uint16_t m_address;

	[ displayName = "Quantity" ]
	bigendian uint16_t m_count;

	[ displayName = "Byte count" ]
	uint8_t m_size;

	// followed by write data
}

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

[
	userAction = "Update byte count",
	autorun = "Auto-update byte count"
]
void AutoUpdateSizeAttr;

struct ModbusWriteMultipleCoilsParams: ModbusWriteMultipleParams {
	[ @AutoUpdateSizeAttr ]
	void updateSize() {
		m_size = m_count / 8;
		if (m_count & 7)
			m_size++;
	}
}

struct ModbusWriteMultipleRegistersParams: ModbusWriteMultipleParams {
	[ @AutoUpdateSizeAttr ]
	void updateSize() {
		m_size = m_count * sizeof(uint16_t);
	}
}

struct ModbusWriteMultipleReplyParams {
	[ displayName = "Starting address" ]
	bigendian uint16_t m_address;

	[ displayName = "Quantity" ]
	bigendian uint16_t m_count;
}

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

// ModbusFunc.ReadFileRecord/WriteFileRecord

typedef ModbusReadReplyParams ModbusReadWriteFileRecordParams;
typedef ModbusReadReplyPdu ModbusReadWriteFileRecordPdu;

struct ModbusFileRecord {
	uint8_t m_referenceType; // must be 0x06
	bigendian uint16_t m_fileNumber;
	bigendian uint16_t m_recordNumber;
	bigendian uint16_t m_recordCount;
}

struct ModbusReadFileRecordReply {
	uint8_t m_size;
	uint8_t m_referenceType; // must be 0x06
}

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

// ModbusFunc.MaskWriteRegister

struct ModbusMaskWriteParams {
	[ displayName = "Address" ]
	bigendian uint16_t m_address;

	[
		displayName = "AND mask",
		formatSpec = "0x%04X"
	]
	bigendian uint16_t m_andMask;

	[
		displayName = "OR mask",
		formatSpec = "0x%04X"
	]
	bigendian uint16_t m_orMask;
}

typedef ModbusMaskWriteParams ModbusMaskWriteReplyParams;

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

// ModbusFunc.ReadWriteMultipleRegisters

struct ModbusReadWriteMultipleRegistersParams {
	[
		displayName = "Read address",
		formatSpec = "0x%04X"
	]
	bigendian uint16_t m_readAddress;

	[ displayName = "Read quantity" ]
	bigendian uint16_t m_readCount;

	[
		displayName = "Write address",
		formatSpec = "0x%04X"
	]
	bigendian uint16_t m_writeAddress;

	[ displayName = "Write quantity" ]
	bigendian uint16_t m_writeCount;

	[ displayName = "Write byte count" ]
	uint8_t m_writeSize;

	// followed by data to be written
}

typedef ModbusReadReplyParams ModbusReadWriteMultipleRegistersReplyParams;

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

// ModbusFunc.ReportServerId

typedef ModbusReadReplyParams ModbusReportServerIdReplyParams;

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

// ModbusFunc.ReadFifoQueue

struct ModbusReadFifoQueueParams {
	[ displayName = "Address" ]
	bigendian uint16_t m_address;
}

struct ModbusReadFifoQueueReplyParams {
	[ displayName = "Byte count" ]
	bigendian uint16_t m_byteCount;

	[ displayName = "FIFO count" ]
	bigendian uint16_t m_fifoCount;

	// followed by FIFO data
}

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

// Modbus ADUs

bitflag enum ModbusAduFlags {
	AsciiEncoded, // by default, functions expect decoded MODBUS-ASCII (binary)
}

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

struct ModbusRtuAduHdr {
	[
		displayName = "Device",
		formatSpec = "#%d"
	]
	uint8_t m_deviceAddress;

	// followed by PDU, then CRC16 checksum
}

struct ModbusRtuAduPduHdr:
	ModbusRtuAduHdr,
	ModbusPduHdr {}

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

struct ModbusTcpAduHdr {
	[ displayName = "Transaction ID" ]
	bigendian uint16_t m_transactionId;

	[ displayName = "Protocol" ]
	bigendian uint16_t m_protocol;

	[ displayName = "Length" ]
	bigendian uint16_t m_length; // length of data following this field

	[
		displayName = "Device",
		formatSpec = "#%d"
	]
	uint8_t m_deviceAddress;

	[
		userAction = "Update length",
		autorun = "Auto-update length"
	]
	void updateLength() {
		m_length = dynamic sizeof(&m_length + 1);
	}

	// followed by PDU
}

struct ModbusTcpAduPduHdr:
	ModbusTcpAduHdr,
	ModbusPduHdr {}

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

enum {
	ModbusFileReferenceType = 6,

	ModbusMaxPduSize        = 253, // historical limit
	ModbusMaxRtuAduSize     = sizeof(ModbusRtuAduHdr) + ModbusMaxPduSize + 2,
	ModbusMaxAsciiAduSize   = 1 + (sizeof(ModbusRtuAduHdr) + ModbusMaxPduSize) * 2 + 4, // :(ADU)(LRC)\r\n
	ModbusMaxTcpAduSize     = sizeof(ModbusTcpAduHdr) + ModbusMaxPduSize,
}

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

string_t formatModbusRegisterAddress(
	size_t address,
	size_t i
) {
	return $"%1"(address + i);
}

string_t formatModbusRegisterValue(void const* p) {
	return $"0x%(1;04X) (%(1;d))"(*(bigendian uint16_t const*)p);
}

// no need to make these two async -- it doesn't read any preceding data

void layoutModbusRegisters(
	jnc.DynamicLayout* layout,
	size_t address,
	size_t count,
	size_t size
) {
	size_t maxCount = size / sizeof(uint16_t);
	if (count > maxCount)
		count = maxCount;

	dylayout (layout) {
		[
			displayName = "Registers",
			backColor = ui.StdColor.PastelYellow,
			digestFunc = getModbusRegisterDigest,
			foldFlag = 2
		]
		dyfield values {
			[
				ungroup,
				indexFormatFunc = address != -1 ? formatModbusRegisterAddress~(address) : null,
				valueFormatFunc = formatModbusRegisterValue,
			]
			dyfield bigendian uint16_t values[count];

			if (size & 1)
				[ displayName = "Padding" ]
				dyfield uint8_t padding;
		}
	}
}

void layoutModbusBits(
	jnc.DynamicLayout* layout,
	string_t displayName,
	size_t address,
	size_t count,
	size_t size
) {
	size_t maxCount = size * 8;
	if (count > maxCount)
		count = maxCount;

	dylayout (layout) {
		[
			displayName = displayName,
			backColor = ui.StdColor.PastelYellow,
			digestFunc = getModbusBitDigest,
			foldFlag = 2
		]
		dyfield values {
			for (size_t i = 0; i < count; i++)
				[ displayName = formatModbusRegisterAddress(address, i) ]
				dyfield uint8_t value : 1;
		}
	}
}

async void layoutModbusPdu(
	jnc.DynamicLayout* layout,
	ModbusRole role
) {
	dylayout (layout) {
		[
			backColor = ui.StdColor.PastelBlue,
			ungroup
		]
		dyfield pdu {
			ModbusFunc func;

			if (role == ModbusRole.Master) { // request
				[ ungroup ]
				dyfield ModbusPduHdr pduHdr;
				func = pduHdr.m_func;
			} else {
				[ ungroup ]
				dyfield ModbusReplyPduHdr pduHdr;
				if (pduHdr.m_exceptionFlag) {
					[ ungroup ]
					dyfield ModbusExceptionParams params;
					return;
				}

				func = (ModbusFunc)pduHdr.m_func;
			}

			switch (func) {
			case ModbusFunc.ReadCoils:
			case ModbusFunc.ReadDiscreteInputs:
			case ModbusFunc.ReadHoldingRegisters:
			case ModbusFunc.ReadInputRegisters:
				if (role == ModbusRole.Master) // request
					[ ungroup ]
					dyfield ModbusReadParams params;
				else { // reply
					static string_t bitDisplayNames[] = {
						null,
						"Coils",
						"Inputs"
					}

					[ ungroup ]
					dyfield ModbusReadReplyParams params;

					if (func <= ModbusFunc.ReadDiscreteInputs)
						layoutModbusBits(layout, bitDisplayNames[func], 0, -1, params.m_size);
					else
						layoutModbusRegisters(layout, -1, -1, params.m_size);
				}
				break;


			case ModbusFunc.WriteSingleCoil:
			case ModbusFunc.WriteSingleRegister:
				[ ungroup ]
				dyfield ModbusWriteSingleParams params; // same for request & reply
				break;

			case ModbusFunc.ReadExceptionStatus:
				if (role == ModbusRole.Slave)
					[ ungroup]
					dyfield ModbusReadExceptionStatusReplyParams params;
				break;

			case ModbusFunc.Diagnostics:
				[ ungroup]
				dyfield ModbusDiagnosticsParams params;  // same for request & reply
				break;

			case ModbusFunc.GetCommEventCounter:
				if (role == ModbusRole.Slave)
					[ ungroup]
					dyfield ModbusGetCommEventCounterReplyParams params;

				break;

			case ModbusFunc.GetCommEventLog:
				if (role == ModbusRole.Slave) {
					[ ungroup]
					dyfield ModbusGetCommEventLogReplyParams params;

					if (params.m_size > 6)
						[
							displayName = "Values",
							backColor = ui.StdColor.PastelYellow,
							foldFlag = 2
						]
						dyfield uint8_t data[params.m_size - 3 * sizeof(uint16_t)];
				}
				break;

			case ModbusFunc.ReportServerId:
				if (role == ModbusRole.Slave) {
					[ ungroup ]
					dyfield ModbusReportServerIdReplyParams params;

					[
						displayName = "Server ID",
						backColor = ui.StdColor.PastelYellow,
						binary
					]
					dyfield char data[params.m_size];
				}
				break; // no extra params

			case ModbusFunc.WriteMultipleCoils:
				if (role == ModbusRole.Master) { // request
					[ ungroup ]
					dyfield ModbusWriteMultipleCoilsParams params;
					layoutModbusBits(layout, "Coils", params.m_address, params.m_count, params.m_size);
				} else // reply
					[ ungroup ]
					dyfield ModbusWriteMultipleReplyParams reply;
				break;

			case ModbusFunc.WriteMultipleRegisters:
				if (role == ModbusRole.Master) { // request
					[ ungroup ]
					dyfield ModbusWriteMultipleRegistersParams params;
					layoutModbusRegisters(layout, params.m_address, params.m_count, params.m_size);
				} else // reply
					[ ungroup ]
					dyfield ModbusWriteMultipleReplyParams params;
				break;

			case ModbusFunc.ReadFileRecord:
			case ModbusFunc.WriteFileRecord:
				[ ungroup ]
				dyfield ModbusReadWriteFileRecordParams params;

				[
					displayName = "Records",
					backColor = ui.StdColor.PastelYellow,
					binary
				]
				dyfield char data[params.m_size];
				break;

			case ModbusFunc.MaskWriteRegister:
				[ ungroup ]
				dyfield ModbusMaskWriteParams params; // same for request & reply
				break;

			case ModbusFunc.ReadWriteMultipleRegisters:
				if (role == ModbusRole.Master) { // request
					[ ungroup ]
					dyfield ModbusReadWriteMultipleRegistersParams params;
					layoutModbusRegisters(layout, params.m_writeAddress, params.m_writeCount, params.m_writeSize);
				} else { // reply
					[ ungroup ]
					dyfield ModbusReadReplyParams params;
					layoutModbusRegisters(layout, -1, -1, params.m_size);
				}
				break;

			case ModbusFunc.ReadFifoQueue:
				if (role == ModbusRole.Master) // request
					[ ungroup ]
					dyfield ModbusReadFifoQueueParams params;
				else {
					[ ungroup ]
					dyfield ModbusReadFifoQueueReplyParams params;
					layoutModbusRegisters(layout, -1, params.m_fifoCount, params.m_byteCount - sizeof(uint16_t));
				}
				break;

			case ModbusFunc.EncapsulatedInterface:
				std.setError("ModbusFunc.EncapsulatedInterface is unsupported");
				throw;

			default:
				std.setError("unknown Modbus function");
				throw;
			}
		}
	}
}

void appendModbusPduDigest(
	std.StringBuilder* digest,
	void const* p,
	size_t size,
	ModbusRole role
) {
	jnc.DynamicLayout layout(p, size);
	dylayout (layout) {
		static string_t directions[] = {
			"request",
			"reply",
		}

		dyfield ModbusPduHdr pduHdr;
		if (!(pduHdr.m_func & ModbusFunc.ExceptionFlag))
			digest.append($"%1 (%2)"(
				typeof(ModbusFunc).getValueName(pduHdr.m_func),
				directions[role]
			));
		else {
			dyfield ModbusExceptionParams params;
			digest.append($"%1 (EXCEPTION: \e[31m%2\e[m)"(
				typeof(ModbusFunc).getValueName(pduHdr.m_func & ~ModbusFunc.ExceptionFlag),
				typeof(ModbusException).getValueString(params.m_exception)
			));
		}

		switch (pduHdr.m_func) {
		case ModbusFunc.ReadCoils:
		case ModbusFunc.ReadDiscreteInputs:
		case ModbusFunc.ReadHoldingRegisters:
		case ModbusFunc.ReadInputRegisters:
			if (role == ModbusRole.Master) {
				dyfield ModbusReadParams params;
				digest.append($" Addr: %1"(params.m_address));
				if (params.m_count > 1)
					digest.append($" N: %1"(params.m_count));
			} else {
				dyfield ModbusReadReplyParams params;
				digest.append(' ');

				if (pduHdr.m_func <= ModbusFunc.ReadDiscreteInputs)
					digest.append(getModbusBitDigest(&params + 1, params.m_size));
				else
					digest.append(getModbusRegisterDigest(&params + 1, params.m_size));
			}
			break;

		case ModbusFunc.WriteSingleCoil:
		case ModbusFunc.WriteSingleRegister:
			dyfield ModbusWriteSingleParams params;
			digest.append($" Addr: %1 Val: %2"(params.m_address, params.m_value));
			break;

		case ModbusFunc.WriteMultipleCoils:
		case ModbusFunc.WriteMultipleRegisters:
			if (role == ModbusRole.Master) {
				dyfield ModbusWriteMultipleParams params;
				digest.append($" Addr: %1 "(params.m_address));

				if (pduHdr.m_func == ModbusFunc.WriteMultipleCoils)
					digest.append(getModbusBitDigest(&params + 1, params.m_size));
				else
					digest.append(getModbusRegisterDigest(&params + 1, params.m_size));
			} else {
				dyfield ModbusWriteMultipleReplyParams params;
				digest.append($" Addr: %1 N: %2"(params.m_address, params.m_count));
			}
			break;

		case ModbusFunc.MaskWriteRegister:
			dyfield ModbusMaskWriteParams params;
			digest.append(
				$" Addr: %1 Masks: 0x%04X/0x%04X"(
				params.m_address,
				params.m_andMask,
				params.m_orMask
			));
			break;

		case ModbusFunc.ReadWriteMultipleRegisters:
			if (role == ModbusRole.Master) {
				dyfield ModbusReadWriteMultipleRegistersParams params;
				digest.append(
					$" Addr: %1/%2 "(
					params.m_readAddress,
					params.m_writeAddress
				));
				digest.append(getModbusRegisterDigest(&params + 1, params.m_writeSize));
			} else {
				dyfield ModbusReadReplyParams params;
				digest.append(' ');
				digest.append(getModbusRegisterDigest(&params + 1, params.m_size));
			}
			break;

		case ModbusFunc.ReadExceptionStatus:
		case ModbusFunc.Diagnostics:
		case ModbusFunc.GetCommEventCounter:
		case ModbusFunc.GetCommEventLog:
		case ModbusFunc.ReportServerId:
		case ModbusFunc.ReadFileRecord:
		case ModbusFunc.WriteFileRecord:
		case ModbusFunc.ReadFifoQueue:
		case ModbusFunc.EncapsulatedInterface:
			break; // not yet
		}
	}
}

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

uint16_t crc16_modbus(
	void const* p,
	size_t size
) {
	return crc16_ansi(p, size, 0xffff);
}

struct ModbusRtuCrc {
	[
		displayName = "CRC",
		formatFunc  = formatModbusRtuCrc,
		formatSpec  = "0x%04X"
	]
	uint16_t m_crc; // Modbus RTU CRC is little-endian

	[
		userAction = "Update CRC",
		autorun = "Auto-update CRC"
	]
	void updateCrc() {
		m_crc = calcCrc();
	}

	uint16_t calcCrc() const {
		size_t size = dynamic offsetof(this);
		return crc16_modbus((char const*)this - size, size);
	}
}

struct ModbusAsciiLrc {
	[
		displayName = "LRC",
		formatFunc  = formatModbusAsciiLrc,
		formatSpec  = "0x%02X"
	]
	uint8_t m_crc; // Modbus RTU CRC is little-endian

	[
		userAction = "Update LRC",
		autorun = "Auto-update LRC"
	]
	void updateLrc() {
		m_crc = calcLrc();
	}

	uint16_t calcLrc() const {
		size_t size = dynamic offsetof(this);
		return lrc((char const*)this - size, size);
	}
}

string_t getModbusChecksumValidityString(bool isValid) {
	return isValid ? "" : "";
}

string_t formatModbusRtuCrc(void const* p) {
	ModbusRtuCrc const* crc = (ModbusRtuCrc const*)p;
	uint16_t expected = crc.calcCrc();
	return crc.m_crc == expected ?
		$"0x%04X\e[m [\e[32mOK\e[m]"(crc.m_crc) :
		$"0x%04X\e[m [\e[31mERROR\e[m] \e[90m(expected 0x%04X)"(crc.m_crc, expected);
}

string_t formatModbusAsciiLrc(void const* p) {
	size_t size = dynamic offsetof(p);
	uint8_t checksum = *(uint8_t const*)p;
	uint8_t expected = lrc(p - size, size);
	return checksum == expected ?
		$"0x%02X\e[m [\e[32mOK\e[m]"(checksum) :
		$"0x%02X\e[m [\e[31mERROR\e[m] \e[90m(expected 0x%02X)"(checksum, expected);
}

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

struct ModbusAsciiCrLf {
	[
		displayName = "CR",
		formatSpec  = "0x%02X"
	]
	char m_cr;

	[
		displayName = "LF",
		formatSpec  = "0x%02X"
	]
	char m_lf;

	[
		userAction = "Init frame",
		autorun = "Auto-init frame"
	]
	void init() {
		char* start = (char*)this - dynamic offsetof(this);
		*start = ':';
		m_cr = '\r';
		m_lf = '\n';
	}
}

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

class HexFieldCodec: ias.FieldCodec {
	override size_t errorcode decode(
		std.Buffer* buffer,
		void const* p,
		size_t size
	) {
		return decodeHexString(buffer, string_t(p, size));
	}

	override size_t errorcode encode(
		std.Buffer* buffer,
		void const* p,
		size_t size
	) {
		return buffer.copy(encodeHexString(p, size));
	}
}

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

[
	displayName = "Modbus ASCII request ADU (decoded)",
	fixedSize
]
void layoutModbusAsciiDecodedRequestAdu(jnc.DynamicLayout* layout) {
	io.layoutModbusAdu(layout, io.ModbusProtocol.Ascii, ModbusRole.Master);
}

[
	displayName = "Modbus ASCII reply ADU (decoded)",
	fixedSize
]
void layoutModbusAsciiDecodedReplyAdu(jnc.DynamicLayout* layout) {
	io.layoutModbusAdu(layout, io.ModbusProtocol.Ascii, ModbusRole.Slave);
}

async void layoutModbusAdu(
	jnc.DynamicLayout* layout,
	ModbusProtocol protocol,
	ModbusRole role,
	ModbusAduFlags flags = 0
) {
	[
		backColor = ui.StdColor.PastelPurple,
		ungroup
	]
	void AduAttr;

	static string_t displayNames[] = {
		"MODBUS-RTU",
		"MODBUS-ASCII",
		"MODBUS-TCP",
	}

	dylayout (layout) {
		[
			displayName = displayNames[protocol],
			displayNameAttr = "\e[1m",
			backColor = ui.StdColor.PastelGray,
			digestFunc = getModbusAduDigest,
			protocol = protocol,
			role = role,
			foldFlag = 1,
			root // root groups are for log only (ungrouped in packet templates)
		]
		dyfield adu {
			dylayout (layout) {
				switch (protocol) {
				case ModbusProtocol.Rtu:
					[ @AduAttr ]
					dyfield ModbusRtuAduHdr aduHdr;

					await layoutModbusPdu(layout, role);

					[ @AduAttr ]
					dyfield ModbusRtuCrc crc;
					break;

				case ModbusProtocol.Ascii:
					if (flags & ModbusAduFlags.AsciiEncoded) {
						[
							backColor = ui.StdColor.PastelGreen,
							ungroup
						]
						void FrameAttr;

						[
							@FrameAttr,
							displayName = "Start",
							formatSpec = "0x%02X"
						]
						dyfield char start;

						[
							backColor = ui.StdColor.PastelYellow,
							displayName = "ADU",
							valueText = "(Hex-encoded)",
							codec = typeof(HexFieldCodec),
							packetTemplate = role == ModbusRole.Slave ?
								declof(layoutModbusAsciiDecodedReplyAdu) :
								declof(layoutModbusAsciiDecodedRequestAdu)
						]
						dyfield adu {
							size_t length = await layout.asyncScanTo('\r');
							dyfield char buffer[length];
						}

						[
							@FrameAttr,
							ungroup
						]
						dyfield ModbusAsciiCrLf crlf;
					} else  {
						[ @AduAttr ]
						dyfield ModbusRtuAduHdr aduHdr;

						await layoutModbusPdu(layout, role);

						[ @AduAttr ]
						dyfield ModbusAsciiLrc lrc;
					}
					break;

				case ModbusProtocol.Tcp:
					[ @AduAttr ]
					dyfield ModbusTcpAduHdr aduHdr;

					await layoutModbusPdu(layout, role);
					break;
				}
			}
		}
	}
}

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

string_t getModbusAduDigest(
	void const* p,
	size_t size,
	jnc.ModuleItemDecl* decl
) {
	static string_t arrows[] = {
		"\u2190", // left arrow
		"\u2192", // right arrow
	}

	ModbusProtocol protocol = decl.findAttributeValue("protocol");
	ModbusRole role = decl.findAttributeValue("role");
	std.StringBuilder digest;

	switch (protocol) {
	case ModbusProtocol.Rtu:
		ModbusRtuAduHdr const* aduHdr = (ModbusRtuAduHdr const*)p;
		digest := $"Dev: %1 %2 "(aduHdr.m_deviceAddress, arrows[role]);

		size -= sizeof(uint16_t); // minus CRC

		appendModbusPduDigest(
			digest,
			p + sizeof(ModbusRtuAduHdr),
			size - sizeof(ModbusRtuAduHdr),
			role
		);

		uint16_t const* crc = (uint16_t const*)(p + size);
		if (*crc != crc16_modbus(p, size))
			digest.append(" [\e[31mCRC ERROR\e[m]");

		break;

	case ModbusProtocol.Ascii:
		ModbusRtuAduHdr const* aduHdr = (ModbusRtuAduHdr const*)p;
		digest := $"Dev: %1 %2 "(aduHdr.m_deviceAddress, arrows[role]);

		size -= sizeof(uint8_t); // minus LRC

		appendModbusPduDigest(
			digest,
			p + sizeof(ModbusRtuAduHdr),
			size - sizeof(ModbusRtuAduHdr),
			role
		);

		uint8_t const* checksum = (uint8_t const*)(p + size);
		if (*checksum != lrc(p, size))
			digest.append(" [\e[31mLRC ERROR\e[m]");

		break;

	case ModbusProtocol.Tcp:
		ModbusTcpAduHdr const* aduHdr = (ModbusTcpAduHdr const*)p;
		digest := $"TID: %1 Dev: %2 %3 "(
			aduHdr.m_transactionId,
			aduHdr.m_deviceAddress,
			arrows[role]
		);

		appendModbusPduDigest(
			digest,
			p + sizeof(ModbusTcpAduHdr),
			size - sizeof(ModbusTcpAduHdr),
			role
		);

		break;
	}

	return digest.detachString();
}

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

enum {
	ModbusDigestValueCount = 3
}

string_t getModbusRegisterDigest(
	void const* p0,
	size_t size
) {
	size_t count0 = size / sizeof(uint16_t);
	size_t count = count0 < ModbusDigestValueCount ? count0 : ModbusDigestValueCount;
	if (!count)
		return "{ }";

	bigendian uint16_t const* p = (bigendian uint16_t const*)p0;
	bigendian uint16_t const* end = p + count;

	std.StringBuilder digest;
	digest.append('{');
	for (; p < end; p++)
		digest.append($" 0x%(1;04X) (%(1;d)),"(*p));

	if (count0 > count)
		digest.append(" ... }");
	else {
		digest.chop(1);
		digest.append(" }");
	}

	return digest.detachString();
}

string_t getModbusBitDigest(
	void const* p,
	size_t size
) {
	if (!size)
		return "{ }";

	size_t count0 = size * 8;

	std.StringBuilder digest;
	digest.append('{');

	uchar_t x = *(uchar_t const*)p;
	uchar_t mask = 1;
	for (size_t i = 0; i < ModbusDigestValueCount; i++, x >>= 1)
		digest.append($" %d,"(x & 1));

	digest.append(" ... }");
	return digest.detachString();
}

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

// PDU definitions (for the legacy plugin & packet templates)

struct ModbusExceptionPdu:
	ModbusPduHdr,
	ModbusExceptionParams {}

struct ModbusReadPdu:
	ModbusPduHdr,
	ModbusReadParams {}

struct ModbusReadReplyPdu:
	ModbusPduHdr,
	ModbusReadReplyParams {}

struct ModbusWriteSinglePdu:
	ModbusPduHdr,
	ModbusWriteSingleParams {}

typedef ModbusWriteSinglePdu ModbusWriteSingleReplyPdu;

struct ModbusReadExceptionStatusReplyPdu:
	ModbusPduHdr,
	ModbusReadExceptionStatusReplyParams {}

struct ModbusGetCommEventCounterReplyPdu:
	ModbusPduHdr,
	ModbusGetCommEventCounterReplyParams {}

struct ModbusGetCommEventLogReplyPdu:
	ModbusPduHdr,
	ModbusGetCommEventLogReplyParams {}

struct ModbusWriteMultiplePdu:
	ModbusPduHdr,
	ModbusWriteMultipleParams {}

struct ModbusWriteMultipleReplyPdu:
	ModbusPduHdr,
	ModbusWriteMultipleReplyParams {}

struct ModbusMaskWritePdu:
	ModbusPduHdr,
	ModbusMaskWriteParams {}

typedef ModbusMaskWritePdu ModbusMaskWriteReplyPdu;

struct ModbusReadWriteMultipleRegistersPdu:
	ModbusPduHdr,
	ModbusReadWriteMultipleRegistersParams {}

typedef ModbusReadReplyPdu ModbusReadWriteMultipleRegistersReplyPdu;

struct ModbusReadFifoQueuePdu:
	ModbusPduHdr,
	ModbusReadFifoQueueParams {}

struct ModbusReadFifoQueueReplyPdu:
	ModbusPduHdr,
	ModbusReadFifoQueueReplyParams {}

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

} // namespace io
