169 lines
5.9 KiB
PHP
169 lines
5.9 KiB
PHP
<?php
|
||
|
||
namespace App\Http\Controllers;
|
||
|
||
use App\Models\Device;
|
||
use App\Models\Command;
|
||
use App\Models\CommandReceipt;
|
||
use Illuminate\Http\Request;
|
||
use Illuminate\Support\Carbon;
|
||
use Illuminate\Support\Facades\DB;
|
||
|
||
class DeviceApiController extends Controller
|
||
{
|
||
// POST /api/device/{imei}/telemetry
|
||
public function telemetry(Request $req, string $imei)
|
||
{
|
||
$data = $req->validate([
|
||
'recorded_at' => ['nullable','date'],
|
||
'lat' => ['required','numeric','between:-90,90'],
|
||
'lng' => ['required','numeric','between:-180,180'],
|
||
'altitude_m' => ['nullable','numeric'],
|
||
'speed_kmh' => ['nullable','numeric'],
|
||
'heading_deg' => ['nullable','numeric'],
|
||
'accuracy_m' => ['nullable','numeric'],
|
||
'battery_percent'=> ['nullable','integer','between:0,100'],
|
||
'is_car_on' => ['nullable','boolean'],
|
||
'raw' => ['nullable'],
|
||
]);
|
||
|
||
$device = Device::firstOrCreate(
|
||
['imei' => $imei],
|
||
['name' => $imei, 'is_active' => true]
|
||
);
|
||
|
||
$now = now();
|
||
$recordedAt = isset($data['recorded_at'])
|
||
? Carbon::parse($data['recorded_at'])
|
||
: $now;
|
||
|
||
DB::table('device_telemetry')->insert([
|
||
'device_id' => $device->id,
|
||
'recorded_at' => $recordedAt,
|
||
'lat' => $data['lat'],
|
||
'lng' => $data['lng'],
|
||
'altitude_m' => $data['altitude_m'] ?? null,
|
||
'speed_kmh' => $data['speed_kmh'] ?? null,
|
||
'heading_deg' => $data['heading_deg'] ?? null,
|
||
'accuracy_m' => $data['accuracy_m'] ?? null,
|
||
'raw' => array_key_exists('raw', $data) ? json_encode($data['raw']) : null,
|
||
'created_at' => $now,
|
||
]);
|
||
|
||
// Upsert status snapshot
|
||
DB::table('device_status')->updateOrInsert(
|
||
['device_id' => $device->id],
|
||
[
|
||
'is_car_on' => (bool)($data['is_car_on'] ?? false),
|
||
'battery_percent' => $data['battery_percent'] ?? null,
|
||
'external_power' => false,
|
||
'reported_at' => $now,
|
||
'last_gps_fix_at' => $recordedAt,
|
||
'updated_at' => $now,
|
||
'sleep_interval_sec'=> DB::raw('COALESCE(sleep_interval_sec, 60)'),
|
||
]
|
||
);
|
||
|
||
$device->forceFill(['last_seen_at' => $now, 'last_ip' => $req->ip()])->save();
|
||
|
||
return response()->json(['ok' => true], 201);
|
||
}
|
||
|
||
// GET /api/device/{imei}/commands?since=ISO8601
|
||
public function commands(Request $req, string $imei)
|
||
{
|
||
$since = $req->query('since');
|
||
|
||
$device = Device::where('imei', $imei)->first();
|
||
if (!$device) {
|
||
return response()->json(['commands' => []]);
|
||
}
|
||
|
||
$q = Command::where('device_id', $device->id)
|
||
->where('status', 'queued')
|
||
->where(function ($q) {
|
||
$q->whereNull('not_before_at')->orWhere('not_before_at', '<=', now());
|
||
})
|
||
->where(function ($q) {
|
||
$q->whereNull('expires_at')->orWhere('expires_at', '>', now());
|
||
})
|
||
->orderBy('priority', 'asc')
|
||
->orderBy('id', 'asc');
|
||
|
||
if ($since) {
|
||
$ts = Carbon::parse($since);
|
||
$q->where('created_at', '>=', $ts);
|
||
}
|
||
|
||
$cmds = $q->limit(50)->get(['id','command_type','payload','priority','created_at']);
|
||
|
||
// Mark as sent (optional)
|
||
if ($cmds->count()) {
|
||
Command::whereIn('id', $cmds->pluck('id'))->update(['status' => 'sent', 'updated_at' => now()]);
|
||
}
|
||
|
||
$payload = $cmds->map(function ($c) {
|
||
return [
|
||
'id' => $c->id,
|
||
'type' => $c->command_type, // rename field to "type"
|
||
'payload' => $c->payload ?? new \stdClass(),
|
||
'priority' => $c->priority,
|
||
'created_at' => $c->created_at,
|
||
];
|
||
});
|
||
|
||
// Mark as sent (optional – your current behavior)
|
||
if ($cmds->count()) {
|
||
\App\Models\Command::whereIn('id', $cmds->pluck('id'))
|
||
->update(['status' => 'sent', 'updated_at' => now()]);
|
||
}
|
||
|
||
$bodyArray = $payload->values()->all(); // a plain array
|
||
$body = json_encode($bodyArray, JSON_UNESCAPED_SLASHES);
|
||
$len = strlen($body);
|
||
|
||
return response($body, 200)
|
||
->header('Content-Type', 'application/json')
|
||
->header('Content-Length', (string)$len)
|
||
->header('Connection', 'keep-alive');
|
||
|
||
|
||
// return response()->json(['commands' => $cmds]);
|
||
}
|
||
|
||
// POST /api/device/{imei}/command-receipts
|
||
public function commandReceipts(Request $req, string $imei)
|
||
{
|
||
$device = Device::where('imei', $imei)->first();
|
||
if (!$device) {
|
||
return response()->json(['ok' => true], 200);
|
||
}
|
||
|
||
$data = $req->validate([
|
||
'command_id' => ['required','integer'],
|
||
'acked_at' => ['nullable','date'],
|
||
'executed_at' => ['nullable','date'],
|
||
'result' => ['nullable','in:ok,error'],
|
||
'result_detail'=> ['nullable','string'],
|
||
]);
|
||
|
||
CommandReceipt::create([
|
||
'command_id' => $data['command_id'],
|
||
'device_id' => $device->id,
|
||
'acked_at' => isset($data['acked_at']) ? Carbon::parse($data['acked_at']) : null,
|
||
'executed_at' => isset($data['executed_at']) ? Carbon::parse($data['executed_at']) : null,
|
||
'result' => $data['result'] ?? null,
|
||
'result_detail'=> $data['result_detail'] ?? null,
|
||
]);
|
||
|
||
// If execution OK, mark command acked/acked+executed
|
||
if (!empty($data['result'])) {
|
||
$newStatus = $data['result'] === 'ok' ? 'acked' : 'failed';
|
||
Command::where('id', $data['command_id'])->where('device_id', $device->id)
|
||
->update(['status' => $newStatus, 'updated_at' => now()]);
|
||
}
|
||
|
||
return response()->json(['ok' => true], 200);
|
||
}
|
||
}
|