//..............................................................................
//
// Modbus RTU/TCP defitions
//

namespace io {

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

pragma(Alignment, 1);

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

// Modbus protocol variations

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

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

// Modbus node roles

enum ModbusRole: uint8_t {
	Master,
	Slave,
}

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

// function codes

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 coil" ]
	WriteCoil                  = 5,

	[ displayName = "Write register" ]
	WriteRegister              = 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,
}

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

// particular function code PDUs

[ displayName = "PDU header" ]
struct ModbusPduHdr {
	[ displayName = "Function" ]
	io.ModbusFunc m_func;
}

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

struct ModbusExceptionPdu: ModbusPduHdr {
	[ displayName = "Exception" ]
	ModbusException m_exception;
}

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

struct ModbusReadPdu: ModbusPduHdr {
	[ displayName = "Read address" ]
	bigendian uint16_t m_address;

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

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

struct ModbusReadReplyPdu: ModbusPduHdr {
	[ displayName = "Size" ]
	uint8_t m_size;

	// followed by read data
}

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

struct ModbusWritePdu: ModbusPduHdr {
	[ displayName = "Write address" ]
	bigendian uint16_t m_address;

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

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

struct ModbusReadExceptionStatusReplyPdu: ModbusPduHdr {
	[ displayName = "Status" ]
	uint8_t m_status;
}

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

struct ModbusGetCommEventCounterReplyPdu: ModbusPduHdr {
	[ displayName = "Status" ]
	bigendian uint16_t m_status;

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

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

struct ModbusGetCommEventLogReplyPdu: ModbusReadReplyPdu {
	[ 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
}

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

struct ModbusWriteMultiplePdu: ModbusPduHdr {
	[ displayName = "Write address" ]
	bigendian uint16_t m_address;

	[ displayName = "Count" ]
	bigendian uint16_t m_count;

	[ displayName = "Size" ]
	uint8_t m_size;

	// followed by write data
}

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

struct ModbusWriteMultipleReplyPdu: ModbusPduHdr {
	[ displayName = "Write address" ]
	bigendian uint16_t m_address;

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

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

struct ModbusMaskWritePdu: ModbusPduHdr {
	[ displayName = "Write address" ]
	bigendian uint16_t m_address;

	[ displayName = "AND mask" ]
	bigendian uint16_t m_andMask;

	[ displayName = "OR mask" ]
	bigendian uint16_t m_orMask;
}

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

struct ModbusReadWriteMultiplePdu: ModbusPduHdr {
	[ displayName = "Read address" ]
	bigendian uint16_t m_readAddress;

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

	[ displayName = "Write address" ]
	bigendian uint16_t m_writeAddress;

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

	[ displayName = "Write size" ]
	uint8_t m_writeSize;
}

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

struct ModbusReadFifoPdu: ModbusPduHdr {
	[ displayName = "Address" ]
	bigendian uint16_t m_address;
}

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

struct ModbusReadFifoReplyPdu: ModbusPduHdr {
	[ displayName = "Byte count" ]
	bigendian uint16_t m_byteCount;

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

