Last active
January 21, 2024 08:22
-
-
Save Sarjuuk/77b69c4cc5e6ce3a8c053d9189b9bf28 to your computer and use it in GitHub Desktop.
communication interface for SIEMENS S5-CP524 and S5-CP525 communications processor
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// Procedure 3964R | |
// Interpreter RK512 | |
class Com525 | |
{ | |
// commands | |
private const CMD_SEND = 0x41; // 'A' | |
private const CMD_SEND_X = 0x4F; // 'O' | |
private const CMD_FETCH = 0x45; // 'E' | |
// control chars | |
private const NUL = 0x00; // NULL | |
private const STX = 0x02; // start transaction | |
private const ETX = 0x03; // end transaction | |
private const DLE = 0x10; // pos. ACK | |
private const NAK = 0x15; // neg. ACK | |
private const DEL = 0xFF; // delete | |
// private const BCC = ; // individual packet checksum | |
/* | |
Am Ende jedes Datenblocks wird | |
zur Datensichenmg ein Blockprüfzeichen (BCC) gesendet. Das | |
Blockprüfzeichen BCC ist die gerade Längsparität der Informationsbits | |
aller Datenbytes eines gesendeten bzw. empfangenen | |
Blocks (EXCLUSIV-ODER-Verknüpfung). Die Bildung beginnt mit dem | |
ersten Nutzdatenbyte nach dem Verbindungsaufbau und endet nach | |
dem Zeichen DLE ETX beim Verbindungsabbau. Für die Informationszeichen | |
ist kein Code vorgeschrieben (Codetransparenz). | |
... XOR everything! | |
*/ | |
// processes | |
public const PROC_3964 = 1; | |
public const PROC_3964R = 2; | |
// log levels | |
public const LOG_NONE = 0; | |
public const LOG_ERROR = 1; | |
public const LOG_WARN = 2; | |
public const LOG_INFO = 3; | |
public const LOG_DEBUG = 4; | |
// timer | |
private const SEND_DELAY = 80 * 1000; | |
private const NAK_DELAY = 1000 * 1000; | |
private $baud = array( | |
11 => 110, | |
15 => 150, | |
30 => 300, | |
60 => 600, | |
12 => 1200, | |
24 => 2400, | |
48 => 4800, | |
96 => 9600, | |
19 => 19200 | |
); | |
private $dataSrc = array( // ! deDE compatible ! | |
'DB' => ['D', 2], // data block | |
'DX' => ['X', 2], // ext. data block | |
'EB' => ['E', 1], // input byte | |
'AB' => ['A', 1], // output byte | |
'MB' => ['M', 1], // flag byte | |
'PB' => ['P', 1], // I/O byte | |
'ZB' => ['Z', 2], // counter location | |
'TB' => ['T', 2], // timer location | |
'AS' => ['S', 2], // absolute address | |
'BS' => ['B', 2], // system address | |
'QB' => ['Q', 1] // ext. I/O | |
); | |
private $logLevel; | |
private $doHash; | |
private $hasMore; | |
private $serial; | |
private $byteBuffer; | |
private $bbIdx; | |
public function __construct(string $port = 'COM1', int $baud = 96, int $proc = self::PROC_3964R, int $logLevel = self::LOG_NONE, array $params = []) | |
{ | |
if (substr(PHP_OS, 0, 3) != 'WIN') | |
{ | |
$this->log('ERR: OS not supported :('); | |
return; | |
} | |
if ($proc == self::PROC_3964) | |
$this->doHash = false; | |
else if ($proc == self::PROC_3964R) | |
$this->doHash = true; | |
else | |
{ | |
$this->log('ERR: unknown procedure selected'); | |
return; | |
} | |
// dafault baud rate | |
if (!in_array($baud, $this->baud) || !isset($this->baud[$baud])) | |
$baud = 96; | |
if (!preg_match('/^COM\d+$/i', $port)) | |
{ | |
$this->log('ERR: '.$port.' is not a valid COM port'); | |
return; | |
} | |
$this->logLevel = $logLevel; | |
foreach ($params as $n => $p) | |
$this->$n = $p; | |
// Parity, Databits and Stopbits are fixed for Procedure 3964R | |
/* | |
MODE COMm[:] [BAUD=b] [PARITY=n|o|e] [DATA=d] [STOP=s] | |
[to=on|off] [xon=on|off] [odsr=on|off] | |
[octs=on|off] [dtr=on|off|hs] | |
[rts=on|off|hs|tg] [idsr=on|off] | |
Baudrate: 9600 | |
Parität: Even | |
Datenbits: 8 | |
Stoppbits: 1 | |
Timeout: OFF | |
XON/XOFF: OFF | |
CTS-Handshaking: OFF | |
DSR-Handshaking: OFF | |
DSR-Prüfung: OFF | |
DTR-Signal: OFF | |
RTS-Signal: OFF | |
*/ | |
exec('mode '.$port.': BAUD='.$baud.' PARITY=E DATA=8 STOP=1 to=off xon=off odsr=off octs=off dtr=off rts=off idsr=off'); | |
$this->serial = fopen($port, 'r+b'); | |
if (!$this->serial) | |
{ | |
$this->log('ERR: could not open communications port'); | |
return; | |
} | |
stream_set_blocking($this->serial, 0); | |
} | |
/*****************/ | |
/* public access */ | |
/*****************/ | |
public function fetchDirect(string $src, int $srcIdx, int $startAddr, int $len, int $cpu = 0, string $cf = '') : array | |
{ | |
if (!$this->isConnected()) | |
{ | |
$this->log('ERR: not connected', self::LOG_ERROR); | |
return []; | |
} | |
$mult; // word = 2 * byte | |
if (isset($this->dataSrc[$src])) | |
{ | |
$mult = $this->dataSrc[$src][1]; | |
$src = ord($this->dataSrc[$src][0]); | |
} | |
else if ($x = array_search($src, array_column($this->dataSrc, 0))) | |
{ | |
$mult = $this->dataSrc[array_keys($this->dataSrc)[$x]][1]; | |
$src = ord($src); | |
} | |
else | |
{ | |
$this->log('ERR: unknown data source supplied - '.$src, self::LOG_ERROR); | |
return []; | |
} | |
if ($len <= 0) | |
{ | |
$this->log('ERR: len too small (min. 1)- '.$len, self::LOG_ERROR); | |
return []; | |
} | |
if ($cpu > 15) | |
{ | |
$this->log('ERR: cpuID too large (max. 15) - '.$cpu, self::LOG_ERROR); | |
return []; | |
} | |
if ($cf && !preg_match('/^[0-9]+\.[0-7]$/', $cf)) | |
{ | |
$this->log('ERR: malformed coord. flag supplied - '.$cf, self::LOG_ERROR); | |
return []; | |
} | |
$this->byteBuffer = []; | |
// max data length per request: 128B | |
for ($i = 0; $i <= ($len * $mult - 1) / 128; $i++) | |
{ | |
$this->hasMore = $i > 0; | |
$tsx = $this->createTransaction(self::CMD_FETCH, $src, $srcIdx, $startAddr, $len, [], $cpu, $cf); | |
if (!$this->process($tsx)) | |
return []; | |
} | |
// return words if needed | |
if ($mult == 2) | |
{ | |
$result = []; | |
$i = 0; | |
foreach ($this->byteBuffer as $idx => $byte) | |
{ | |
if (!($idx % 2)) | |
{ | |
if (!$i) | |
$i = $idx; | |
$result[$i] = ($byte << 8); | |
} | |
else | |
{ | |
$result[$i] |= $byte; | |
$i++; | |
} | |
} | |
return $result; | |
} | |
return $this->byteBuffer; | |
} | |
public function sendDirect(string $dest, int $destIdx, int $startAddr, array $data, int $cpu = 0, string $cf = '') : bool | |
{ | |
if (!$this->isConnected()) | |
return false; | |
$mult; // word = 2 * byte | |
if ($this->dataSrc[$dest]) | |
{ | |
$mult = $this->dataSrc[$dest][1]; | |
$dest = ord($this->dataSrc[$dest][0]); | |
} | |
else if ($x = array_search($dest, array_column($this->dataSrc, 0))) | |
{ | |
$mult = $this->dataSrc[$x][1]; | |
$dest = ord($dest); | |
} | |
else | |
{ | |
$this->log('ERR: unknown data source supplied - '.$dest, self::LOG_ERROR); | |
return false; | |
} | |
if (!$data) | |
{ | |
$this->log('ERR: no data to send', self::LOG_ERROR); | |
return false; | |
} | |
if ($cpu > 15) | |
{ | |
$this->log('ERR: cpuID too large (max. 15) - '.$cpu, self::LOG_ERROR); | |
return false; | |
} | |
if ($cf && !preg_match('/^[0-9]+\.[0-7]$/', $cf)) | |
{ | |
$this->log('ERR: malformed coord. flag supplied - '.$cf, self::LOG_ERROR); | |
return false; | |
} | |
$cmd = chr($dest) == 'X' ? self::CMD_SEND_X : self::CMD_SEND; | |
$this->byteBuffer = []; | |
// words to bytes | |
if ($mult == 2) | |
{ | |
foreach ($data as $d) | |
{ | |
$this->byteBuffer[] = ($d >> 8) & 0xFF; // hi byte | |
$this->byteBuffer[] = $d & 0xFF; // lo byte | |
} | |
} | |
else | |
$this->byteBuffer = $data; | |
// max data length per request: 128B | |
for ($i = 0; $i <= ((count($this->byteBuffer) - 1) / 128); $i++) | |
{ | |
$this->hasMore = $i > 0; | |
$tsx = $this->createTransaction($cmd, $dest, $destIdx, $startAddr, count($data), array_slice($this->byteBuffer, $i * 128, 128), $cpu, $cf); | |
if (!$this->process($tsx)) | |
return false; | |
} | |
return true; | |
} | |
public function isConnected() : bool | |
{ | |
return $this->serial !== false; | |
} | |
public function flushFileBuffer() : void | |
{ | |
while (fread($this->serial, 1)); | |
} | |
/*********/ | |
/* WRITE */ | |
/*********/ | |
private function doACK() : void | |
{ | |
$this->write(self::DLE); | |
} | |
private function doNAK() : void | |
{ | |
$this->write(self::NAK); | |
} | |
private function startTransaction() : void | |
{ | |
$this->write(self::STX); | |
} | |
private function commitTransaction(int ...$bytes) : void | |
{ | |
// append end transaction bytes | |
array_push($bytes, self::DLE, self::ETX); | |
// ... also append BCC | |
if ($this->doHash) | |
{ | |
$bcc = 0x0; | |
foreach ($bytes as $b) | |
$bcc ^= $b; // xor | |
$bytes[] = $bcc; | |
} | |
$this->write(...$bytes); | |
} | |
/********/ | |
/* READ */ | |
/********/ | |
private function expectACK() : bool | |
{ | |
$bytes = []; | |
$this->read($bytes); | |
return $bytes === [self::DLE]; | |
} | |
private function expectTransaction(&$gotNAK = false) : bool | |
{ | |
$bytes = []; | |
$this->read($bytes); | |
$gotNAK = ($bytes === [self::NAK]); | |
return $bytes === [self::DLE, self::STX]; | |
} | |
private function receiveData() : bool | |
{ | |
$bytes = []; | |
$this->read($bytes, 50); | |
// BCC check if applicable | |
if ($this->doHash) | |
{ | |
$bcc = array_pop($bytes); | |
$hash = 0x0; | |
foreach ($bytes as $b) | |
$hash ^= $b; | |
if ($bcc !== $hash) | |
{ | |
$this->log('packet hash mismatch - received: '.$bcc.' calculated: '.$hash, self::LOG_WARN); | |
return false; | |
} | |
} | |
// check HEADER for errors (byte:0) | |
if ($this->hasMore && $bytes[0] != self::DEL) | |
{ | |
$this->log('ERR: received packet not tagged as followup packet', self::LOG_WARN); | |
return false; | |
} | |
else if (!$this->hasMore && $bytes[0] != self::NUL) | |
{ | |
$this->log('ERR: received packet not tagged as initial packet', self::LOG_WARN); | |
return false; | |
} | |
// check HEADER for errors (byte:3) | |
if ($bytes[3]) | |
{ | |
$this->log('Received Error Byte: 0x'.$bytes[3], self::LOG_WARN); | |
foreach ($this->errors as $k => $e) | |
if (($k & $bytes[3]) == $k) | |
$this->log($e, self::LOG_WARN); | |
return false; | |
} | |
// strip HEADER and FOOTER | |
$bytes = array_slice($bytes, 4, -2); | |
// <DLE> escapes <DLE> .. unescape | |
for ($i = 1; $i < count($bytes); $i++) | |
{ | |
if ($bytes[$i] != self::DLE || $bytes[$i-1] != $bytes[$i]) | |
continue; | |
unset($bytes[$i]); // preserves numeric keys in rest of array | |
$i++; | |
} | |
foreach ($bytes as $b) | |
$this->byteBuffer[$this->bbIdx++] = $b; | |
return true; | |
} | |
/*****************/ | |
/* communication */ | |
/*****************/ | |
private function createTransaction(int $cmd, int $src, int $srcIdx, int $startAddr, int $len, array $data = [], int $cpu = 0, string $cf = '') : array | |
{ | |
if (!$this->hasMore) | |
$this->bbIdx = $startAddr; | |
$packet = array( | |
$this->hasMore ? self::DEL : self::NUL, // 0: head 1 | |
self::NUL, // 1: head 2 | |
$cmd, // 2: FETCH | SEND | |
$src, // 3: DB probably | |
); | |
if (!$this->hasMore) | |
{ | |
if ($cf) | |
$cf = explode('.', $cf); // [byte, bit] | |
$byte10 = ($cf ? (int)$cf[1] : 0xF); | |
if (!$cpu && !$cf) | |
$byte10 |= 0xF0; | |
else if ($cpu && $cf) | |
$byte10 |= $cpu << 4; | |
$packet[] = $srcIdx; // 4: Block No. | |
$packet[] = $startAddr; // 5: | |
$packet[] = ($len & 0xFF00) >> 8; // 6: length hi-byte | |
$packet[] = $len & 0xFF; // 7: length lo-byte | |
$packet[] = $cf ? (int)$cf[0] : self::DEL; // 8: coordination flag - byte | |
$packet[] = $byte10; // 9: coordination flag - bit | CPU-Nr | |
} | |
if ($cmd == self::CMD_SEND || $cmd == self::CMD_SEND_X) | |
foreach ($data as $d) | |
$packet[] = $d; | |
return $packet; | |
} | |
private function process(array $tsx) : bool | |
{ | |
/* 1 */ | |
$this->startTransaction(); | |
/* 2 */ | |
if (!$this->expectACK()) | |
{ | |
// $this->doNAK(); | |
usleep(self::NAK_DELAY); | |
return false; | |
} | |
/* 3 */ | |
$this->commitTransaction(...$tsx); | |
/* 4 */ | |
// up to 5sec delay | |
$delay = 2000; | |
$update = 50; | |
$gotNAK = false; | |
while (!$this->expectTransaction($gotNAK) && !$gotNAK && $delay) | |
{ | |
$delay -= $update; | |
usleep($update * 1000); | |
} | |
if ($gotNAK) | |
{ | |
// $this->doACK(); | |
usleep(self::NAK_DELAY); | |
return false; | |
} | |
/* 5 */ | |
$this->doACK(); | |
/* 6 */ | |
if (!$this->receiveData()) | |
{ | |
// $this->doNAK(); | |
usleep(self::NAK_DELAY); | |
return false; | |
} | |
/* 7 */ | |
$this->doACK(); | |
return true; | |
} | |
private function write(int ...$bytes) : void | |
{ | |
$buff = ''; | |
foreach ($bytes as $p) | |
$buff .= chr($p); | |
$this->log(' >> WRITE >> '.implode(' ', array_map(function($x) { return '0x'.str_pad(strtoupper(dechex($x)), 2, '0', STR_PAD_LEFT); }, $bytes)), self::LOG_DEBUG); | |
fwrite($this->serial, $buff); | |
usleep(self::SEND_DELAY); | |
} | |
private function read(array &$bytes, int $idleTime = 0) : void | |
{ | |
$bytes = []; | |
$this->_read($bytes); | |
// hickups are possible during receive. recheck 5x $idleTime | |
$maxDelay = 5 * $idleTime; | |
while ($maxDelay > 0 && !$this->isEOT($bytes)) | |
{ | |
usleep($idleTime * 1000); | |
$maxDelay -= $idleTime; | |
$this->_read($bytes); | |
} | |
$this->log(' << RECV << '.implode(' ', array_map(function($x) { return '0x'.str_pad(strtoupper(dechex($x)), 2, '0', STR_PAD_LEFT); }, $bytes)), self::LOG_DEBUG); | |
} | |
private function _read(array &$bytes) : void | |
{ | |
$chr = fread($this->serial, 1); | |
while ($chr !== '') | |
{ | |
$bytes[] = ord($chr); | |
$chr = fread($this->serial, 1); | |
} | |
} | |
/******************/ | |
/* misc internals */ | |
/******************/ | |
// received data dump is closed by end transaction bytes | |
private function isEOT(array $data) : bool | |
{ | |
$base = $this->doHash ? 3 : 2; | |
if (count($data) < (4 + $base)) // header + footer + no data | |
return false; | |
return array_slice($data, -$base, 2) == [self::DLE, self::ETX]; | |
} | |
private function log(string $msg, int $lvl = self::LOG_ERROR /*, $raw = true*/) : void | |
{ | |
if (!$this->logLevel || $lvl > $this->logLevel) | |
return; | |
if ($msg) | |
echo str_pad(date('H:i:s'), 12).$msg; | |
echo "\n"; | |
} | |
} | |
?> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment