Skip to content

Commit

Permalink
fix setting endian before creating request will create invalid request
Browse files Browse the repository at this point in the history
add ability to set delay between sending the request and receiving request (helps with serial devices)
add support for serial devices in examples/index.php
  • Loading branch information
aldas committed Jun 14, 2023
1 parent 09bd467 commit 8ccb744
Show file tree
Hide file tree
Showing 23 changed files with 224 additions and 43 deletions.
132 changes: 108 additions & 24 deletions examples/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,38 @@
use ModbusTcpClient\Packet\ModbusFunction\ReadHoldingRegistersResponse;
use ModbusTcpClient\Packet\ModbusFunction\ReadInputRegistersRequest;
use ModbusTcpClient\Packet\ResponseFactory;
use ModbusTcpClient\Packet\RtuConverter;
use ModbusTcpClient\Utils\Endian;
use ModbusTcpClient\Utils\Packet;

$returnJson = filter_var($_GET['json'] ?? false, FILTER_VALIDATE_BOOLEAN);
// To allow Nginx/Apache to read that device add following udev rule
// echo 'KERNEL=="ttyUSB0", GROUP="www-data", MODE="0660"' | sudo tee /etc/udev/rules.d/60-ttyusb-acl.rules
// sudo udevadm control --reload-rules && sudo udevadm trigger
$deviceURI = '/dev/ttyUSB0'; // do not make this changeable from WEB. This could be serious security risk.
$isSerialDevice = false; // change to true to enable reading serial devices. this will disable ip/port logic and uses RTU
if (getenv('MODBUS_SERIAL_ENABLED')) { // can be set from Nginx/Apache fast-cgi conf
$isSerialDevice = filter_var(getenv('MODBUS_SERIAL_ENABLED'), FILTER_VALIDATE_BOOLEAN);
if ($isSerialDevice && getenv('MODBUS_SERIAL_DEVICE')) {
$deviceURI = getenv('MODBUS_SERIAL_DEVICE');
}
}
if ($isSerialDevice && stripos(PHP_OS, 'WIN') === 0) {
echo 'Serial usb example can not be run on Windows!' . PHP_EOL;
exit(0);
}

// if you want to let others specify their own ip/ports for querying data create file named '.allow-change' in this directory
// NB: this is a potential security risk!!!
$canChangeIpPort = file_exists('.allow-change');

$canChangeIpPort = !$isSerialDevice && file_exists('.allow-change');
$ip = '192.168.100.1';
$port = 502;
if ($canChangeIpPort) {
$ip = filter_var($_GET['ip'] ?? '', FILTER_VALIDATE_IP) ? $_GET['ip'] : $ip;
$port = (int)($_GET['port'] ?? $port);
}

$returnJson = filter_var($_GET['json'] ?? false, FILTER_VALIDATE_BOOLEAN);
$isRTU = $isSerialDevice || filter_var($_GET['rtu'] ?? false, FILTER_VALIDATE_BOOLEAN);
$fc = (int)($_GET['fc'] ?? 3);
$unitId = (int)($_GET['unitid'] ?? 0);
$startAddress = (int)($_GET['address'] ?? 256);
Expand All @@ -29,24 +46,46 @@
Endian::$defaultEndian = $endianess;

$log = [];
$log[] = "Using: function code: {$fc}, ip: {$ip}, port: {$port}, address: {$startAddress}, quantity: {$quantity}, endianess: {$endianess}";

$connection = BinaryStreamConnection::getBuilder()
->setPort($port)
->setHost($ip)
$builder = BinaryStreamConnection::getBuilder()
->setConnectTimeoutSec(1.5) // timeout when establishing connection to the server
->setWriteTimeoutSec(0.5) // timeout when writing/sending packet to the server
->setReadTimeoutSec(1.0) // timeout when waiting response from server
->build();
->setWriteTimeoutSec(1.0) // timeout when writing/sending packet to the server
->setReadTimeoutSec(1.0); // timeout when waiting response from server

$protocolType = "Modbus TCP";
if ($isRTU) {
$protocolType = "Modbus RTU";
$builder->setIsCompleteCallback(static function ($binaryData, $streamIndex): bool {
return Packet::isCompleteLengthRTU($binaryData);
});
}

