From cd992925ce668d4e93da1c0a8feead9fcb49f976 Mon Sep 17 00:00:00 2001 From: toimtoimtoim Date: Mon, 30 Apr 2018 14:26:34 +0300 Subject: [PATCH 1/4] support parsin string from data --- src/Utils/Types.php | 40 +++++++++++++++++++++++++++++++++- tests/unit/Utils/TypesTest.php | 30 +++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/Utils/Types.php b/src/Utils/Types.php index 09cf255..f4f951f 100644 --- a/src/Utils/Types.php +++ b/src/Utils/Types.php @@ -329,7 +329,7 @@ public static function parseFloat($binaryData, $endianness = null) { $endianness = Endian::getCurrentEndianness($endianness); if ($endianness & Endian::LOW_WORD_FIRST) { - $binaryData = substr($binaryData, 2,2) . substr($binaryData, 0,2); + $binaryData = substr($binaryData, 2, 2) . substr($binaryData, 0, 2); } if ($endianness & Endian::BIG_ENDIAN) { @@ -388,4 +388,42 @@ public static function parseUInt64($binaryData, $endianness = null) return $result; } + /** + * Parse ascii string from registers to utf-8 string. Supports extended ascii codes ala 'ø' (decimal 248) + * + * @param string $binaryData binary string representing register (words) contents + * @param int $length number of characters to parse from data + * @param int $endianness byte and word order for modbus binary data + * @return string + */ + public static function parseAsciiStringFromRegister($binaryData, $length = 0, $endianness = null) + { + $data = $binaryData; + + $endianness = Endian::getCurrentEndianness($endianness); + if ($endianness & Endian::BIG_ENDIAN) { + + $data = ''; + // big endian needs bytes in word reversed + foreach (str_split($binaryData, 2) as $word) { + if (isset($word[1])) { + $data .= $word[1] . $word[0]; // low byte + high byte + } else { + $data .= $word[0]; // assume that last single byte is in correct place + } + } + } + + if (!$length) { + $length = strlen($data); + } + + $result = unpack("Z{$length}", $data)[1]; + + // needed to for extended ascii characters as 'ø' (decimal 248) + $result = mb_convert_encoding($result, 'UTF-8', 'ASCII'); + + return $result; + } + } \ No newline at end of file diff --git a/tests/unit/Utils/TypesTest.php b/tests/unit/Utils/TypesTest.php index d05d4d5..bad7d49 100644 --- a/tests/unit/Utils/TypesTest.php +++ b/tests/unit/Utils/TypesTest.php @@ -1,4 +1,5 @@ assertEquals(0, Types::parseFloat("\x00\x00\x00\x00", Endian::LITTLE_ENDIAN), null, 0.0000001); } + public function testShouldParseStringFromRegisterAsLittleEndian() + { + // null terminated data + $string = Types::parseAsciiStringFromRegister("\x53\xF8\x72\x65\x6E\x00", 0, Endian::LITTLE_ENDIAN); + $this->assertEquals('Søren', $string); + + $string = Types::parseAsciiStringFromRegister("\x53\xF8\x72\x65\x6E", 0, Endian::LITTLE_ENDIAN); + $this->assertEquals('Søren', $string); + + // parse substring from data + $string = Types::parseAsciiStringFromRegister("\x53\xF8\x72\x65\x6E\x00", 3, Endian::LITTLE_ENDIAN); + $this->assertEquals('Sør', $string); + } + + public function testShouldParseStringFromRegisterAsBigEndian() + { + // null terminated data + $string = Types::parseAsciiStringFromRegister("\xF8\x53\x65\x72\x00\x6E", 0, Endian::BIG_ENDIAN); + $this->assertEquals('Søren', $string); + + // odd number of bytes in data + $string = Types::parseAsciiStringFromRegister("\xF8\x53\x65\x72\x00", 0, Endian::BIG_ENDIAN); + $this->assertEquals('Søre', $string); + + // parse substring from data + $string = Types::parseAsciiStringFromRegister("\xF8\x53\x65\x72\x00\x6E", 3, Endian::BIG_ENDIAN); + $this->assertEquals('Sør', $string); + } + } \ No newline at end of file From 9c13d9891f04821fe0237b397500b7cee2112eb8 Mon Sep 17 00:00:00 2001 From: toimtoimtoim Date: Mon, 30 Apr 2018 15:10:14 +0300 Subject: [PATCH 2/4] add getAsciiStringAt() to Read Holding Registers (FC=03) response --- .../ReadHoldingRegistersResponse.php | 29 ++++++++++++- src/Utils/Types.php | 3 +- .../ReadHoldingRegistersResponseTest.php | 43 +++++++++++++++++++ tests/unit/Utils/TypesTest.php | 4 ++ 4 files changed, 76 insertions(+), 3 deletions(-) diff --git a/src/Packet/ModbusFunction/ReadHoldingRegistersResponse.php b/src/Packet/ModbusFunction/ReadHoldingRegistersResponse.php index 990c2d3..693e14d 100644 --- a/src/Packet/ModbusFunction/ReadHoldingRegistersResponse.php +++ b/src/Packet/ModbusFunction/ReadHoldingRegistersResponse.php @@ -157,7 +157,7 @@ public function getDoubleWordAt($firstWordAddress) { $address = ($firstWordAddress - $this->getStartAddress()) * 2; $byteCount = $this->getByteCount(); - if ($address < 0 || ($address+4) > $byteCount) { + if ($address < 0 || ($address + 4) > $byteCount) { throw new \OutOfBoundsException('address out of bounds'); } return new DoubleWord(substr($this->data, $address, 4)); @@ -171,9 +171,34 @@ public function getQuadWordAt($firstWordAddress) { $address = ($firstWordAddress - $this->getStartAddress()) * 2; $byteCount = $this->getByteCount(); - if ($address < 0 || ($address+8) > $byteCount) { + if ($address < 0 || ($address + 8) > $byteCount) { throw new \OutOfBoundsException('address out of bounds'); } return new QuadWord(substr($this->data, $address, 8)); } + + /** + * Parse ascii string from registers to utf-8 string + * + * @param $startFromWord int start parsing string from that word/register + * @param $length int count of characters to parse + * @param int $endianness byte and word order for modbus binary data + * @return string + */ + public function getAsciiStringAt($startFromWord, $length, $endianness = null) + { + $address = ($startFromWord - $this->getStartAddress()) * 2; + + $byteCount = $this->getByteCount(); + if ($address < 0 || $address >= $byteCount) { + throw new \OutOfBoundsException('startFromWord out of bounds'); + } + if ($length < 1) { + // length can be bigger than bytes count - we will just parse less as there is nothing to parse + throw new \OutOfBoundsException('length out of bounds'); + } + + $binaryData = substr($this->data, $address); + return Types::parseAsciiStringFromRegister($binaryData, $length, $endianness); + } } diff --git a/src/Utils/Types.php b/src/Utils/Types.php index f4f951f..d65f36c 100644 --- a/src/Utils/Types.php +++ b/src/Utils/Types.php @@ -414,7 +414,8 @@ public static function parseAsciiStringFromRegister($binaryData, $length = 0, $e } } - if (!$length) { + $rawLen = strlen($data); + if (!$length || $length > $rawLen) { $length = strlen($data); } diff --git a/tests/unit/Packet/ModbusFunction/ReadHoldingRegistersResponseTest.php b/tests/unit/Packet/ModbusFunction/ReadHoldingRegistersResponseTest.php index 9a0bc5d..746fc5e 100644 --- a/tests/unit/Packet/ModbusFunction/ReadHoldingRegistersResponseTest.php +++ b/tests/unit/Packet/ModbusFunction/ReadHoldingRegistersResponseTest.php @@ -302,4 +302,47 @@ public function testGetQuadWordAtOutOfBounderOver() $packet->getQuadWordAt(51); } + public function testGetAsciiString() + { + $packet = (new ReadHoldingRegistersResponse("\x08\x01\x00\xF8\x53\x65\x72\x00\x6E", 3, 33152))->withStartAddress(50); + $this->assertCount(4, $packet->getWords()); + + $this->assertEquals('Søren', $packet->getAsciiStringAt(51,5)); + } + + /** + * @expectedException \OutOfBoundsException + * @expectedExceptionMessage startFromWord out of bounds + */ + public function testGetAsciiStringInvalidAddressLow() + { + $packet = (new ReadHoldingRegistersResponse("\x08\x01\x00\xF8\x53\x65\x72\x00\x6E", 3, 33152))->withStartAddress(50); + $this->assertCount(4, $packet->getWords()); + + $packet->getAsciiStringAt(49,5); + } + + /** + * @expectedException \OutOfBoundsException + * @expectedExceptionMessage startFromWord out of bounds + */ + public function testGetAsciiStringInvalidAddressHigh() + { + $packet = (new ReadHoldingRegistersResponse("\x08\x01\x00\xF8\x53\x65\x72\x00\x6E", 3, 33152))->withStartAddress(50); + $this->assertCount(4, $packet->getWords()); + + $packet->getAsciiStringAt(54,5); + } + + /** + * @expectedException \OutOfBoundsException + * @expectedExceptionMessage length out of bounds + */ + public function testGetAsciiStringInvalidLength() + { + $packet = (new ReadHoldingRegistersResponse("\x08\x01\x00\xF8\x53\x65\x72\x00\x6E", 3, 33152))->withStartAddress(50); + $this->assertCount(4, $packet->getWords()); + + $packet->getAsciiStringAt(50,0); + } } \ No newline at end of file diff --git a/tests/unit/Utils/TypesTest.php b/tests/unit/Utils/TypesTest.php index bad7d49..b5873de 100644 --- a/tests/unit/Utils/TypesTest.php +++ b/tests/unit/Utils/TypesTest.php @@ -406,6 +406,10 @@ public function testShouldParseStringFromRegisterAsLittleEndian() public function testShouldParseStringFromRegisterAsBigEndian() { + // null terminated data + $string = Types::parseAsciiStringFromRegister("\x00\x6E", 10, Endian::BIG_ENDIAN); + $this->assertEquals('n', $string); + // null terminated data $string = Types::parseAsciiStringFromRegister("\xF8\x53\x65\x72\x00\x6E", 0, Endian::BIG_ENDIAN); $this->assertEquals('Søren', $string); From adf3cc470f03dc4356e97940ed9a5ad066620403 Mon Sep 17 00:00:00 2001 From: toimtoimtoim Date: Mon, 30 Apr 2018 15:39:42 +0300 Subject: [PATCH 3/4] improve a little documentation about endianess --- README.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9085903..4193500 100644 --- a/README.md +++ b/README.md @@ -23,11 +23,18 @@ This library is influenced by [phpmodbus](https://github.com/adduc/phpmodbus) library and meant to be provide decoupled Modbus protocol (request/response packets) and networking related features so you could build modbus client with our own choice of networking code (ext_sockets/streams/Reactphp asynchronous streams) or use library provided networking classes (php Streams) ## Endianness +Applies to multibyte data that are stored in Word/Double/Quad word registers basically everything +that is not (u)int16/byte/char. + +So if we receive from network 0x12345678 (bytes: ABCD) and want to convert that to a 32 bit register there could be 4 different +ways to interpret bytes and word order depending on modbus server architecture and client architecture. +NB: TCP, and UDP, are transmitted in big-endian order so we choose this as base for examples + Library supports following byte and word orders: -* Big endian (ABCD) -* Big endian low word first (CDAB) (used by Wago-750) -* Little endian (DCBA) -* Little endian low word first (BADC) +* Big endian (ABCD - word1 = 0x1234, word2 = 0x5678) +* Big endian low word first (CDAB - word1 = 0x5678, word2 = 0x1234) (used by Wago-750) +* Little endian (DCBA - word1 = 0x3412, word2 = 0x7856) +* Little endian low word first (BADC - word1 = 0x7856, word2 = 0x3412) See [Endian.php](src/Utils/Endian.php) for additional info and [Types.php](src/Utils/Types.php) for supported data types. From 32ac20829d9ac85db1d484511511842b86323c77 Mon Sep 17 00:00:00 2001 From: toimtoimtoim Date: Mon, 30 Apr 2018 15:43:54 +0300 Subject: [PATCH 4/4] add php 7.2 to travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index ec666df..35da768 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,7 @@ php: - 5.6 - 7.0 - 7.1 + - 7.2 before_install: - travis_retry composer self-update