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