if ($isSerialDevice) {
$log[] = "Using: {$protocolType} function code: {$fc}, device: {$deviceURI}, address: {$startAddress}, quantity: {$quantity}, endianess: {$endianess}";
$builder->setUri($deviceURI)
->setProtocol('serial')
// delay this is crucial for some serial devices and delay needs to be long as 100ms (depending on the quantity)
// or you will experience read errors ("stream_select interrupted") or invalid CRCs
->setDelayRead(100_000); // 100 milliseconds
} else {
$log[] = "Using: {$protocolType} function code: {$fc}, ip: {$ip}, port: {$port}, address: {$startAddress}, quantity: {$quantity}, endianess: {$endianess}";
$builder->setPort($port)->setHost($ip);
}

$connection = $builder->build();

if ($fc === 4) {
$packet = new ReadInputRegistersRequest($startAddress, $quantity, $unitId);
} else {
$fc = 3;
$packet = new ReadHoldingRegistersRequest($startAddress, $quantity, $unitId);
}
$log[] = 'Packet to be sent (in hex): ' . $packet->toHex();
if ($isRTU) {
$packet = RtuConverter::toRtu($packet);
$log[] = 'Modbus RTU Packet to be sent (in hex): ' . unpack('H*', $packet)[1];
} else {
$log[] = 'Modbus TCP Packet to be sent (in hex): ' . $packet->toHex();
}

$startTime = round(microtime(true) * 1000, 3);
$result = [];
Expand All @@ -56,7 +95,12 @@
$log[] = 'Binary received (in hex): ' . unpack('H*', $binaryData)[1];

/** @var $response ReadHoldingRegistersResponse */
$response = ResponseFactory::parseResponseOrThrow($binaryData)->withStartAddress($startAddress);
if ($isRTU) {
$response = RtuConverter::fromRtuOrThrow($binaryData);
} else {
$response = ResponseFactory::parseResponseOrThrow($binaryData);
}
$response = $response->withStartAddress($startAddress);

