Skip to content

Instantly share code, notes, and snippets.

@Sarjuuk
Last active January 21, 2024 08:22
Show Gist options
  • Save Sarjuuk/77b69c4cc5e6ce3a8c053d9189b9bf28 to your computer and use it in GitHub Desktop.
Save Sarjuuk/77b69c4cc5e6ce3a8c053d9189b9bf28 to your computer and use it in GitHub Desktop.
communication interface for SIEMENS S5-CP524 and S5-CP525 communications processor
<?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