projectTA/app/Http/Controllers/ESP32CamController.php

697 lines
26 KiB
PHP

<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Http;
class ESP32CamController extends Controller
{
/**
* Upload foto dari ESP32-CAM
*/
public function upload(Request $request)
{
try {
// Direktori untuk menyimpan gambar
$uploadDir = 'esp32cam/';
// Pastikan direktori ada di storage/app/public
if (!Storage::disk('public')->exists($uploadDir)) {
Storage::disk('public')->makeDirectory($uploadDir);
}
// Buat nama file unik dengan timestamp
$filename = 'esp32cam_' . Carbon::now()->format('Ymd_His') . '.jpg';
$fullPath = $uploadDir . $filename;
// Ambil konten gambar dari request
$imageContent = $request->getContent();
if (!empty($imageContent)) {
// Simpan gambar menggunakan Laravel Storage
if (Storage::disk('public')->put($fullPath, $imageContent)) {
// Mengatur waktu file agar sesuai dengan waktu pengambilan saat ini
$now = time();
$filePath = storage_path('app/public/' . $fullPath);
@touch($filePath, $now);
// Log informasi upload menggunakan Laravel Logger
Log::info("ESP32-CAM image uploaded successfully", [
'filename' => $filename,
'path' => $fullPath,
'size' => strlen($imageContent) . ' bytes',
'time' => Carbon::now()->format('Y-m-d H:i:s')
]);
return response()->json([
'status' => 'success',
'message' => 'Gambar tersimpan sebagai ' . $filename,
'path' => $fullPath,
'time' => Carbon::now()->format('Y-m-d H:i:s')
]);
}
throw new \Exception('Gagal menyimpan gambar');
}
throw new \Exception('Tidak ada data yang diterima');
} catch (\Exception $e) {
Log::error("ESP32-CAM upload error: " . $e->getMessage());
return response()->json([
'status' => 'error',
'message' => $e->getMessage()
], 500);
}
}
/**
* Menampilkan semua foto
*/
public function all(Request $request)
{
$uploadDir = 'esp32cam/';
$files = Storage::disk('public')->files($uploadDir);
// Urutkan file berdasarkan waktu modifikasi (terbaru lebih dulu)
usort($files, function($a, $b) {
return Storage::disk('public')->lastModified($b) - Storage::disk('public')->lastModified($a);
});
$photos = [];
foreach ($files as $file) {
$filePath = storage_path('app/public/' . $file);
$fileModTime = Storage::disk('public')->lastModified($file);
$fileName = basename($file);
$fileSize = round(Storage::disk('public')->size($file) / 1024, 2) . ' KB';
// Mendapatkan timestamp dari nama file (format nama file: esp32cam_YmdHis)
$timestampFromFilename = null;
if (preg_match('/esp32cam_(\d{8})_(\d{6})/', $fileName, $matches)) {
$dateStr = $matches[1];
$timeStr = $matches[2];
$timestampFromFilename = strtotime(
substr($dateStr, 0, 4) . '-' .
substr($dateStr, 4, 2) . '-' .
substr($dateStr, 6, 2) . ' ' .
substr($timeStr, 0, 2) . ':' .
substr($timeStr, 2, 2) . ':' .
substr($timeStr, 4, 2)
);
}
// Menggunakan timestamp dari nama file jika tersedia, jika tidak gunakan waktu modifikasi
$captureTimestamp = $timestampFromFilename ?: $fileModTime;
$captureTime = date('Y-m-d H:i:s', $captureTimestamp);
// Mendapatkan timestamp dari EXIF data jika tersedia
if (function_exists('exif_read_data')) {
try {
$exifData = @exif_read_data($filePath);
if ($exifData && isset($exifData['DateTimeOriginal'])) {
// Format EXIF datetime: YYYY:MM:DD HH:MM:SS
$exifTime = strtotime($exifData['DateTimeOriginal']);
if ($exifTime) {
$captureTimestamp = $exifTime;
$captureTime = date('Y-m-d H:i:s', $exifTime);
}
}
} catch (\Exception $e) {
Log::warning("Error reading EXIF data: " . $e->getMessage());
}
}
// Format untuk tampilan
$displayTime = date('d-m-Y H:i:s', $captureTimestamp);
$photoItem = [
'name' => $fileName,
'path' => 'storage/' . $file,
'time' => $displayTime,
'size' => $fileSize
];
$photos[] = $photoItem;
}
return view('photos', ['photos' => $photos]);
}
/**
* Mengambil foto terbaru
*/
public function latest()
{
$uploadDir = 'esp32cam/';
$files = Storage::disk('public')->files($uploadDir);
if (count($files) > 0) {
// Urutkan file berdasarkan waktu modifikasi (terbaru lebih dulu)
usort($files, function($a, $b) {
return Storage::disk('public')->lastModified($b) - Storage::disk('public')->lastModified($a);
});
$latestPhoto = $files[0];
// Get file path for EXIF reading
$filePath = storage_path('app/public/' . $latestPhoto);
$fileModTime = Storage::disk('public')->lastModified($latestPhoto);
$fileSize = round(Storage::disk('public')->size($latestPhoto) / 1024, 2) . ' KB';
$fileName = basename($latestPhoto);
// Mendapatkan timestamp dari nama file (format nama file: esp32cam_YmdHis)
$timestampFromFilename = null;
if (preg_match('/esp32cam_(\d{8})_(\d{6})/', $fileName, $matches)) {
$dateStr = $matches[1];
$timeStr = $matches[2];
$timestampFromFilename = strtotime(
substr($dateStr, 0, 4) . '-' .
substr($dateStr, 4, 2) . '-' .
substr($dateStr, 6, 2) . ' ' .
substr($timeStr, 0, 2) . ':' .
substr($timeStr, 2, 2) . ':' .
substr($timeStr, 4, 2)
);
}
// Menggunakan timestamp dari nama file jika tersedia, jika tidak gunakan waktu modifikasi
$captureTimestamp = $timestampFromFilename ?: $fileModTime;
$captureTime = date('Y-m-d H:i:s', $captureTimestamp);
// Get timestamp from EXIF data if available
if (function_exists('exif_read_data')) {
try {
$exifData = @exif_read_data($filePath);
if ($exifData && isset($exifData['DateTimeOriginal'])) {
// Format EXIF datetime: YYYY:MM:DD HH:MM:SS
$exifTime = strtotime($exifData['DateTimeOriginal']);
if ($exifTime) {
$captureTimestamp = $exifTime;
$captureTime = date('Y-m-d H:i:s', $exifTime);
}
}
} catch (\Exception $e) {
Log::warning("Error reading EXIF data: " . $e->getMessage());
}
}
// Format untuk tampilan
$displayTime = date('d-m-Y H:i:s', $captureTimestamp);
return response()->json([
'status' => 'success',
'photo' => [
'name' => $fileName,
'path' => 'storage/' . $latestPhoto,
'time' => $displayTime,
'timestamp' => $captureTimestamp,
'size' => $fileSize
]
]);
}
return response()->json([
'status' => 'error',
'message' => 'Tidak ada foto yang tersedia'
], 404);
}
/**
* Hapus foto tertentu
*/
public function delete(Request $request)
{
$filename = $request->input('filename');
$uploadDir = 'esp32cam/';
if (Storage::disk('public')->exists($uploadDir . $filename)) {
Storage::disk('public')->delete($uploadDir . $filename);
return response()->json([
'status' => 'success',
'message' => 'Foto berhasil dihapus'
]);
}
return response()->json([
'status' => 'error',
'message' => 'Foto tidak ditemukan'
], 404);
}
/**
* Membersihkan foto lama
*/
public function cleanup()
{
try {
$uploadDir = 'esp32cam/';
$files = Storage::disk('public')->files($uploadDir);
$deletedCount = 0;
// Hapus file yang lebih lama dari 7 hari, tapi simpan minimal 10 file terakhir
if (count($files) > 10) {
// Urutkan file berdasarkan waktu modifikasi (terlama lebih dulu)
usort($files, function($a, $b) {
return Storage::disk('public')->lastModified($a) - Storage::disk('public')->lastModified($b);
});
// Ambil file lama untuk dihapus (tapi sisakan 10 file terakhir)
$filesToDelete = array_slice($files, 0, count($files) - 10);
foreach ($filesToDelete as $file) {
$fileTime = Storage::disk('public')->lastModified($file);
if (time() - $fileTime > 7 * 24 * 60 * 60) { // 7 hari dalam detik
Storage::disk('public')->delete($file);
Log::info("Deleted old ESP32-CAM image: " . $file);
$deletedCount++;
}
}
}
return response()->json([
'status' => 'success',
'message' => 'Cleanup completed. Deleted ' . $deletedCount . ' old files.',
'remaining_files' => count($files) - $deletedCount
]);
} catch (\Exception $e) {
Log::error("ESP32-CAM cleanup error: " . $e->getMessage());
return response()->json([
'status' => 'error',
'message' => $e->getMessage()
], 500);
}
}
/**
* Fetch image from ESP32-CAM web server and save it
*
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function fetchFromCamera(Request $request)
{
try {
// Debug untuk melihat isi request
Log::info('ESP32CAM Debug - Request Content:', [
'all' => $request->all(),
'content' => $request->getContent(),
'content_decoded' => json_decode($request->getContent(), true),
'headers' => $request->header()
]);
// Gunakan IP kamera dari request atau default
$cameraIp = '192.168.1.19'; // Default IP
// Coba ambil dari request JSON
$requestData = json_decode($request->getContent(), true);
if (isset($requestData['camera_ip']) && !empty($requestData['camera_ip'])) {
$cameraIp = $requestData['camera_ip'];
}
Log::info('ESP32CAM Debug - Camera IP:', [
'camera_ip' => $cameraIp,
'is_empty' => empty($cameraIp)
]);
if (empty($cameraIp)) {
return response()->json([
'status' => 'error',
'message' => 'IP address ESP32-CAM diperlukan'
], 400);
}
// URL untuk mengambil gambar dari ESP32-CAM
$captureUrl = "http://{$cameraIp}/capture";
Log::info('ESP32CAM Capture URL: ' . $captureUrl);
// Ambil gambar dari ESP32-CAM dengan timeout yang lebih lama
$response = Http::timeout(30)->get($captureUrl);
if ($response->successful()) {
// Direktori untuk menyimpan gambar
$uploadDir = 'esp32cam/';
// Pastikan direktori ada di storage/app/public
if (!Storage::disk('public')->exists($uploadDir)) {
Storage::disk('public')->makeDirectory($uploadDir);
}
// Buat nama file unik dengan timestamp
$filename = 'esp32cam_' . Carbon::now()->format('Ymd_His') . '.jpg';
$fullPath = $uploadDir . $filename;
// Simpan gambar menggunakan Laravel Storage
Storage::disk('public')->put($fullPath, $response->body());
// Mengatur waktu file agar sesuai dengan waktu pengambilan saat ini
$now = time();
$filePath = storage_path('app/public/' . $fullPath);
@touch($filePath, $now);
// Log informasi
Log::info("ESP32-CAM image fetched successfully", [
'camera_ip' => $cameraIp,
'filename' => $filename,
'path' => $fullPath,
'size' => strlen($response->body()) . ' bytes',
'time' => Carbon::now()->format('Y-m-d H:i:s')
]);
return response()->json([
'status' => 'success',
'message' => 'Gambar berhasil diambil dari ESP32-CAM',
'filename' => $filename,
'path' => $fullPath,
'time' => Carbon::now()->format('Y-m-d H:i:s')
]);
}
Log::error('ESP32-CAM fetch error: Failed with status ' . $response->status());
return response()->json([
'status' => 'error',
'message' => 'Gagal mengambil gambar dari ESP32-CAM. Status: ' . $response->status(),
'camera_ip' => $cameraIp
], 500);
} catch (\Exception $e) {
Log::error("ESP32-CAM fetch error: " . $e->getMessage(), [
'camera_ip' => $cameraIp ?? '192.168.1.19',
'exception' => $e
]);
return response()->json([
'status' => 'error',
'message' => 'Gagal mengambil gambar: ' . $e->getMessage(),
'camera_ip' => $cameraIp ?? '192.168.1.19'
], 500);
}
}
/**
* Proxy untuk endpoint status dari ESP32-CAM
*
* @param Request $request
* @return \Illuminate\Http\Response
*/
public function proxyStatus(Request $request)
{
try {
$cameraIp = $request->query('ip');
if (empty($cameraIp)) {
return response()->json([
'status' => 'error',
'message' => 'IP address ESP32-CAM diperlukan'
], 400);
}
// Periksa apakah URL sudah mengandung http:// atau https://
if (!preg_match('/^https?:\/\//', $cameraIp)) {
$statusUrl = "http://{$cameraIp}/status";
} else {
// Gunakan URL langsung jika sudah termasuk protokol
$statusUrl = "{$cameraIp}/status";
}
Log::info('ESP32CAM Proxy Status URL: ' . $statusUrl);
$response = Http::timeout(10)->get($statusUrl);
if ($response->successful()) {
return response($response->body(), 200)
->header('Content-Type', 'application/json');
}
return response()->json([
'status' => 'error',
'message' => 'Gagal mengakses status ESP32-CAM. Status: ' . $response->status(),
'camera_ip' => $cameraIp
], 500);
} catch (\Exception $e) {
Log::error("ESP32-CAM proxy status error: " . $e->getMessage());
return response()->json([
'status' => 'error',
'message' => 'Gagal mengakses status: ' . $e->getMessage()
], 500);
}
}
/**
* Proxy untuk stream video dari ESP32-CAM
*
* @param Request $request
* @return \Symfony\Component\HttpFoundation\StreamedResponse
*/
public function proxyStream(Request $request)
{
$cameraIp = $request->query('ip');
if (empty($cameraIp)) {
return response()->json([
'status' => 'error',
'message' => 'IP address ESP32-CAM diperlukan'
], 400);
}
// Periksa apakah URL sudah mengandung http:// atau https://
if (!preg_match('/^https?:\/\//', $cameraIp)) {
$streamUrl = "http://{$cameraIp}/stream";
} else {
// Gunakan URL langsung jika sudah termasuk protokol
$streamUrl = "{$cameraIp}/stream";
}
Log::info('ESP32CAM Proxy Stream URL: ' . $streamUrl);
// Menggunakan streaming response untuk meneruskan MJPEG stream
return response()->stream(function() use ($streamUrl) {
$ch = curl_init($streamUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, false); // Langsung kirim output ke browser
curl_exec($ch);
curl_close($ch);
}, 200, [
'Content-Type' => 'multipart/x-mixed-replace; boundary=frame',
'Cache-Control' => 'no-cache, private',
'Connection' => 'close',
'Pragma' => 'no-cache'
]);
}
/**
* Proxy untuk pengaturan kamera ESP32-CAM
*
* @param Request $request
* @return \Illuminate\Http\Response
*/
public function proxySettings(Request $request)
{
try {
$cameraIp = $request->query('ip');
$resolution = $request->query('resolution');
$quality = $request->query('quality');
if (empty($cameraIp)) {
return response()->json([
'status' => 'error',
'message' => 'IP address ESP32-CAM diperlukan'
], 400);
}
// Periksa apakah URL sudah mengandung http:// atau https://
if (!preg_match('/^https?:\/\//', $cameraIp)) {
$baseUrl = "http://{$cameraIp}";
} else {
// Gunakan URL langsung jika sudah termasuk protokol
$baseUrl = $cameraIp;
}
// Bangun URL dengan parameter yang diperlukan
$settingsUrl = "{$baseUrl}/camera-settings";
$params = [];
if (!empty($resolution)) {
$params['resolution'] = $resolution;
}
if (!empty($quality)) {
$params['quality'] = $quality;
}
if (!empty($params)) {
$settingsUrl .= '?' . http_build_query($params);
}
Log::info('ESP32CAM Proxy Settings URL: ' . $settingsUrl);
$response = Http::timeout(10)->get($settingsUrl);
if ($response->successful()) {
return response($response->body(), 200)
->header('Content-Type', 'application/json');
}
return response()->json([
'status' => 'error',
'message' => 'Gagal mengubah pengaturan ESP32-CAM. Status: ' . $response->status(),
'camera_ip' => $cameraIp
], 500);
} catch (\Exception $e) {
Log::error("ESP32-CAM proxy settings error: " . $e->getMessage());
return response()->json([
'status' => 'error',
'message' => 'Gagal mengubah pengaturan: ' . $e->getMessage()
], 500);
}
}
/**
* Proxy untuk endpoint capture dari ESP32-CAM
*
* @param Request $request
* @return \Illuminate\Http\Response
*/
public function proxyCapture(Request $request)
{
try {
$cameraIp = $request->query('ip');
if (empty($cameraIp)) {
return response()->json([
'status' => 'error',
'message' => 'IP address ESP32-CAM diperlukan'
], 400);
}
// Periksa apakah URL sudah mengandung http:// atau https://
if (!preg_match('/^https?:\/\//', $cameraIp)) {
$captureUrl = "http://{$cameraIp}/capture";
} else {
// Gunakan URL langsung jika sudah termasuk protokol
$captureUrl = "{$cameraIp}/capture";
}
Log::info('ESP32CAM Proxy Capture URL: ' . $captureUrl);
$response = Http::timeout(15)->get($captureUrl);
if ($response->successful()) {
// Jika hanya ingin menampilkan gambar tanpa menyimpan
if ($request->query('display_only') === 'true') {
return response($response->body(), 200)
->header('Content-Type', 'image/jpeg');
}
// Jika ingin menyimpan gambar
$uploadDir = 'esp32cam/';
// Pastikan direktori ada di storage/app/public
if (!Storage::disk('public')->exists($uploadDir)) {
Storage::disk('public')->makeDirectory($uploadDir);
}
// Buat nama file unik dengan timestamp
$filename = 'esp32cam_' . Carbon::now()->format('Ymd_His') . '.jpg';
$fullPath = $uploadDir . $filename;
// Simpan gambar menggunakan Laravel Storage
Storage::disk('public')->put($fullPath, $response->body());
// Mengatur waktu file agar sesuai dengan waktu pengambilan saat ini
$now = time();
$filePath = storage_path('app/public/' . $fullPath);
@touch($filePath, $now);
return response()->json([
'status' => 'success',
'message' => 'Gambar berhasil diambil dan disimpan',
'filename' => $filename,
'path' => $fullPath,
'time' => Carbon::now()->format('Y-m-d H:i:s')
]);
}
return response()->json([
'status' => 'error',
'message' => 'Gagal mengambil gambar dari ESP32-CAM. Status: ' . $response->status(),
'camera_ip' => $cameraIp
], 500);
} catch (\Exception $e) {
Log::error("ESP32-CAM proxy capture error: " . $e->getMessage());
return response()->json([
'status' => 'error',
'message' => 'Gagal mengambil gambar: ' . $e->getMessage()
], 500);
}
}
/**
* Proxy untuk menghentikan streaming dari ESP32-CAM
*
* @param Request $request
* @return \Illuminate\Http\Response
*/
public function proxyStopStream(Request $request)
{
try {
$cameraIp = $request->query('ip');
if (empty($cameraIp)) {
return response()->json([
'status' => 'error',
'message' => 'IP address ESP32-CAM diperlukan'
], 400);
}
// Periksa apakah URL sudah mengandung http:// atau https://
if (!preg_match('/^https?:\/\//', $cameraIp)) {
$stopStreamUrl = "http://{$cameraIp}/stopstream";
} else {
// Gunakan URL langsung jika sudah termasuk protokol
$stopStreamUrl = "{$cameraIp}/stopstream";
}
Log::info('ESP32CAM Proxy Stop Stream URL: ' . $stopStreamUrl);
$response = Http::timeout(5)->get($stopStreamUrl);
if ($response->successful()) {
return response()->json([
'status' => 'success',
'message' => 'Streaming dihentikan'
]);
}
return response()->json([
'status' => 'error',
'message' => 'Gagal menghentikan streaming. Status: ' . $response->status(),
'camera_ip' => $cameraIp
], 500);
} catch (\Exception $e) {
Log::error("ESP32-CAM proxy stop stream error: " . $e->getMessage());
return response()->json([
'status' => 'error',
'message' => 'Gagal menghentikan streaming: ' . $e->getMessage()
], 500);
}
}
}