278 lines
9.3 KiB
PHP
278 lines
9.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Belevo\EnelixUtils\CC100;
|
|
|
|
use InvalidArgumentException;
|
|
use RuntimeException;
|
|
|
|
final class CC100ChannelModel
|
|
{
|
|
/** @var array<string, int> */
|
|
private const CHANNEL_COUNTS = [
|
|
'DI' => 8,
|
|
'DO' => 4,
|
|
'AI' => 2,
|
|
'AO' => 2,
|
|
'PT' => 2,
|
|
];
|
|
|
|
/** @var array<string, int> */
|
|
private const DEFAULT_INTERVALS = [
|
|
'DI' => 500,
|
|
'DO' => 500,
|
|
'AI' => 5000,
|
|
'AO' => 5000,
|
|
'PT' => 5000,
|
|
];
|
|
|
|
/**
|
|
* @return list<array{Aktiv: bool, Typ: string, Kanal: int, Anzeigename: string, Abfrageintervall: int}>
|
|
*/
|
|
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<array{
|
|
* key: string,
|
|
* type: string,
|
|
* channel: int,
|
|
* name: string,
|
|
* active: bool,
|
|
* pollInterval: int
|
|
* }>
|
|
*/
|
|
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<string, array{x1: float, y1: float, x2: float, y2: float}>
|
|
*/
|
|
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;
|
|
}
|
|
}
|