196 lines
7.4 KiB
PHP
196 lines
7.4 KiB
PHP
<?php
|
|
namespace App\Console\Commands;
|
|
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class PanasonicCstaBridgeCommand extends Command
|
|
{
|
|
protected $signature = 'cti:bridge-panasonic
|
|
{--host=192.168.0.101 : Panasonic host/IP}
|
|
{--port=33333 : Panasonic CSTA TCP port}
|
|
{--timeout=10 : Connect and read timeout in seconds}
|
|
{--read-bytes=2048 : Bytes to read per frame/chunk}
|
|
{--reconnect=1 : Auto reconnect when socket closes}
|
|
{--reconnect-delay=5 : Seconds before reconnect}
|
|
{--max-idle-timeouts=30 : Reconnect after N consecutive read timeouts (0 = never)}
|
|
{--startup-send= : Optional ASCII payload sent right after connect}
|
|
{--startup-send-hex= : Optional HEX payload sent right after connect}
|
|
{--log-file=storage/logs/cti-panasonic-raw.ndjson : NDJSON file where raw frames are appended}
|
|
{--print-hex=1 : Print each frame as hex in console output}
|
|
{--print-text=1 : Print best-effort text rendering in console output}';
|
|
|
|
protected $description = 'Minimal Panasonic CSTA bridge daemon: connects to the PBX, captures raw frames and keeps the socket alive for protocol analysis.';
|
|
|
|
public function handle(): int
|
|
{
|
|
$host = (string) $this->option('host');
|
|
$port = max(1, (int) $this->option('port'));
|
|
$timeout = max(1, (int) $this->option('timeout'));
|
|
$readBytes = max(64, min(65535, (int) $this->option('read-bytes')));
|
|
$reconnect = (bool) $this->option('reconnect');
|
|
$reconnectDelay = max(1, (int) $this->option('reconnect-delay'));
|
|
$maxIdleTimeouts = max(0, (int) $this->option('max-idle-timeouts'));
|
|
$startupSend = (string) ($this->option('startup-send') ?? '');
|
|
$startupSendHex = strtoupper(preg_replace('/[^0-9A-Fa-f]/', '', (string) ($this->option('startup-send-hex') ?? '')) ?? '');
|
|
$logFile = (string) $this->option('log-file');
|
|
$printHex = (bool) $this->option('print-hex');
|
|
$printText = (bool) $this->option('print-text');
|
|
|
|
if ($startupSend !== '' && $startupSendHex !== '') {
|
|
$this->error('Usa solo uno tra --startup-send e --startup-send-hex.');
|
|
return self::FAILURE;
|
|
}
|
|
|
|
if ($startupSendHex !== '' && strlen($startupSendHex) % 2 !== 0) {
|
|
$this->error('Il payload HEX deve avere un numero pari di caratteri.');
|
|
return self::FAILURE;
|
|
}
|
|
|
|
$startupPayload = $startupSendHex !== '' ? (hex2bin($startupSendHex) ?: ''): $startupSend;
|
|
$absoluteLogFile = $this->normalizeLogFilePath($logFile);
|
|
|
|
$this->info("Bridge Panasonic CSTA verso {$host}:{$port}");
|
|
$this->line('Log raw frames: ' . $absoluteLogFile);
|
|
|
|
do {
|
|
$socket = @stream_socket_client("tcp://{$host}:{$port}", $errno, $errstr, $timeout);
|
|
if (! is_resource($socket)) {
|
|
$this->error("Connessione fallita ({$errno}): {$errstr}");
|
|
if (! $reconnect) {
|
|
return self::FAILURE;
|
|
}
|
|
|
|
sleep($reconnectDelay);
|
|
continue;
|
|
}
|
|
|
|
stream_set_timeout($socket, $timeout);
|
|
stream_set_blocking($socket, true);
|
|
|
|
$this->info('Connessione CSTA stabilita.');
|
|
$this->appendFrameLog($absoluteLogFile, [
|
|
'type' => 'connection.opened',
|
|
'host' => $host,
|
|
'port' => $port,
|
|
'ts' => now()->toIso8601String(),
|
|
]);
|
|
|
|
if ($startupPayload !== '') {
|
|
@fwrite($socket, $startupPayload);
|
|
$this->appendFrameLog($absoluteLogFile, [
|
|
'type' => 'connection.startup_payload',
|
|
'host' => $host,
|
|
'port' => $port,
|
|
'ts' => now()->toIso8601String(),
|
|
'payload_hex' => strtoupper(bin2hex($startupPayload)),
|
|
'payload_text' => $this->renderPrintable($startupPayload),
|
|
]);
|
|
}
|
|
|
|
$idleTimeouts = 0;
|
|
while (! feof($socket)) {
|
|
$chunk = fread($socket, $readBytes);
|
|
$chunk = is_string($chunk) ? $chunk : '';
|
|
|
|
if ($chunk === '') {
|
|
$meta = stream_get_meta_data($socket);
|
|
if (($meta['timed_out'] ?? false) === true) {
|
|
$idleTimeouts++;
|
|
$this->warn("Timeout lettura CSTA ({$idleTimeouts})");
|
|
|
|
if ($maxIdleTimeouts > 0 && $idleTimeouts >= $maxIdleTimeouts) {
|
|
$this->warn('Soglia timeout raggiunta, riconnessione in corso.');
|
|
break;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
$idleTimeouts = 0;
|
|
$hex = strtoupper(bin2hex($chunk));
|
|
$text = $this->renderPrintable($chunk);
|
|
|
|
$record = [
|
|
'type' => 'frame.received',
|
|
'host' => $host,
|
|
'port' => $port,
|
|
'ts' => now()->toIso8601String(),
|
|
'size' => strlen($chunk),
|
|
'payload_hex' => $hex,
|
|
'payload_text' => $text,
|
|
];
|
|
|
|
$this->appendFrameLog($absoluteLogFile, $record);
|
|
|
|
if ($printHex) {
|
|
$this->line('HEX ' . implode(' ', str_split($hex, 2)));
|
|
}
|
|
|
|
if ($printText) {
|
|
$this->line('TEXT ' . ($text !== '' ? $text : '[binary/non-printable]'));
|
|
}
|
|
}
|
|
|
|
fclose($socket);
|
|
$this->appendFrameLog($absoluteLogFile, [
|
|
'type' => 'connection.closed',
|
|
'host' => $host,
|
|
'port' => $port,
|
|
'ts' => now()->toIso8601String(),
|
|
]);
|
|
$this->warn('Socket CSTA chiusa.');
|
|
|
|
if (! $reconnect) {
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
sleep($reconnectDelay);
|
|
} while (true);
|
|
}
|
|
|
|
private function normalizeLogFilePath(string $path): string
|
|
{
|
|
if (str_starts_with($path, '/')) {
|
|
return $path;
|
|
}
|
|
|
|
return base_path($path);
|
|
}
|
|
|
|
/**
|
|
* @param array<string,mixed> $payload
|
|
*/
|
|
private function appendFrameLog(string $logFile, array $payload): void
|
|
{
|
|
$dir = dirname($logFile);
|
|
if (! is_dir($dir)) {
|
|
@mkdir($dir, 0775, true);
|
|
}
|
|
|
|
$json = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
|
if (! is_string($json) || $json === '') {
|
|
return;
|
|
}
|
|
|
|
@file_put_contents($logFile, $json . PHP_EOL, FILE_APPEND);
|
|
|
|
Log::info('Panasonic CSTA bridge', [
|
|
'event' => $payload['type'] ?? 'unknown',
|
|
'host' => $payload['host'] ?? null,
|
|
'port' => $payload['port'] ?? null,
|
|
'size' => $payload['size'] ?? null,
|
|
]);
|
|
}
|
|
|
|
private function renderPrintable(string $payload): string
|
|
{
|
|
$printable = preg_replace('/[^\P{C}\n\r\t]/u', '.', $payload);
|
|
|
|
return is_string($printable) ? trim($printable) : '';
|
|
}
|
|
}
|