Add unit tests for department lane dynamic image overrides and introduce classes for self-serve signal and virtual hardware management
- Add `DepartmentLaneDynamicImageRouteTest` to verify dynamic image preview handling for studio lanes. - Introduce `selfserve_machine_signal` class to standardize signal normalization, recording, and gateway signal management workflows. - Add `selfserve_virtual_hardware` class to handle virtual hardware configurations, including gateway and binding management. - Enhance structure with auxiliary methods for payload normalization, workspace merging, and validation warnings.
This commit is contained in:
@@ -1016,6 +1016,8 @@ final class TruckwashEdgeAgent
|
||||
private string $controlPlaneStatusPath;
|
||||
private string $stagedUpdatePath;
|
||||
private int $lastHeartbeatAt = 0;
|
||||
private int $lastMachineSignalPollAt = 0;
|
||||
private int $lastMachineSignalMonitorRefreshAt = 0;
|
||||
private string $agentInstanceId;
|
||||
|
||||
public function __construct(string $configPath)
|
||||
@@ -1058,6 +1060,7 @@ final class TruckwashEdgeAgent
|
||||
$this->flushOutbox();
|
||||
$this->pumpBrokerTransport();
|
||||
$this->heartbeat();
|
||||
$this->pollMachineStartSignals();
|
||||
if ($this->resumePendingOperationCompletion()) {
|
||||
$this->pumpBrokerTransport();
|
||||
continue;
|
||||
@@ -1291,6 +1294,171 @@ final class TruckwashEdgeAgent
|
||||
], JSON_UNESCAPED_SLASHES) . PHP_EOL);
|
||||
}
|
||||
|
||||
private function pollMachineStartSignals(): void
|
||||
{
|
||||
$interval = max(1, (int)$this->config->get('machineSignalPollIntervalSeconds', 2));
|
||||
if ((time() - $this->lastMachineSignalPollAt) < $interval) {
|
||||
return;
|
||||
}
|
||||
$this->lastMachineSignalPollAt = time();
|
||||
|
||||
$gatewayId = (int)$this->config->get('gatewayId');
|
||||
$agentToken = (string)$this->config->get('agentToken');
|
||||
if ($gatewayId <= 0 || trim($agentToken) === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->machineSignalMonitors($gatewayId, $agentToken) as $monitor) {
|
||||
$localIp = trim((string)($monitor['local_ip'] ?? ''));
|
||||
if ($localIp === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$component = strtolower(trim((string)($monitor['component'] ?? 'input'))) === 'switch' ? 'switch' : 'input';
|
||||
$channel = (int)($monitor['channel'] ?? 0);
|
||||
$status = $this->machineSignalMonitorStatus($localIp, $channel, $component);
|
||||
$on = $this->machineSignalOnState($status, $component);
|
||||
if ($on === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$stateKey = sprintf(
|
||||
'selfserve_machine_signal:%s:%s:%d',
|
||||
preg_replace('/[^A-Za-z0-9_\-:.]+/', '_', (string)($monitor['relay_id'] ?? 'relay')),
|
||||
$component,
|
||||
$channel
|
||||
);
|
||||
$previous = $this->stateStore->getJson($stateKey, null);
|
||||
$previousOn = is_array($previous) && array_key_exists('on', $previous) ? (bool)$previous['on'] : false;
|
||||
|
||||
if ($on && !$previousOn) {
|
||||
$event = $component === 'switch' ? 'switch.on' : 'input.toggle_on';
|
||||
$payload = [
|
||||
'agent_token' => $agentToken,
|
||||
'agent_instance_id' => $this->agentInstanceId,
|
||||
'lane_id' => (int)($monitor['lane_id'] ?? 0),
|
||||
'relay_id' => (string)($monitor['relay_id'] ?? ''),
|
||||
'device_id' => (string)($monitor['device_id'] ?? ''),
|
||||
'component' => $component,
|
||||
'channel' => $channel,
|
||||
'event' => $event,
|
||||
'source' => 'edge_gateway_poll',
|
||||
'status' => $status,
|
||||
];
|
||||
$payload[$component === 'switch' ? 'output' : 'state'] = true;
|
||||
|
||||
$this->sendControlPlaneEvent(
|
||||
'/edge-agent/gateways/' . $gatewayId . '/selfserve/machine-signal',
|
||||
$payload,
|
||||
'machine_signal'
|
||||
);
|
||||
}
|
||||
|
||||
$this->stateStore->setJson($stateKey, [
|
||||
'on' => $on,
|
||||
'component' => $component,
|
||||
'channel' => $channel,
|
||||
'relay_id' => (string)($monitor['relay_id'] ?? ''),
|
||||
'updated_at' => date('c'),
|
||||
]);
|
||||
} catch (Throwable $throwable) {
|
||||
$this->logger->warning('Machine start signal poll failed: ' . $throwable->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
private function machineSignalMonitors(int $gatewayId, string $agentToken): array
|
||||
{
|
||||
$cache = $this->stateStore->getJson('selfserve_machine_signal_monitors', []);
|
||||
if (
|
||||
is_array($cache)
|
||||
&& isset($cache['monitors'], $cache['refreshed_at'])
|
||||
&& is_array($cache['monitors'])
|
||||
&& (time() - (int)$cache['refreshed_at']) < 60
|
||||
) {
|
||||
return array_values(array_filter($cache['monitors'], 'is_array'));
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->http->post('/edge-agent/gateways/' . $gatewayId . '/selfserve/machine-signal-bindings', [
|
||||
'agent_token' => $agentToken,
|
||||
'agent_instance_id' => $this->agentInstanceId,
|
||||
], 10);
|
||||
$payload = is_array($response['data'] ?? null) ? (array)$response['data'] : (is_array($response) ? $response : []);
|
||||
$monitors = is_array($payload['monitors'] ?? null) ? array_values(array_filter((array)$payload['monitors'], 'is_array')) : [];
|
||||
$this->lastMachineSignalMonitorRefreshAt = time();
|
||||
$this->stateStore->setJson('selfserve_machine_signal_monitors', [
|
||||
'refreshed_at' => $this->lastMachineSignalMonitorRefreshAt,
|
||||
'monitors' => $monitors,
|
||||
]);
|
||||
|
||||
return $monitors;
|
||||
} catch (Throwable $throwable) {
|
||||
if (is_array($cache) && isset($cache['monitors']) && is_array($cache['monitors'])) {
|
||||
return array_values(array_filter($cache['monitors'], 'is_array'));
|
||||
}
|
||||
$this->logger->warning('Machine start signal monitor refresh failed: ' . $throwable->getMessage());
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private function machineSignalMonitorStatus(string $localIp, int $channel, string $component): array
|
||||
{
|
||||
if ($component === 'input') {
|
||||
try {
|
||||
$input = $this->workerHttp->post('/relay/input-status', [
|
||||
'local_ip' => $localIp,
|
||||
'channel' => $channel,
|
||||
], 5) ?? [];
|
||||
return [
|
||||
'input_state' => $input['state'] ?? null,
|
||||
'input' => $input,
|
||||
];
|
||||
} catch (Throwable) {
|
||||
// Fall back to the combined status endpoint below.
|
||||
}
|
||||
}
|
||||
|
||||
return $this->workerHttp->post('/relay/status', [
|
||||
'local_ip' => $localIp,
|
||||
'channel' => $channel,
|
||||
'include_input' => $component === 'input',
|
||||
], 8) ?? [];
|
||||
}
|
||||
|
||||
private function machineSignalOnState(array $status, string $component): ?bool
|
||||
{
|
||||
if ($component === 'switch') {
|
||||
if (array_key_exists('output', $status)) {
|
||||
return (bool)$status['output'];
|
||||
}
|
||||
if (array_key_exists('on', $status)) {
|
||||
return (bool)$status['on'];
|
||||
}
|
||||
if (isset($status['raw']) && is_array($status['raw']) && array_key_exists('output', $status['raw'])) {
|
||||
return (bool)$status['raw']['output'];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (array_key_exists('input_state', $status)) {
|
||||
return $status['input_state'] === null ? null : (bool)$status['input_state'];
|
||||
}
|
||||
if (isset($status['input']) && is_array($status['input']) && array_key_exists('state', $status['input'])) {
|
||||
return (bool)$status['input']['state'];
|
||||
}
|
||||
if (array_key_exists('state', $status)) {
|
||||
return (bool)$status['state'];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function processCommandQueue(): void
|
||||
{
|
||||
$gatewayId = (int)$this->config->get('gatewayId');
|
||||
|
||||
Reference in New Issue
Block a user