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()]); } 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); } }