370 lines
10 KiB
PHP
370 lines
10 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class LoRaMessage extends Model
|
|
{
|
|
protected $fillable = [
|
|
'device_id',
|
|
'node_id',
|
|
'gateway_id',
|
|
'direction',
|
|
'message_type',
|
|
'payload',
|
|
'parsed_data',
|
|
'rssi',
|
|
'snr',
|
|
'spreading_factor',
|
|
'frequency',
|
|
'bandwidth',
|
|
'is_processed',
|
|
'is_acknowledged',
|
|
'status',
|
|
'error_message',
|
|
'metadata',
|
|
'transmitted_at',
|
|
'received_at'
|
|
];
|
|
|
|
protected $casts = [
|
|
'parsed_data' => 'array',
|
|
'metadata' => 'array',
|
|
'is_processed' => 'boolean',
|
|
'is_acknowledged' => 'boolean',
|
|
'transmitted_at' => 'datetime',
|
|
'received_at' => 'datetime',
|
|
'rssi' => 'float',
|
|
'snr' => 'float',
|
|
'frequency' => 'float'
|
|
];
|
|
|
|
public function device(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Device::class);
|
|
}
|
|
|
|
/**
|
|
* Parse LoRa payload berdasarkan message type
|
|
*/
|
|
public function parsePayload(): array
|
|
{
|
|
try {
|
|
switch ($this->message_type) {
|
|
case 'sensor_data':
|
|
return $this->parseSensorData();
|
|
case 'heartbeat':
|
|
return $this->parseHeartbeat();
|
|
case 'command':
|
|
return $this->parseCommand();
|
|
case 'ack':
|
|
return $this->parseAcknowledgment();
|
|
case 'config':
|
|
return $this->parseConfig();
|
|
default:
|
|
return $this->parseGeneric();
|
|
}
|
|
} catch (\Exception $e) {
|
|
Log::error("LoRa payload parsing failed: " . $e->getMessage(), [
|
|
'message_id' => $this->id,
|
|
'payload' => $this->payload
|
|
]);
|
|
return ['error' => $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse sensor data payload
|
|
* Format: "SENSOR|VIBRATION|1.5,2.3,1.8|2.0" atau "SENSOR|PIR|1|85|120|front" atau "SENSOR|DOOR|1|EMP-1234|45|keycard"
|
|
*/
|
|
private function parseSensorData(): array
|
|
{
|
|
$parts = explode('|', $this->payload);
|
|
|
|
if (count($parts) < 3) {
|
|
throw new \Exception('Invalid sensor data format');
|
|
}
|
|
|
|
$sensorType = strtoupper($parts[1]);
|
|
$data = ['sensor_type' => $sensorType];
|
|
|
|
switch ($sensorType) {
|
|
case 'VIBRATION':
|
|
// Format: SENSOR|VIBRATION|x,y,z|threshold
|
|
if (count($parts) >= 4) {
|
|
$axes = explode(',', $parts[2]);
|
|
$data = array_merge($data, [
|
|
'x_axis' => (float)($axes[0] ?? 0),
|
|
'y_axis' => (float)($axes[1] ?? 0),
|
|
'z_axis' => (float)($axes[2] ?? 0),
|
|
'threshold' => (float)($parts[3] ?? 2.0)
|
|
]);
|
|
}
|
|
break;
|
|
|
|
case 'PIR':
|
|
// Format: SENSOR|PIR|detected|intensity|duration|zone
|
|
if (count($parts) >= 6) {
|
|
$data = array_merge($data, [
|
|
'motion_detected' => (bool)$parts[2],
|
|
'motion_intensity' => (int)$parts[3],
|
|
'duration_seconds' => (int)$parts[4],
|
|
'detection_zone' => $parts[5]
|
|
]);
|
|
}
|
|
break;
|
|
|
|
case 'DOOR':
|
|
// Format: SENSOR|DOOR|opened|id_card|duration|method|location
|
|
if (count($parts) >= 7) {
|
|
$data = array_merge($data, [
|
|
'door_opened' => (bool)$parts[2],
|
|
'user_id_card' => $parts[3] !== 'NULL' ? $parts[3] : null,
|
|
'duration_seconds' => (int)$parts[4],
|
|
'access_method' => $parts[5],
|
|
'door_location' => $parts[6]
|
|
]);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
$data['raw_data'] = array_slice($parts, 2);
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Parse heartbeat payload
|
|
* Format: "HEARTBEAT|battery_level|signal_strength|uptime"
|
|
*/
|
|
private function parseHeartbeat(): array
|
|
{
|
|
$parts = explode('|', $this->payload);
|
|
|
|
return [
|
|
'message_type' => 'heartbeat',
|
|
'battery_level' => isset($parts[1]) ? (int)$parts[1] : null,
|
|
'signal_strength' => isset($parts[2]) ? (int)$parts[2] : null,
|
|
'uptime_seconds' => isset($parts[3]) ? (int)$parts[3] : null,
|
|
'timestamp' => now()->toISOString()
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Parse command payload
|
|
* Format: "COMMAND|action|parameters"
|
|
*/
|
|
private function parseCommand(): array
|
|
{
|
|
$parts = explode('|', $this->payload);
|
|
|
|
return [
|
|
'message_type' => 'command',
|
|
'action' => $parts[1] ?? 'unknown',
|
|
'parameters' => isset($parts[2]) ? explode(',', $parts[2]) : [],
|
|
'command_id' => $parts[3] ?? null
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Parse acknowledgment payload
|
|
* Format: "ACK|command_id|status|message"
|
|
*/
|
|
private function parseAcknowledgment(): array
|
|
{
|
|
$parts = explode('|', $this->payload);
|
|
|
|
return [
|
|
'message_type' => 'ack',
|
|
'command_id' => $parts[1] ?? null,
|
|
'ack_status' => $parts[2] ?? 'unknown',
|
|
'ack_message' => $parts[3] ?? null
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Parse config payload
|
|
* Format: "CONFIG|parameter|value"
|
|
*/
|
|
private function parseConfig(): array
|
|
{
|
|
$parts = explode('|', $this->payload);
|
|
|
|
return [
|
|
'message_type' => 'config',
|
|
'parameter' => $parts[1] ?? null,
|
|
'value' => $parts[2] ?? null,
|
|
'config_id' => $parts[3] ?? null
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Parse generic payload
|
|
*/
|
|
private function parseGeneric(): array
|
|
{
|
|
return [
|
|
'message_type' => 'generic',
|
|
'raw_payload' => $this->payload,
|
|
'parts' => explode('|', $this->payload)
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Generate command payload untuk dikirim ke LoRa node
|
|
*/
|
|
public static function generateCommandPayload(string $action, array $parameters = [], string $commandId = null): string
|
|
{
|
|
$commandId = $commandId ?? uniqid('CMD_');
|
|
$paramStr = implode(',', $parameters);
|
|
|
|
return "COMMAND|{$action}|{$paramStr}|{$commandId}";
|
|
}
|
|
|
|
/**
|
|
* Generate config payload untuk konfigurasi node
|
|
*/
|
|
public static function generateConfigPayload(string $parameter, $value, string $configId = null): string
|
|
{
|
|
$configId = $configId ?? uniqid('CFG_');
|
|
|
|
return "CONFIG|{$parameter}|{$value}|{$configId}";
|
|
}
|
|
|
|
/**
|
|
* Cek kualitas sinyal LoRa
|
|
*/
|
|
public function getSignalQuality(): string
|
|
{
|
|
if ($this->rssi === null) {
|
|
return 'unknown';
|
|
}
|
|
|
|
// RSSI thresholds (dBm)
|
|
if ($this->rssi >= -70) {
|
|
return 'excellent';
|
|
} elseif ($this->rssi >= -85) {
|
|
return 'good';
|
|
} elseif ($this->rssi >= -100) {
|
|
return 'fair';
|
|
} else {
|
|
return 'poor';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Estimate jarak berdasarkan RSSI (rough calculation)
|
|
*/
|
|
public function estimateDistance(): ?float
|
|
{
|
|
if ($this->rssi === null) {
|
|
return null;
|
|
}
|
|
|
|
// Simplified path loss calculation
|
|
// Distance (km) = 10^((Tx_Power - RSSI - 32.44 - 20*log10(frequency)) / 20)
|
|
$txPower = 14; // Typical LoRa TX power in dBm
|
|
$frequency = $this->frequency ?? 868.0; // MHz
|
|
|
|
$pathLoss = $txPower - $this->rssi - 32.44 - (20 * log10($frequency));
|
|
$distance = pow(10, $pathLoss / 20);
|
|
|
|
return round($distance, 2);
|
|
}
|
|
|
|
/**
|
|
* Scope untuk inbound messages
|
|
*/
|
|
public function scopeInbound($query)
|
|
{
|
|
return $query->where('direction', 'inbound');
|
|
}
|
|
|
|
/**
|
|
* Scope untuk outbound messages
|
|
*/
|
|
public function scopeOutbound($query)
|
|
{
|
|
return $query->where('direction', 'outbound');
|
|
}
|
|
|
|
/**
|
|
* Scope untuk unprocessed messages
|
|
*/
|
|
public function scopeUnprocessed($query)
|
|
{
|
|
return $query->where('is_processed', false);
|
|
}
|
|
|
|
/**
|
|
* Scope untuk message type tertentu
|
|
*/
|
|
public function scopeMessageType($query, $type)
|
|
{
|
|
return $query->where('message_type', $type);
|
|
}
|
|
|
|
/**
|
|
* Scope untuk node tertentu
|
|
*/
|
|
public function scopeFromNode($query, $nodeId)
|
|
{
|
|
return $query->where('node_id', $nodeId);
|
|
}
|
|
|
|
/**
|
|
* Scope untuk gateway tertentu
|
|
*/
|
|
public function scopeFromGateway($query, $gatewayId)
|
|
{
|
|
return $query->where('gateway_id', $gatewayId);
|
|
}
|
|
|
|
/**
|
|
* Mark message as processed
|
|
*/
|
|
public function markAsProcessed(bool $success = true, string $errorMessage = null): void
|
|
{
|
|
$this->update([
|
|
'is_processed' => true,
|
|
'status' => $success ? 'processed' : 'failed',
|
|
'error_message' => $errorMessage
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Mark outbound message as acknowledged
|
|
*/
|
|
public function markAsAcknowledged(): void
|
|
{
|
|
$this->update([
|
|
'is_acknowledged' => true,
|
|
'status' => 'acknowledged'
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get message summary untuk dashboard
|
|
*/
|
|
public function getMessageSummary(): array
|
|
{
|
|
return [
|
|
'id' => $this->id,
|
|
'node_id' => $this->node_id,
|
|
'direction' => $this->direction,
|
|
'message_type' => $this->message_type,
|
|
'signal_quality' => $this->getSignalQuality(),
|
|
'estimated_distance' => $this->estimateDistance(),
|
|
'is_processed' => $this->is_processed,
|
|
'status' => $this->status,
|
|
'received_at' => $this->received_at?->format('Y-m-d H:i:s'),
|
|
'transmitted_at' => $this->transmitted_at?->format('Y-m-d H:i:s')
|
|
];
|
|
}
|
|
}
|