foreach ($response as $address => $word) {
$doubleWord = isset($response[$address + 1]) ? $response->getDoubleWordAt($address) : null;
Expand Down Expand Up @@ -127,22 +171,62 @@

?>

<h2>Example Modbus TCP FC3/FC4 request</h2>
<h2>Example Modbus TCP/RTU FC3/FC4 request</h2>
<form>
Modbus TCP or RTU: <select name="rtu"<?php if ($isSerialDevice) {
echo ' disabled';
} ?>>
<option value="0" <?php if (!$isRTU) {
echo 'selected';
} ?>>Modbus TCP
</option>
<option value="1" <?php if ($isRTU) {
echo 'selected';
} ?>>Modbus RTU
</option>
</select><br>
Function code: <select name="fc">
<option value="3" <?php if ($fc === 3) { echo 'selected'; } ?>>Read Holding Registers (FC=03)</option>
<option value="4" <?php if ($fc === 4) { echo 'selected'; } ?>>Read Input Registers (FC=04)</option>
<option value="3" <?php if ($fc === 3) {
echo 'selected';
} ?>>Read Holding Registers (FC=03)
</option>
<option value="4" <?php if ($fc === 4) {
echo 'selected';
} ?>>Read Input Registers (FC=04)
</option>
</select><br>
IP: <input type="text" name="ip" value="<?php echo $ip; ?>" <?php if (!$canChangeIpPort) { echo 'disabled'; } ?>><br>
Port: <input type="number" name="port" value="<?php echo $port; ?>"><br>
UnitID (SlaveID): <input type="number" name="unitid" value="<?php echo $unitId; ?>"><br>
Address: <input type="number" name="address" value="<?php echo $startAddress; ?>"> (NB: does your modbus server use `0` based addressing or `1` based?)<br>
Quantity: <input type="number" name="quantity" value="<?php echo $quantity; ?>"><br>
<?php if ($isSerialDevice) {
echo "Device: {$deviceURI}<br>";
} else {
echo "IP: <input type=\"text\" name=\"ip\" value=\"{$ip}\"";
if (!$canChangeIpPort) {
echo ' disabled';
}
echo "><br>";
echo "Port: <input type=\"number\" name=\"port\" value=\"{$port}\"><br>";
} ?>
UnitID (SlaveID): <input type="number" min="0" max="247" name="unitid" value="<?php echo $unitId; ?>"><br>
Address: <input type="number" name="address" value="<?php echo $startAddress; ?>"> (NB: does your modbus server
documentation uses
`0` based addressing or `1` based?)<br>
Quantity: <input type="number" min="1" max="124" name="quantity" value="<?php echo $quantity; ?>"><br>
Endianess: <select name="endianess">
<option value="1" <?php if ($endianess === 1) { echo 'selected'; } ?>>BIG_ENDIAN</option>
<option value="5" <?php if ($endianess === 5) { echo 'selected'; } ?>>BIG_ENDIAN_LOW_WORD_FIRST</option>
<option value="2" <?php if ($endianess === 2) { echo 'selected'; } ?>>LITTLE_ENDIAN</option>
<option value="6" <?php if ($endianess === 6) { echo 'selected'; } ?>>LITTLE_ENDIAN_LOW_WORD_FIRST</option>
<option value="1" <?php if ($endianess === 1) {
echo 'selected';
} ?>>BIG_ENDIAN
</option>
<option value="5" <?php if ($endianess === 5) {
echo 'selected';
} ?>>BIG_ENDIAN_LOW_WORD_FIRST
</option>
<option value="2" <?php if ($endianess === 2) {
echo 'selected';
} ?>>LITTLE_ENDIAN
</option>
<option value="6" <?php if ($endianess === 6) {
echo 'selected';
} ?>>LITTLE_ENDIAN_LOW_WORD_FIRST
</option>
</select><br>
<button type="submit">Send</button>
</form>
Expand Down
5 changes: 5 additions & 0 deletions examples/rtu_usb_to_serial.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
$sttyModes = implode(' ', [
'cs8', // enable character size 8 bits
'9600', // enable baud rate 9600
'-cstopb', // 1 stop bit
'-parenb', // parity none

'-icanon', // disable enable special characters: erase, kill, werase, rprnt
'min 0', // with -icanon, set N characters minimum for a completed read
'ignbrk', // enable ignore break characters
Expand Down Expand Up @@ -59,6 +62,8 @@

do {
// give sensor (5ms) some time to respond. SHT20 modbus minimal response time seems to be 20ms and more
// this is crucial for some serial devices and delay needs to be even longer (100ms) or you will experience
// read errors or invalid CRCs
usleep(5000);
$binaryData = fread($fd, 255);
} while ($binaryData === '');
Expand Down
3 changes: 3 additions & 0 deletions examples/rtu_usb_to_serial_stream.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
->setIsCompleteCallback(static function ($binaryData, $streamIndex): bool {
return Packet::isCompleteLengthRTU($binaryData);
})
// delay this is crucial for some serial devices and delay needs to be long as 100ms (depending on the quantity)
// or you will experience read errors ("stream_select interrupted") or invalid CRCs
->setDelayRead(100_000) // 100 milliseconds, serial devices may need delay between sending and received
->setLogger(new EchoLogger())
->build();

Expand Down
7 changes: 7 additions & 0 deletions src/Network/BinaryStreamConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public function __construct(BinaryStreamConnectionBuilder $builder)
$this->connectTimeoutSec = $builder->getConnectTimeoutSec();
$this->readTimeoutSec = $builder->getReadTimeoutSec();
$this->writeTimeoutSec = $builder->getWriteTimeoutSec();
$this->delayRead = $builder->getDelayRead();
$this->protocol = $builder->getProtocol();
$this->logger = $builder->getLogger();
$this->createStreamCallback = $builder->getCreateStreamCallback();
Expand Down Expand Up @@ -57,6 +58,12 @@ public function connect(): BinaryStreamConnection

public function receive(): string
{
$delay = $this->getDelayRead();
if ($delay > 0) {
// this is useful slow serial devices that need delay between writing request to the serial device
// and receiving response from device.
usleep($delay);
}
$result = $this->receiveFrom([$this->stream], $this->getReadTimeoutSec(), $this->getLogger());
return reset($result);
}
Expand Down
11 changes: 11 additions & 0 deletions src/Network/BinaryStreamConnectionBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,17 @@ public function setWriteTimeoutSec(float $writeTimeoutSec): static
return $this;
}

/**
* @param int $delayReadMicroSec delay before read in done (microseconds). This is useful for (USB) Serial
* devices that need time between writing request to the device and reading the response from device.
* @return static
*/
public function setDelayRead(int $delayReadMicroSec): static
{
$this->delayRead = $delayReadMicroSec;
return $this;
}

/**
* @param string $protocol
* @return static
Expand Down
14 changes: 14 additions & 0 deletions src/Network/BinaryStreamConnectionProperties.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ abstract class BinaryStreamConnectionProperties
*/
protected float $writeTimeoutSec = 1;

/**
* @var int delay before read in done (microseconds). This is useful for (USB) Serial devices that need time between
* writing the request to the device and reading the response from device.
*/
protected int $delayRead = 0;

/**
* @var string|null uri to connect to. Has higher priority than $protocol/$host/$port. Example: 'tcp://192.168.0.1:502'
*/
Expand Down Expand Up @@ -165,6 +171,14 @@ public function getCreateStreamCallback(): callable
return $this->createStreamCallback;
}

/**
* @return int
*/
public function getDelayRead(): int
{
return $this->delayRead;
}

/**
* @return callable
*/
Expand Down
3 changes: 3 additions & 0 deletions src/Network/SerialStreamCreator.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ class SerialStreamCreator implements StreamCreator
const DEFAULT_STTY_MODES = [
'cs8', // set character size 8 bits
'9600', // set baud rate 9600
'-cstopb', // 1 stop bit
'-parenb', // parity none

'-icanon', // disable enable special characters: erase, kill, werase, rprnt
'min 0', // with -icanon, set N characters minimum for a completed read
'ignbrk', // enable ignore break characters
Expand Down
5 changes: 3 additions & 2 deletions src/Packet/ModbusFunction/MaskWriteRegisterRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use ModbusTcpClient\Packet\ModbusRequest;
use ModbusTcpClient\Packet\ProtocolDataUnitRequest;
use ModbusTcpClient\Packet\Word;
use ModbusTcpClient\Utils\Endian;
use ModbusTcpClient\Utils\Types;

/**
Expand Down Expand Up @@ -121,8 +122,8 @@ public static function parse(string $binaryString): MaskWriteRegisterRequest|Err
14,
ModbusPacket::MASK_WRITE_REGISTER,
function (int $transactionId, int $unitId, int $startAddress) use ($binaryString) {
$andMask = Types::parseInt16($binaryString[10] . $binaryString[11]);
$orMask = Types::parseInt16($binaryString[12] . $binaryString[13]);
$andMask = Types::parseInt16($binaryString[10] . $binaryString[11], Endian::BIG_ENDIAN);
$orMask = Types::parseInt16($binaryString[12] . $binaryString[13], Endian::BIG_ENDIAN);
return new self($startAddress, $andMask, $orMask, $unitId, $transactionId);
}
);
Expand Down
5 changes: 3 additions & 2 deletions src/Packet/ModbusFunction/MaskWriteRegisterResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use ModbusTcpClient\Packet\ModbusPacket;
use ModbusTcpClient\Packet\StartAddressResponse;
use ModbusTcpClient\Packet\Word;
use ModbusTcpClient\Utils\Endian;
use ModbusTcpClient\Utils\Types;

/**
Expand Down Expand Up @@ -37,8 +38,8 @@ class MaskWriteRegisterResponse extends StartAddressResponse
public function __construct(string $rawData, int $unitId = 0, int $transactionId = null)
{
parent::__construct($rawData, $unitId, $transactionId);
$this->andMask = Types::parseUInt16(substr($rawData, 2, 2));
$this->orMask = Types::parseUInt16(substr($rawData, 4, 2));
$this->andMask = Types::parseUInt16(substr($rawData, 2, 2), Endian::BIG_ENDIAN);
$this->orMask = Types::parseUInt16(substr($rawData, 4, 2), Endian::BIG_ENDIAN);
}

public function getFunctionCode(): int
Expand Down
3 changes: 2 additions & 1 deletion src/Packet/ModbusFunction/ReadCoilsRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use ModbusTcpClient\Packet\ModbusPacket;
use ModbusTcpClient\Packet\ModbusRequest;
use ModbusTcpClient\Packet\ProtocolDataUnitRequest;
use ModbusTcpClient\Utils\Endian;
use ModbusTcpClient\Utils\Types;

/**
Expand Down Expand Up @@ -87,7 +88,7 @@ public static function parse(string $binaryString): ErrorResponse|ReadCoilsReque
12,
ModbusPacket::READ_COILS,
function (int $transactionId, int $unitId, int $startAddress) use ($binaryString) {
$quantity = Types::parseUInt16($binaryString[10] . $binaryString[11]);
$quantity = Types::parseUInt16($binaryString[10] . $binaryString[11], Endian::BIG_ENDIAN);
return new self($startAddress, $quantity, $unitId, $transactionId);
}
);
Expand Down
3 changes: 2 additions & 1 deletion src/Packet/ModbusFunction/ReadHoldingRegistersRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use ModbusTcpClient\Packet\ModbusPacket;
use ModbusTcpClient\Packet\ModbusRequest;
use ModbusTcpClient\Packet\ProtocolDataUnitRequest;
use ModbusTcpClient\Utils\Endian;
use ModbusTcpClient\Utils\Types;

/**
Expand Down Expand Up @@ -86,7 +87,7 @@ public static function parse(string $binaryString): ErrorResponse|ReadHoldingReg
12,
ModbusPacket::READ_HOLDING_REGISTERS,
function (int $transactionId, int $unitId, int $startAddress) use ($binaryString) {
$quantity = Types::parseUInt16($binaryString[10] . $binaryString[11]);
$quantity = Types::parseUInt16($binaryString[10] . $binaryString[11], Endian::BIG_ENDIAN);
return new self($startAddress, $quantity, $unitId, $transactionId);
}
);
Expand Down
3 changes: 2 additions & 1 deletion src/Packet/ModbusFunction/ReadInputDiscretesRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

use ModbusTcpClient\Packet\ErrorResponse;
use ModbusTcpClient\Packet\ModbusPacket;
use ModbusTcpClient\Utils\Endian;
use ModbusTcpClient\Utils\Types;

/**
Expand Down Expand Up @@ -40,7 +41,7 @@ public static function parse(string $binaryString): ReadInputDiscretesRequest|Er
12,
ModbusPacket::READ_INPUT_DISCRETES,
function (int $transactionId, int $unitId, int $startAddress) use ($binaryString) {
$quantity = Types::parseUInt16($binaryString[10] . $binaryString[11]);
$quantity = Types::parseUInt16($binaryString[10] . $binaryString[11], Endian::BIG_ENDIAN);
return new self($startAddress, $quantity, $unitId, $transactionId);
}
);
Expand Down
3 changes: 2 additions & 1 deletion src/Packet/ModbusFunction/ReadInputRegistersRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

use ModbusTcpClient\Packet\ErrorResponse;
use ModbusTcpClient\Packet\ModbusPacket;
use ModbusTcpClient\Utils\Endian;
use ModbusTcpClient\Utils\Types;

/**
Expand Down Expand Up @@ -40,7 +41,7 @@ public static function parse(string $binaryString): ReadInputRegistersRequest|Er
12,
ModbusPacket::READ_INPUT_REGISTERS,
function (int $transactionId, int $unitId, int $startAddress) use ($binaryString) {
$quantity = Types::parseUInt16($binaryString[10] . $binaryString[11]);
$quantity = Types::parseUInt16($binaryString[10] . $binaryString[11], Endian::BIG_ENDIAN);
return new self($startAddress, $quantity, $unitId, $transactionId);
}
);
Expand Down
Loading

0 comments on commit 8ccb744

Please sign in to comment.