Ankündigung

Einklappen
Keine Ankündigung bisher.

PHP Skript zur KNX IP Tunneling Verbindung

Einklappen
X
 
  • Filter
  • Zeit
  • Anzeigen
Alles löschen
neue Beiträge

    PHP Skript zur KNX IP Tunneling Verbindung

    Hallo,

    ich stelle diesen kleinen PHP Code bereit für die, die Interesse haben, mehr mit PHP und KNX zu erstellen. Er baut eine IP Tunneling Verbindung auf, sendet Daten für DPT 1, 5 oder 9 und trennt sich danach wieder.

    Das hier hat auch nichts mit dem Wrapper von mir zu tun, es ist komplett eigenständig.
    ​


    PHP-Code:
    <?php
    header('Content-Type: text/html; charset=utf-8');
    
    class KNXTunnel {
        private string $ip;
        private int $port;
        private $socket = null;
        private int $channelId = 0;
        private int $sequenceNumber = 0;
    
        public function __construct(string $ip, int $port = 3671) {
            $this->ip = $ip;
            $this->port = $port;
        }
    
        private function log(string $msg): void {
            echo $msg . "<br>\n";
            if (ob_get_level() > 0) { @ob_flush(); }
            @flush();
        }
    
        private function getLocalIp(): string {
            $sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
            socket_connect($sock, $this->ip, $this->port);
            socket_getsockname($sock, $addr);
            socket_close($sock);
            return $addr;
        }
    
        private function statusText(int $status): string {
            return match ($status) {
                0x00 => 'E_NO_ERROR',
                0x22 => 'E_CONNECTION_TYPE (Verbindungstyp nicht unterstützt)',
                0x23 => 'E_CONNECTION_OPTION (Option nicht unterstützt)',
                0x24 => 'E_NO_MORE_CONNECTIONS (max. Anzahl Tunnel-Verbindungen erreicht)',
                0x26 => 'E_DATA_CONNECTION',
                0x27 => 'E_KNX_CONNECTION',
                0x29 => 'E_NO_MORE_UNIQUE_CONNECTIONS',
                default => 'unbekannt (0x' . sprintf('%02X', $status) . ')',
            };
        }
    
        public function connect(): bool {
            $this->socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
            if (!$this->socket) {
                $this->log("❌ Socket konnte nicht erstellt werden : " . socket_strerror(socket_last_error()));
                return false;
            }
    
            socket_set_option($this->socket, SOL_SOCKET, SO_RCVTIMEO, ['sec' => 3, 'usec' => 0]);
    
            $myPcIp = $this->getLocalIp();
            $this->log("ℹ️ Lokale IP : {$myPcIp}");
    
            if (!@socket_bind($this->socket, $myPcIp, 0)) {
                $this->log("❌ Fehler beim Binden an PC IP {$myPcIp} : " . socket_strerror(socket_last_error($this->socket)));
                return false;
            }
    
            $localPort = 0;
            socket_getsockname($this->socket, $myPcIp, $localPort);
            $ipParts = array_map('intval', explode('.', $myPcIp));
    
            $hpaiControl = pack('C*', 0x08, 0x01, $ipParts[0], $ipParts[1], $ipParts[2], $ipParts[3]) . pack('n', $localPort);
    
            $hpaiData = pack('C*', 0x08, 0x01, $ipParts[0], $ipParts[1], $ipParts[2], $ipParts[3]) . pack('n', $localPort);
    
            $header = "\x06\x10\x02\x05\x00\x1a";
    
            $cri = "\x04\x04\x02\x00";
    
            $connectRequest = $header . $hpaiControl . $hpaiData . $cri;
    
            socket_sendto($this->socket, $connectRequest, strlen($connectRequest), 0, $this->ip, $this->port);
            $this->log("📤 CONNECT_REQUEST gesendet an {$this->ip}:{$this->port}");
    
            $buffer = '';
            $from = '';
            $fromPort = 0;
            $bytes = @socket_recvfrom($this->socket, $buffer, 1024, 0, $from, $fromPort);
    
            if ($bytes === false) {
                $this->log("❌ recvfrom Fehler : " . socket_strerror(socket_last_error($this->socket)));
                return false;
            }
    
            if ($bytes < 8) {
                $this->log("❌ Zu kurze Antwort empfangen ({$bytes} Bytes).");
                return false;
            }
    
            $this->log("📥 Rohdaten empfangen von {$from}:{$fromPort} (" . $bytes . " Bytes): " . bin2hex($buffer));
    
            $serviceType = (ord($buffer[2]) << 8) | ord($buffer[3]);
    
            if ($serviceType !== 0x0206) {
                $this->log("❌ Unerwarteter Service Type : 0x" . sprintf('%04X', $serviceType) . " (erwartet : 0x0206 CONNECT_RESPONSE)");
                return false;
            }
    
            $status = ord($buffer[7]);
            if ($status !== 0x00) {
                $this->log("❌ IP Gateway abbruch, Status : 0x" . sprintf('%02X', $status) . " (" . $this->statusText($status) . ")");
                return false;
            }
    
            $this->channelId = ord($buffer[6]);
            $this->sequenceNumber = 0;
            $this->log("✅ IP Gateway Tunnel erfolgreich aufgebaut! (Channel ID : {$this->channelId})");
            return true;
        }
    
        private function parseGroupAddress(string $ga): string {
            $parts = explode('/', $ga);
            if (count($parts) === 3) {
                $main = (int)$parts[0] & 0x1F;
                $middle = (int)$parts[1] & 0x07;
                $sub = (int)$parts[2] & 0xFF;
                $val = ($main << 11) | ($middle << 8) | $sub;
                return pack('n', $val);
            }
            return "\x00\x00";
        }
    
        public function encodeDPT(string $dptType, $value): string {
            switch (strtoupper($dptType)) {
                case 'DPT1':
                    return pack('C', 0x80 | ($value ? 1 : 0));
    
                case 'DPT5':
                    $val = max(0, min(255, (int)$value));
                    return pack('CC', 0x80, $val);
    
                case 'DPT9':
                    $v = (float)$value;
                    $sign = ($v < 0) ? 0x8000 : 0x0000;
                    $v = abs($v);
                    $mantissa = (int)round($v * 100);
                    $exponent = 0;
                    while ($mantissa > 2047) {
                        $mantissa >>= 1;
                        $exponent++;
                    }
                    if ($sign) $mantissa = -$mantissa;
                    $raw = $sign | (($exponent & 0x0F) << 11) | ($mantissa & 0x07FF);
                    return pack('C*', 0x80, ($raw >> 8) & 0xFF, $raw & 0xFF);
    
                default:
                    throw new Exception("DPT Typ '{$dptType}' nicht unterstützt.");
            }
        }
    
        public function send(string $groupAddress, string $dptType, $value): bool {
            if ($this->channelId === 0) {
                $this->log("❌ send() ohne aktiven Tunnel aufgerufen, zuerst connect() erfolgreich ausführen.");
                return false;
            }
    
            $gaBytes = $this->parseGroupAddress($groupAddress);
            $payload = $this->encodeDPT($dptType, $value);
            $payloadLen = strlen($payload);
    
            $cemi = "\x11\x00\xbc\xe0\x00\x00" . $gaBytes . pack('C', $payloadLen) . "\x00" . $payload;
            $totalLen = 6 + 4 + strlen($cemi);
    
            $header = "\x06\x10\x04\x20" . pack('n', $totalLen);
            $connectionHeader = pack('CC', 0x04, $this->channelId) . pack('CC', $this->sequenceNumber, 0x00);
    
            $packet = $header . $connectionHeader . $cemi;
    
            socket_sendto($this->socket, $packet, strlen($packet), 0, $this->ip, $this->port);
            $this->log("📤 Telegramm an <b>{$groupAddress}</b> gesendet! (Seq: {$this->sequenceNumber})");
    
            $ack = $this->receiveACK();
            $this->sequenceNumber = ($this->sequenceNumber + 1) % 256;
    
            return $ack;
        }
    
        private function receiveACK(): bool {
            $buffer = '';
            $from = '';
            $port = 0;
            $bytes = @socket_recvfrom($this->socket, $buffer, 1024, 0, $from, $port);
    
            if ($bytes === false) {
                $this->log("❌ Kein ACK erhalten (Timeout) : " . socket_strerror(socket_last_error($this->socket)));
                return false;
            }
    
            if ($bytes >= 10 && ord($buffer[2]) === 0x04 && ord($buffer[3]) === 0x21) {
                $status = ord($buffer[9]);
                if ($status === 0x00) {
                    $this->log("📥 ACK vom IP Gateway erhalten.");
                    return true;
                }
                $this->log("⚠️ ACK erhalten, aber mit Fehlerstatus: 0x" . sprintf('%02X', $status));
                return false;
            }
    
            $this->log("⚠️ Unerwartete Antwort statt ACK erhalten : " . bin2hex($buffer));
            return false;
        }
    
        public function disconnect(): void {
            if ($this->channelId !== 0 && $this->socket) {
                $disconnectRequest = "\x06\x10\x02\x09\x00\x10" . pack('CC', $this->channelId, 0x00) . "\x08\x01\x00\x00\x00\x00\x00\x00";
                @socket_sendto($this->socket, $disconnectRequest, strlen($disconnectRequest), 0, $this->ip, $this->port);
                $this->log("🔌 IP Gateway Tunnel Verbindung (Channel {$this->channelId}) ordnungsgemäß getrennt.");
                $this->channelId = 0;
            }
            if ($this->socket) {
                @socket_close($this->socket);
                $this->socket = null;
            }
        }
    }
    
    $Ip = '192.168.178.42';
    $knx = new KNXTunnel($Ip);
    
    try {
        if ($knx->connect()) {
            $knx->send('1/0/0', 'DPT1', true);
        }
    } finally {
        $knx->disconnect();
    }​
    ​
    Mit besten Grüßen,
    Guido

    Meine Projekte hier : KNX Busmonitor​​ / KNX Schalter mit Audio​ / KNX PiRadio​
Lädt...
X