*/ private const CHANNEL_COUNTS = [ 'DI' => 8, 'DO' => 4, 'AI' => 2, 'AO' => 2, 'PT' => 2, ]; /** @var array */ private const DEFAULT_INTERVALS = [ 'DI' => 500, 'DO' => 500, 'AI' => 5000, 'AO' => 5000, 'PT' => 5000, ]; /** * @return list */ public static function defaultSelection(): array { $selection = []; foreach (self::CHANNEL_COUNTS as $type => $count) { for ($channel = 1; $channel <= $count; $channel++) { $selection[] = [ 'Aktiv' => false, 'Typ' => $type, 'Kanal' => $channel, 'Anzeigename' => self::defaultName($type, $channel), 'Abfrageintervall' => self::DEFAULT_INTERVALS[$type], ]; } } return $selection; } /** * @return list */ public static function normalizeSelection(string $json): array { try { $rows = json_decode($json, true, 512, JSON_THROW_ON_ERROR); } catch (\JsonException $exception) { throw new InvalidArgumentException('Kanalauswahl ist kein gueltiges JSON.', 0, $exception); } if (!is_array($rows)) { throw new InvalidArgumentException('Kanalauswahl muss eine Liste sein.'); } $result = []; $keys = []; foreach ($rows as $index => $row) { if (!is_array($row)) { throw new InvalidArgumentException('Kanalzeile ' . ($index + 1) . ' ist ungueltig.'); } $type = strtoupper(trim((string) ($row['Typ'] ?? ''))); $channel = filter_var($row['Kanal'] ?? null, FILTER_VALIDATE_INT); if ($channel === false || !isset(self::CHANNEL_COUNTS[$type])) { throw new InvalidArgumentException('Kanalzeile ' . ($index + 1) . ' hat einen ungueltigen Typ oder Kanal.'); } if ($channel < 1 || $channel > self::CHANNEL_COUNTS[$type]) { throw new InvalidArgumentException( sprintf('%s erlaubt nur die Kanaele 1 bis %d.', $type, self::CHANNEL_COUNTS[$type]) ); } $key = $type . $channel; if (isset($keys[$key])) { throw new InvalidArgumentException('Kanal ' . $key . ' ist doppelt konfiguriert.'); } $keys[$key] = true; $name = trim((string) ($row['Anzeigename'] ?? '')); if ($name === '') { $name = self::defaultName($type, $channel); } if (strlen($name) > 80) { throw new InvalidArgumentException('Der Anzeigename von ' . $key . ' ist zu lang.'); } $pollInterval = filter_var($row['Abfrageintervall'] ?? null, FILTER_VALIDATE_INT); if ($pollInterval === false || $pollInterval < 100 || $pollInterval > 3600000) { throw new InvalidArgumentException( 'Das Abfrageintervall von ' . $key . ' muss zwischen 100 und 3600000 ms liegen.' ); } $result[] = [ 'key' => $key, 'type' => $type, 'channel' => $channel, 'name' => $name, 'active' => (bool) ($row['Aktiv'] ?? false), 'pollInterval' => $pollInterval, ]; } return $result; } /** * @return array{variableType: int, profile: string, writable: bool, position: int} */ public static function definition(string $type, int $channel): array { $type = strtoupper($type); if (!isset(self::CHANNEL_COUNTS[$type]) || $channel < 1 || $channel > self::CHANNEL_COUNTS[$type]) { throw new InvalidArgumentException('Unbekannter CC100-Kanal.'); } $offsets = ['DI' => 100, 'DO' => 200, 'AI' => 300, 'AO' => 400, 'PT' => 500]; return [ 'variableType' => in_array($type, ['DI', 'DO'], true) ? 0 : 2, 'profile' => match ($type) { 'DO' => '~Switch', 'AI', 'AO' => '~Volt', 'PT' => '~Temperature', default => '', }, 'writable' => in_array($type, ['DO', 'AO'], true), 'position' => $offsets[$type] + $channel, ]; } public static function defaultName(string $type, int $channel): string { return match ($type) { 'DI' => 'Digitaler Eingang ' . $channel, 'DO' => 'Digitaler Ausgang ' . $channel, 'AI' => 'Analoger Eingang ' . $channel, 'AO' => 'Analoger Ausgang ' . $channel, 'PT' => 'PT1000 Temperatur ' . $channel, default => $type . $channel, }; } /** * @return array */ public static function parseCalibration(string $content): array { $lines = preg_split('/\R/', trim($content)) ?: []; $lines = array_values(array_filter(array_map('trim', $lines), static fn (string $line): bool => $line !== '')); if (count($lines) < 2) { throw new InvalidArgumentException('Kalibrierdatei ist leer oder unvollstaendig.'); } $headers = preg_split('/\s+/', array_shift($lines)) ?: []; $calibration = []; foreach ($headers as $index => $header) { if (!isset($lines[$index])) { throw new InvalidArgumentException('Kalibrierwert fuer ' . $header . ' fehlt.'); } $key = strtoupper($header); if ($key === 'A01') { $key = 'AO1'; } elseif ($key === 'A02') { $key = 'AO2'; } $values = preg_split('/\s+/', $lines[$index]) ?: []; if (count($values) !== 4 || count(array_filter($values, 'is_numeric')) !== 4) { throw new InvalidArgumentException('Kalibrierwert fuer ' . $key . ' ist ungueltig.'); } [$x1, $y1, $x2, $y2] = array_map('floatval', $values); if ($x1 === $x2 || $y1 === $y2) { throw new InvalidArgumentException('Kalibrierpunkte fuer ' . $key . ' sind nicht eindeutig.'); } $calibration[$key] = compact('x1', 'y1', 'x2', 'y2'); } return $calibration; } /** @param array{x1: float, y1: float, x2: float, y2: float} $calibration */ public static function rawToEngineering(float $raw, array $calibration): float { return (($calibration['y2'] - $calibration['y1']) / ($calibration['x2'] - $calibration['x1'])) * ($raw - $calibration['x1']) + $calibration['y1']; } /** @param array{x1: float, y1: float, x2: float, y2: float} $calibration */ public static function engineeringToRaw(float $value, array $calibration): int { $raw = (($calibration['x2'] - $calibration['x1']) / ($calibration['y2'] - $calibration['y1'])) * ($value - $calibration['y1']) + $calibration['x1']; return (int) round($raw); } public static function pt1000Temperature(float $resistance): float { if (!is_finite($resistance) || $resistance < 760.0 || $resistance > 2300.0) { throw new InvalidArgumentException('PT1000-Widerstand liegt ausserhalb des unterstuetzten Bereichs.'); } $low = -60.0; $high = 350.0; for ($iteration = 0; $iteration < 50; $iteration++) { $middle = ($low + $high) / 2.0; if (self::pt1000Resistance($middle) < $resistance) { $low = $middle; } else { $high = $middle; } } return round(($low + $high) / 2.0, 1); } public static function parseInteger(string $content): int { $value = trim($content); if (!preg_match('/^-?\d+$/', $value)) { throw new RuntimeException('Hardwarewert ist keine ganze Zahl.'); } return (int) $value; } public static function bitState(int $aggregate, int $channel): bool { if ($channel < 1 || $channel > 8) { throw new InvalidArgumentException('Bitkanal muss zwischen 1 und 8 liegen.'); } return ($aggregate & (1 << ($channel - 1))) !== 0; } public static function withBitState(int $aggregate, int $channel, bool $state): int { $mask = 1 << ($channel - 1); return $state ? ($aggregate | $mask) : ($aggregate & ~$mask); } private static function pt1000Resistance(float $temperature): float { $a = 3.9083e-3; $b = -5.775e-7; $c = -4.183e-12; $factor = 1.0 + ($a * $temperature) + ($b * $temperature * $temperature); if ($temperature < 0.0) { $factor += $c * ($temperature - 100.0) * $temperature * $temperature * $temperature; } return 1000.0 * $factor; } }