Skip to content

Commit

Permalink
Merge pull request #3 from aldas/parse_strings
Browse files Browse the repository at this point in the history
Parse strings
  • Loading branch information
aldas committed Apr 30, 2018
2 parents f7792cd + 32ac208 commit 34f66be
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 7 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ php:
- 5.6
- 7.0
- 7.1
- 7.2

before_install:
- travis_retry composer self-update
Expand Down
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
29 changes: 27 additions & 2 deletions src/Packet/ModbusFunction/ReadHoldingRegistersResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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);
}
}
41 changes: 40 additions & 1 deletion src/Utils/Types.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -388,4 +388,43 @@ 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
}
}
}

$rawLen = strlen($data);
if (!$length || $length > $rawLen) {
$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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
34 changes: 34 additions & 0 deletions tests/unit/Utils/TypesTest.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?php

namespace Tests\Utils;


Expand Down Expand Up @@ -389,4 +390,37 @@ public function testShouldParseFloatAsLittleEndian()
$this->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("\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);

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

}

0 comments on commit 34f66be

Please sign in to comment.