diff --git a/app/Jobs/GenerateScreenJob.php b/app/Jobs/GenerateScreenJob.php index 61e1305..b9661cc 100644 --- a/app/Jobs/GenerateScreenJob.php +++ b/app/Jobs/GenerateScreenJob.php @@ -29,10 +29,9 @@ class GenerateScreenJob implements ShouldQueue */ public function handle(): void { - $newImageUuid = ImageGenerationService::generateImage($this->markup); + $newImageUuid = ImageGenerationService::generateImage($this->markup, $this->deviceId); Device::find($this->deviceId)->update(['current_screen_image' => $newImageUuid]); - \Log::info("Device $this->deviceId: updated with new image: $newImageUuid"); if ($this->pluginId) { // cache current image diff --git a/app/Models/Device.php b/app/Models/Device.php index 065793b..f7df91e 100644 --- a/app/Models/Device.php +++ b/app/Models/Device.php @@ -17,6 +17,9 @@ class Device extends Model 'proxy_cloud' => 'boolean', 'last_log_request' => 'json', 'proxy_cloud_response' => 'json', + 'width' => 'integer', + 'height' => 'integer', + 'rotate' => 'integer', ]; public function getBatteryPercentAttribute() diff --git a/app/Models/User.php b/app/Models/User.php index 5528c2c..ffe8c97 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -25,7 +25,7 @@ class User extends Authenticatable // implements MustVerifyEmail 'email', 'password', 'assign_new_devices', - 'assign_new_device_id' + 'assign_new_device_id', ]; /** diff --git a/app/Services/ImageGenerationService.php b/app/Services/ImageGenerationService.php index a9e1b3b..faa90ba 100644 --- a/app/Services/ImageGenerationService.php +++ b/app/Services/ImageGenerationService.php @@ -5,13 +5,16 @@ namespace App\Services; use App\Models\Device; use App\Models\Plugin; use Illuminate\Support\Facades\Storage; +use ImagickPixel; use Ramsey\Uuid\Uuid; use Spatie\Browsershot\Browsershot; use Wnx\SidecarBrowsershot\BrowsershotLambda; class ImageGenerationService { - public static function generateImage(string $markup): string { + public static function generateImage(string $markup, $deviceId): string + { + $device = Device::find($deviceId); $uuid = Uuid::uuid4()->toString(); $pngPath = Storage::disk('public')->path('/images/generated/'.$uuid.'.png'); $bmpPath = Storage::disk('public')->path('/images/generated/'.$uuid.'.bmp'); @@ -20,7 +23,7 @@ class ImageGenerationService if (config('app.puppeteer_mode') === 'sidecar-aws') { try { BrowsershotLambda::html($markup) - ->windowSize($device->width ?? 800, $device->height ?? 480) + ->windowSize(800, 480) ->save($pngPath); } catch (\Exception $e) { throw new \RuntimeException('Failed to generate PNG: '.$e->getMessage(), 0, $e); @@ -29,18 +32,30 @@ class ImageGenerationService try { Browsershot::html($markup) ->setOption('args', config('app.puppeteer_docker') ? ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu'] : []) - ->windowSize($device->width ?? 800, $device->height ?? 480) + ->windowSize(800, 480) ->save($pngPath); } catch (\Exception $e) { throw new \RuntimeException('Failed to generate PNG: '.$e->getMessage(), 0, $e); } } - try { - ImageGenerationService::convertToBmpImageMagick($pngPath, $bmpPath); - } catch (\ImagickException $e) { - throw new \RuntimeException('Failed to convert image to BMP: '.$e->getMessage(), 0, $e); + if (isset($device->last_firmware_version) + && version_compare($device->last_firmware_version, '1.5.2', '<')) { + try { + ImageGenerationService::convertToBmpImageMagick($pngPath, $bmpPath); + } catch (\ImagickException $e) { + throw new \RuntimeException('Failed to convert image to BMP: '.$e->getMessage(), 0, $e); + } + } else { + try { + ImageGenerationService::convertToPngImageMagick($pngPath, $device->width, $device->height, $device->rotate); + } catch (\ImagickException $e) { + throw new \RuntimeException('Failed to convert image to PNG: '.$e->getMessage(), 0, $e); + } } + $device->update(['current_screen_image' => $uuid]); + \Log::info("Device $device->id: updated with new image: $uuid"); + return $uuid; } @@ -59,6 +74,28 @@ class ImageGenerationService $imagick->clear(); } + /** + * @throws \ImagickException + */ + private static function convertToPngImageMagick(string $pngPath, ?int $width, ?int $height, ?int $rotate): void + { + $imagick = new \Imagick($pngPath); + if ($width !== 800 || $height !== 480) { + $imagick->resizeImage($width, $height, \Imagick::FILTER_LANCZOS, 1, true); + } + if ($rotate !== null && $rotate !== 0) { + $imagick->rotateImage(new ImagickPixel('black'), $rotate); + } + $imagick->setImageType(\Imagick::IMGTYPE_GRAYSCALE); + $imagick->quantizeImage(2, \Imagick::COLORSPACE_GRAY, 0, true, false); + $imagick->setImageDepth(8); + $imagick->stripImage(); + + $imagick->setFormat('png'); + $imagick->writeImage($pngPath); + $imagick->clear(); + } + public static function cleanupFolder(): void { $activeDeviceImageUuids = Device::pluck('current_screen_image')->filter()->toArray(); diff --git a/config/app.php b/config/app.php index e5b3078..444d0ac 100644 --- a/config/app.php +++ b/config/app.php @@ -130,7 +130,7 @@ return [ 'force_https' => env('FORCE_HTTPS', false), 'puppeteer_docker' => env('PUPPETEER_DOCKER', false), 'puppeteer_mode' => env('PUPPETEER_MODE', 'local'), - + /* |-------------------------------------------------------------------------- | Application Version diff --git a/database/migrations/2025_05_10_202133_add_rotate_to_devices_table.php b/database/migrations/2025_05_10_202133_add_rotate_to_devices_table.php new file mode 100644 index 0000000..e439b1b --- /dev/null +++ b/database/migrations/2025_05_10_202133_add_rotate_to_devices_table.php @@ -0,0 +1,22 @@ +integer('rotate')->nullable()->default(0)->after('width'); + }); + } + + public function down(): void + { + Schema::table('devices', function (Blueprint $table) { + $table->dropColumn('rotate'); + }); + } +}; diff --git a/resources/views/livewire/device-dashboard.blade.php b/resources/views/livewire/device-dashboard.blade.php index f7720c0..e21428d 100644 --- a/resources/views/livewire/device-dashboard.blade.php +++ b/resources/views/livewire/device-dashboard.blade.php @@ -59,7 +59,7 @@ new class extends Component { @elseif($current_image_path) - Current Image + Current Image @endif diff --git a/resources/views/livewire/devices/configure.blade.php b/resources/views/livewire/devices/configure.blade.php index 05573cc..ffd2ebc 100644 --- a/resources/views/livewire/devices/configure.blade.php +++ b/resources/views/livewire/devices/configure.blade.php @@ -15,6 +15,7 @@ new class extends Component { public $default_refresh_interval; public $width; public $height; + public $rotate; // Playlist properties public $playlists; @@ -39,6 +40,7 @@ new class extends Component { $this->default_refresh_interval = $device->default_refresh_interval; $this->width = $device->width; $this->height = $device->height; + $this->rotate = $device->rotate; $this->playlists = $device->playlists()->with('items.plugin')->orderBy('created_at')->get(); return view('livewire.devices.configure', [ @@ -65,6 +67,7 @@ new class extends Component { 'default_refresh_interval' => 'required|integer|min:1', 'width' => 'required|integer|min:1', 'height' => 'required|integer|min:1', + 'rotate' => 'required|integer|min:0|max:359', ]); $this->device->update([ @@ -74,6 +77,7 @@ new class extends Component { 'default_refresh_interval' => $this->default_refresh_interval, 'width' => $this->width, 'height' => $this->height, + 'rotate' => $this->rotate, ]); Flux::modal('edit-device')->close(); @@ -215,7 +219,7 @@ new class extends Component {
-
+
@php $current_image_uuid =$device->current_screen_image; if($current_image_uuid) { @@ -226,7 +230,7 @@ new class extends Component { } @endphp -
+

{{ $device->name }}

@@ -282,6 +286,7 @@ new class extends Component {
+
@@ -315,7 +320,7 @@ new class extends Component { @if(!$device->mirror_device_id) @if($current_image_path) - Next Image + Next Image @endif diff --git a/routes/api.php b/routes/api.php index 8b909de..2f23008 100644 --- a/routes/api.php +++ b/routes/api.php @@ -82,9 +82,11 @@ Route::get('/display', function (Request $request) { $image_path = 'images/setup-logo.bmp'; $filename = 'setup-logo.bmp'; } else { - if (file_exists(storage_path('app/public/images/generated/'.$image_uuid.'.bmp'))) { + if (isset($device->last_firmware_version) + && version_compare($device->last_firmware_version, '1.5.2', '<') + && Storage::disk('public')->exists('images/generated/'.$image_uuid.'.bmp')) { $image_path = 'images/generated/'.$image_uuid.'.bmp'; - } elseif (file_exists(storage_path('app/public/images/generated/'.$image_uuid.'.png'))) { + } elseif (Storage::disk('public')->exists('images/generated/'.$image_uuid.'.png')) { $image_path = 'images/generated/'.$image_uuid.'.png'; } else { $image_path = 'images/generated/'.$image_uuid.'.bmp'; @@ -106,7 +108,6 @@ Route::get('/display', function (Request $request) { if (config('services.trmnl.image_url_timeout')) { $response['image_url_timeout'] = config('services.trmnl.image_url_timeout'); } - // If update_firmware is true, reset it after returning it, to avoid upgrade loop if ($device->update_firmware) { $device->resetUpdateFirmwareFlag(); @@ -271,9 +272,11 @@ Route::get('/current_screen', function (Request $request) { $image_path = 'images/setup-logo.bmp'; $filename = 'setup-logo.bmp'; } else { - if (file_exists(storage_path('app/public/images/generated/'.$image_uuid.'.bmp'))) { + if (isset($device->last_firmware_version) + && version_compare($device->last_firmware_version, '1.5.2', '<') + && Storage::disk('public')->exists('images/generated/'.$image_uuid.'.bmp')) { $image_path = 'images/generated/'.$image_uuid.'.bmp'; - } elseif (file_exists(storage_path('app/public/images/generated/'.$image_uuid.'.png'))) { + } elseif (Storage::disk('public')->exists('images/generated/'.$image_uuid.'.png')) { $image_path = 'images/generated/'.$image_uuid.'.png'; } else { $image_path = 'images/generated/'.$image_uuid.'.bmp'; diff --git a/tests/Feature/Api/DeviceEndpointsTest.php b/tests/Feature/Api/DeviceEndpointsTest.php index 693d4c1..7ec86f6 100644 --- a/tests/Feature/Api/DeviceEndpointsTest.php +++ b/tests/Feature/Api/DeviceEndpointsTest.php @@ -448,9 +448,9 @@ test('authenticated user can fetch their devices', function () { 'friendly_id', 'mac_address', 'battery_voltage', - 'rssi' - ] - ] + 'rssi', + ], + ], ]) ->assertJsonCount(2, 'data'); @@ -463,9 +463,9 @@ test('authenticated user can fetch their devices', function () { 'friendly_id' => $devices[0]->friendly_id, 'mac_address' => $devices[0]->mac_address, 'battery_voltage' => 3.72, - 'rssi' => -63 - ] - ] + 'rssi' => -63, + ], + ], ]); }); diff --git a/tests/Feature/Api/DeviceImageFormatTest.php b/tests/Feature/Api/DeviceImageFormatTest.php new file mode 100644 index 0000000..2997853 --- /dev/null +++ b/tests/Feature/Api/DeviceImageFormatTest.php @@ -0,0 +1,120 @@ +makeDirectory('/images/generated'); +}); + +test('device with firmware version 1.5.1 gets bmp format', function () { + $device = Device::factory()->create([ + 'mac_address' => '00:11:22:33:44:55', + 'api_key' => 'test-api-key', + 'current_screen_image' => 'test-image', + 'last_firmware_version' => '1.5.1', + ]); + + // Create both bmp and png files + Storage::disk('public')->put('images/generated/test-image.bmp', 'fake bmp content'); + Storage::disk('public')->put('images/generated/test-image.png', 'fake png content'); + + // Test /api/display endpoint + $displayResponse = $this->withHeaders([ + 'id' => $device->mac_address, + 'access-token' => $device->api_key, + 'rssi' => -70, + 'battery_voltage' => 3.8, + 'fw-version' => '1.5.1', + ])->get('/api/display'); + + $displayResponse->assertOk() + ->assertJson([ + 'filename' => 'test-image.bmp', + ]); + + // Test /api/current_screen endpoint + $currentScreenResponse = $this->withHeaders([ + 'access-token' => $device->api_key, + ])->get('/api/current_screen'); + + $currentScreenResponse->assertOk() + ->assertJson([ + 'filename' => 'test-image.bmp', + ]); +}); + +test('device with firmware version 1.5.2 gets png format', function () { + $device = Device::factory()->create([ + 'mac_address' => '00:11:22:33:44:55', + 'api_key' => 'test-api-key', + 'current_screen_image' => 'test-image', + 'last_firmware_version' => '1.5.2', + ]); + + // Create both bmp and png files + Storage::disk('public')->put('images/generated/test-image.png', 'fake bmp content'); + + // Test /api/display endpoint + $displayResponse = $this->withHeaders([ + 'id' => $device->mac_address, + 'access-token' => $device->api_key, + 'rssi' => -70, + 'battery_voltage' => 3.8, + 'fw-version' => '1.5.2', + ])->get('/api/display'); + + $displayResponse->assertOk() + ->assertJson([ + 'filename' => 'test-image.png', + ]); + + // Test /api/current_screen endpoint + $currentScreenResponse = $this->withHeaders([ + 'access-token' => $device->api_key, + ])->get('/api/current_screen'); + + $currentScreenResponse->assertOk() + ->assertJson([ + 'filename' => 'test-image.png', + ]); +}); + +test('device falls back to bmp when png does not exist', function () { + $device = Device::factory()->create([ + 'mac_address' => '00:11:22:33:44:55', + 'api_key' => 'test-api-key', + 'current_screen_image' => 'test-image', + 'last_firmware_version' => '1.5.2', + ]); + + // Create only bmp file + Storage::disk('public')->put('images/generated/test-image.bmp', 'fake bmp content'); + + // Test /api/display endpoint + $displayResponse = $this->withHeaders([ + 'id' => $device->mac_address, + 'access-token' => $device->api_key, + 'rssi' => -70, + 'battery_voltage' => 3.8, + 'fw-version' => '1.5.2', + ])->get('/api/display'); + + $displayResponse->assertOk() + ->assertJson([ + 'filename' => 'test-image.bmp', + ]); + + // Test /api/current_screen endpoint + $currentScreenResponse = $this->withHeaders([ + 'access-token' => $device->api_key, + ])->get('/api/current_screen'); + + $currentScreenResponse->assertOk() + ->assertJson([ + 'filename' => 'test-image.bmp', + ]); +}); diff --git a/tests/Feature/GenerateScreenJobTest.php b/tests/Feature/GenerateScreenJobTest.php index b146424..feb1f40 100644 --- a/tests/Feature/GenerateScreenJobTest.php +++ b/tests/Feature/GenerateScreenJobTest.php @@ -23,7 +23,6 @@ test('it generates screen images and updates device', function () { // Assert both PNG and BMP files were created $uuid = $device->current_screen_image; Storage::disk('public')->assertExists("/images/generated/{$uuid}.png"); - Storage::disk('public')->assertExists("/images/generated/{$uuid}.bmp"); })->skipOnGitHubActions(); test('it cleans up unused images', function () {