	// followed by FIFO data
}

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

enum {
	ModbusMaxPduSize = sizeof(ModbusWriteMultiplePdu) + 255,
}

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

// calculates the actual size of PDU (-1 on error)

size_t errorcode getModbusPduSize(
	ModbusRole role,
	ModbusPduHdr const* pduHdr,
	size_t size
) {
	if (size < sizeof(ModbusPduHdr))
		return sizeof(ModbusPduHdr);

	switch (pduHdr.m_func) {
	case io.ModbusFunc.ReadCoils:
	case io.ModbusFunc.ReadDiscreteInputs:
	case io.ModbusFunc.ReadHoldingRegisters:
	case io.ModbusFunc.ReadInputRegisters:
	case io.ModbusFunc.ReportServerId:
		return
			role == ModbusRole.Master ? sizeof(ModbusReadPdu) :
			size < sizeof(ModbusReadReplyPdu) ? sizeof(ModbusReadReplyPdu) :
			sizeof(ModbusReadReplyPdu) + ((ModbusReadReplyPdu const*) pduHdr).m_size;

	case io.ModbusFunc.WriteCoil:
	case io.ModbusFunc.WriteRegister:
		return sizeof(ModbusWritePdu);

	case io.ModbusFunc.ReadExceptionStatus:
		return role == ModbusRole.Master ?
			sizeof(ModbusPduHdr) :
			sizeof(ModbusReadExceptionStatusReplyPdu);

	case io.ModbusFunc.Diagnostics:
		std.setError("ModbusFunc.Diagnostics is unsupported");
		return -1;

	case io.ModbusFunc.GetCommEventCounter:
		return role == ModbusRole.Master ?
			sizeof(ModbusPduHdr) :
			sizeof(ModbusGetCommEventCounterReplyPdu);

	case io.ModbusFunc.GetCommEventLog:
		return role == ModbusRole.Master ?
			sizeof(ModbusPduHdr) :
			size < sizeof(ModbusGetCommEventLogReplyPdu) ? sizeof(ModbusGetCommEventLogReplyPdu) :
			sizeof(ModbusReadReplyPdu) + ((ModbusReadReplyPdu const*) pduHdr).m_size;

	case io.ModbusFunc.WriteMultipleCoils:
	case io.ModbusFunc.WriteMultipleRegisters:
		return
			role == ModbusRole.Slave ? sizeof(ModbusWriteMultipleReplyPdu) :
			size < sizeof(ModbusWriteMultiplePdu) ? sizeof(ModbusWriteMultiplePdu) :
			sizeof(ModbusWriteMultiplePdu) + ((ModbusWriteMultiplePdu const*) pduHdr).m_size;

	case io.ModbusFunc.ReadFileRecord:
	case io.ModbusFunc.WriteFileRecord:
		return
			size < sizeof(ModbusReadReplyPdu) ? sizeof(ModbusReadReplyPdu) :
			sizeof(ModbusReadReplyPdu) + ((ModbusReadReplyPdu const*) pduHdr).m_size;

	case io.ModbusFunc.MaskWriteRegister:
		return sizeof(ModbusMaskWritePdu);

	case io.ModbusFunc.ReadWriteMultipleRegisters:
		return
			role == ModbusRole.Master ?
			size < sizeof(ModbusReadWriteMultiplePdu) ? sizeof(ModbusReadWriteMultiplePdu) :
			sizeof(ModbusReadWriteMultiplePdu) + ((ModbusReadWriteMultiplePdu const*) pduHdr).m_writeSize :
			size < sizeof(ModbusReadReplyPdu) ? sizeof(ModbusReadReplyPdu) :
			sizeof(ModbusReadReplyPdu) + ((ModbusReadReplyPdu const*) pduHdr).m_size;

	case io.ModbusFunc.ReadFifoQueue:
		return
			role == ModbusRole.Master ? sizeof(ModbusReadFifoPdu) :
			size < sizeof(ModbusReadFifoReplyPdu) ? sizeof(ModbusReadFifoReplyPdu) :
			sizeof(ModbusReadFifoReplyPdu) + ((ModbusReadFifoReplyPdu const*) pduHdr).m_fifoCount * sizeof(uint16_t);

	case io.ModbusFunc.EncapsulatedInterface:
		std.setError("ModbusFunc.EncapsulatedInterface is unsupported");
		return -1;

	default:
		if (pduHdr.m_func & ModbusFunc.ExceptionFlag)
			return sizeof(ModbusExceptionPdu);

		std.setError("Unknown Modbus function");
		return -1;
	}
}

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

// Modbus ADUs

struct ModbusRtuAduHdr {
	[ displayName = "Device address" ]
	uint8_t m_deviceAddress;

	// followed by PDU and CRC16 checksum
}

struct ModbusAsciiAduHdr {
	[ displayName = "Device address" ]
	char m_deviceAddress[2];

	// followed by hex-encoded PDU, LRC8 checksum, and then \r\n
}

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 address" ]
	uint8_t m_deviceAddress;

	// followed by PDU
}

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

enum {
	ModbusMaxRtuAduSize   = sizeof(ModbusRtuAduHdr) + ModbusMaxPduSize + 2,
	ModbusMaxAsciiAduSize = 1 + sizeof(ModbusAsciiAduHdr) + ModbusMaxPduSize * 2 + 4,
	ModbusMaxTcpAduSize = sizeof(ModbusTcpAduHdr) + ModbusMaxPduSize,
}

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

// calculates the actual size of Modbus RTU ADU

size_t errorcode getModbusRtuAduSize(
	ModbusRole role,
	ModbusRtuAduHdr const* aduHdr,
	size_t size
) {
	if (size < sizeof(ModbusRtuAduHdr))
		return sizeof(ModbusRtuAduHdr);

	ModbusPduHdr const* pduHdr = (ModbusPduHdr const*) (aduHdr + 1);
	size_t pduSize = getModbusPduSize(role, pduHdr, size - sizeof(ModbusRtuAduHdr));
	return pduSize + sizeof(uint8_t) + sizeof(uint16_t);
}

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

//! @}

} // namespace io
