mirror of
https://github.com/usetrmnl/byos_laravel.git
synced 2026-01-13 15:07:49 +00:00
feat(#16): refactor
This commit is contained in:
parent
580a5833a8
commit
cc63c8cce2
9 changed files with 148 additions and 75 deletions
|
|
@ -36,8 +36,7 @@ class ScreenGeneratorCommand extends Command
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
GenerateScreenJob::dispatchSync($deviceId, null, $markup);
|
||||||
GenerateScreenJob::dispatchSync($deviceId, $markup);
|
|
||||||
|
|
||||||
$this->info('Screen generation job finished.');
|
$this->info('Screen generation job finished.');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Jobs;
|
|
||||||
|
|
||||||
use App\Models\Plugin;
|
|
||||||
use Illuminate\Bus\Queueable;
|
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class GeneratePluginJob implements ShouldQueue
|
|
||||||
{
|
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new job instance.
|
|
||||||
*/
|
|
||||||
public function __construct(
|
|
||||||
private readonly int $pluginId,
|
|
||||||
private readonly string $markup
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the job.
|
|
||||||
*/
|
|
||||||
public function handle(): void
|
|
||||||
{
|
|
||||||
$newImageUuid = CommonFunctions::generateImage($this->markup);
|
|
||||||
|
|
||||||
Plugin::find($this->pluginId)->update(['current_image' => $newImageUuid]);
|
|
||||||
\Log::info("Plugin $this->pluginId: updated with new image: $newImageUuid");
|
|
||||||
|
|
||||||
CommonFunctions::cleanupFolder();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
namespace App\Jobs;
|
namespace App\Jobs;
|
||||||
|
|
||||||
use App\Models\Device;
|
use App\Models\Device;
|
||||||
|
use App\Models\Plugin;
|
||||||
|
use App\Services\ImageGenerationService;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
|
@ -18,6 +20,7 @@ class GenerateScreenJob implements ShouldQueue
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly int $deviceId,
|
private readonly int $deviceId,
|
||||||
|
private readonly ?int $pluginId,
|
||||||
private readonly string $markup
|
private readonly string $markup
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
|
@ -26,11 +29,16 @@ class GenerateScreenJob implements ShouldQueue
|
||||||
*/
|
*/
|
||||||
public function handle(): void
|
public function handle(): void
|
||||||
{
|
{
|
||||||
$newImageUuid = CommonFunctions::generateImage($this->markup);
|
$newImageUuid = ImageGenerationService::generateImage($this->markup);
|
||||||
|
|
||||||
Device::find($this->deviceId)->update(['current_screen_image' => $newImageUuid]);
|
Device::find($this->deviceId)->update(['current_screen_image' => $newImageUuid]);
|
||||||
\Log::info("Device $this->deviceId: updated with new image: $newImageUuid");
|
\Log::info("Device $this->deviceId: updated with new image: $newImageUuid");
|
||||||
|
|
||||||
CommonFunctions::cleanupFolder();
|
if ($this->pluginId) {
|
||||||
|
// cache current image
|
||||||
|
Plugin::find($this->pluginId)->update(['current_image' => $newImageUuid]);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageGenerationService::cleanupFolder();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ class Plugin extends Model
|
||||||
'data_payload' => 'json',
|
'data_payload' => 'json',
|
||||||
'data_payload_updated_at' => 'datetime',
|
'data_payload_updated_at' => 'datetime',
|
||||||
'is_native' => 'boolean',
|
'is_native' => 'boolean',
|
||||||
'current_image' => 'string',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
protected static function boot()
|
protected static function boot()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Jobs;
|
namespace App\Services;
|
||||||
|
|
||||||
use App\Models\Device;
|
use App\Models\Device;
|
||||||
use App\Models\Plugin;
|
use App\Models\Plugin;
|
||||||
|
|
@ -9,7 +9,7 @@ use Ramsey\Uuid\Uuid;
|
||||||
use Spatie\Browsershot\Browsershot;
|
use Spatie\Browsershot\Browsershot;
|
||||||
use Wnx\SidecarBrowsershot\BrowsershotLambda;
|
use Wnx\SidecarBrowsershot\BrowsershotLambda;
|
||||||
|
|
||||||
class CommonFunctions
|
class ImageGenerationService
|
||||||
{
|
{
|
||||||
public static function generateImage(string $markup): string {
|
public static function generateImage(string $markup): string {
|
||||||
$uuid = Uuid::uuid4()->toString();
|
$uuid = Uuid::uuid4()->toString();
|
||||||
|
|
@ -37,7 +37,7 @@ class CommonFunctions
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
CommonFunctions::convertToBmpImageMagick($pngPath, $bmpPath);
|
ImageGenerationService::convertToBmpImageMagick($pngPath, $bmpPath);
|
||||||
} catch (\ImagickException $e) {
|
} catch (\ImagickException $e) {
|
||||||
throw new \RuntimeException('Failed to convert image to BMP: '.$e->getMessage(), 0, $e);
|
throw new \RuntimeException('Failed to convert image to BMP: '.$e->getMessage(), 0, $e);
|
||||||
}
|
}
|
||||||
|
|
@ -33,7 +33,7 @@ new class extends Component {
|
||||||
try {
|
try {
|
||||||
$rendered = Blade::render($this->blade_code);
|
$rendered = Blade::render($this->blade_code);
|
||||||
foreach ($this->checked_devices as $device) {
|
foreach ($this->checked_devices as $device) {
|
||||||
GenerateScreenJob::dispatchSync($device, $rendered);
|
GenerateScreenJob::dispatchSync($device, null, $rendered);
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->addError('error', $e->getMessage());
|
$this->addError('error', $e->getMessage());
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Jobs\GenerateScreenJob;
|
use App\Jobs\GenerateScreenJob;
|
||||||
use App\Jobs\GeneratePluginJob;
|
|
||||||
use App\Models\Device;
|
use App\Models\Device;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
@ -47,31 +46,32 @@ Route::get('/display', function (Request $request) {
|
||||||
// Get current screen image from mirror device or continue if not available
|
// Get current screen image from mirror device or continue if not available
|
||||||
if (! $image_uuid = $device->mirrorDevice?->current_screen_image) {
|
if (! $image_uuid = $device->mirrorDevice?->current_screen_image) {
|
||||||
$refreshTimeOverride = null;
|
$refreshTimeOverride = null;
|
||||||
$nextPlaylistItem = $device->getNextPlaylistItem();
|
|
||||||
// Skip if cloud proxy is enabled for the device
|
// Skip if cloud proxy is enabled for the device
|
||||||
if (! $device->proxy_cloud && $nextPlaylistItem) {
|
if (! $device->proxy_cloud || $device->getNextPlaylistItem()) {
|
||||||
$refreshTimeOverride = $nextPlaylistItem->playlist()->first()->refresh_time;
|
$playlistItem = $device->getNextPlaylistItem();
|
||||||
$plugin = $nextPlaylistItem->plugin;
|
if ($playlistItem) {
|
||||||
|
$refreshTimeOverride = $playlistItem->playlist()->first()->refresh_time;
|
||||||
|
$plugin = $playlistItem->plugin;
|
||||||
|
|
||||||
// Check and update stale data if needed
|
// Check and update stale data if needed
|
||||||
if ($plugin->isDataStale() || $plugin->current_image == null) {
|
if ($plugin->isDataStale() || $plugin->current_image == null) {
|
||||||
$plugin->updateDataPayload();
|
$plugin->updateDataPayload();
|
||||||
|
|
||||||
if ($plugin->render_markup) {
|
if ($plugin->render_markup) {
|
||||||
$markup = Blade::render($plugin->render_markup, ['data' => $plugin->data_payload]);
|
$markup = Blade::render($plugin->render_markup, ['data' => $plugin->data_payload]);
|
||||||
} elseif ($plugin->render_markup_view) {
|
} elseif ($plugin->render_markup_view) {
|
||||||
$markup = view($plugin->render_markup_view, ['data' => $plugin->data_payload])->render();
|
$markup = view($plugin->render_markup_view, ['data' => $plugin->data_payload])->render();
|
||||||
|
}
|
||||||
|
|
||||||
|
GenerateScreenJob::dispatchSync($device->id, $plugin->id, $markup);
|
||||||
}
|
}
|
||||||
|
|
||||||
GeneratePluginJob::dispatchSync($plugin->id, $markup);
|
$plugin->refresh();
|
||||||
}
|
|
||||||
|
|
||||||
$plugin->refresh();
|
if ($plugin->current_image != null) {
|
||||||
|
$playlistItem->update(['last_displayed_at' => now()]);
|
||||||
if ($plugin->current_image != null)
|
$device->update(['current_screen_image' => $plugin->current_image]);
|
||||||
{
|
}
|
||||||
$nextPlaylistItem->update(['last_displayed_at' => now()]);
|
|
||||||
$device->update(['current_screen_image' => $plugin->current_image]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -198,11 +198,11 @@ Route::get('/devices', function (Request $request) {
|
||||||
'friendly_id',
|
'friendly_id',
|
||||||
'mac_address',
|
'mac_address',
|
||||||
'last_battery_voltage as battery_voltage',
|
'last_battery_voltage as battery_voltage',
|
||||||
'last_rssi_level as rssi'
|
'last_rssi_level as rssi',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'data' => $devices
|
'data' => $devices,
|
||||||
]);
|
]);
|
||||||
})->middleware('auth:sanctum');
|
})->middleware('auth:sanctum');
|
||||||
|
|
||||||
|
|
@ -217,7 +217,7 @@ Route::post('/display/update', function (Request $request) {
|
||||||
|
|
||||||
$view = Blade::render($request['markup']);
|
$view = Blade::render($request['markup']);
|
||||||
|
|
||||||
GenerateScreenJob::dispatchSync($deviceId, $view);
|
GenerateScreenJob::dispatchSync($deviceId, null, $view);
|
||||||
|
|
||||||
response()->json([
|
response()->json([
|
||||||
'message' => 'success',
|
'message' => 'success',
|
||||||
|
|
|
||||||
|
|
@ -469,7 +469,7 @@ test('authenticated user can fetch their devices', function () {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('plugin doesn\'t update image unless required', function () {
|
test('plugin caches image until data is stale', function () {
|
||||||
// Create source device with a playlist
|
// Create source device with a playlist
|
||||||
$device = Device::factory()->create([
|
$device = Device::factory()->create([
|
||||||
'mac_address' => '55:11:22:33:44:55',
|
'mac_address' => '55:11:22:33:44:55',
|
||||||
|
|
@ -479,11 +479,11 @@ test('plugin doesn\'t update image unless required', function () {
|
||||||
|
|
||||||
$plugin = Plugin::factory()->create([
|
$plugin = Plugin::factory()->create([
|
||||||
'name' => 'Zen Quotes',
|
'name' => 'Zen Quotes',
|
||||||
'polling_url' => 'https://zenquotes.io/api/today',
|
'polling_url' => null,
|
||||||
'data_stale_minutes' => 1,
|
'data_stale_minutes' => 1,
|
||||||
'data_strategy' => 'polling',
|
'data_strategy' => 'polling',
|
||||||
'polling_verb' => 'get',
|
'polling_verb' => 'get',
|
||||||
'render_markup_view' => 'recipes.zen',
|
'render_markup_view' => 'trmnl',
|
||||||
'is_native' => false,
|
'is_native' => false,
|
||||||
'data_payload_updated_at' => null,
|
'data_payload_updated_at' => null,
|
||||||
]);
|
]);
|
||||||
|
|
@ -543,3 +543,107 @@ test('plugin doesn\'t update image unless required', function () {
|
||||||
expect($thirdResponse['filename'])
|
expect($thirdResponse['filename'])
|
||||||
->not->toBe($firstResponse['filename']);
|
->not->toBe($firstResponse['filename']);
|
||||||
})->skipOnGitHubActions();
|
})->skipOnGitHubActions();
|
||||||
|
|
||||||
|
test('plugins in playlist are rendered in order', function () {
|
||||||
|
// Create source device with a playlist
|
||||||
|
$device = Device::factory()->create([
|
||||||
|
'mac_address' => '55:11:22:33:44:55',
|
||||||
|
'api_key' => 'source-api-key',
|
||||||
|
'proxy_cloud' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create two plugins
|
||||||
|
$firstPlugin = Plugin::factory()->create([
|
||||||
|
'name' => 'First Plugin',
|
||||||
|
'polling_url' => null,
|
||||||
|
'data_stale_minutes' => 1,
|
||||||
|
'data_strategy' => 'polling',
|
||||||
|
'polling_verb' => 'get',
|
||||||
|
'render_markup_view' => 'trmnl',
|
||||||
|
'is_native' => false,
|
||||||
|
'data_payload_updated_at' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$secondPlugin = Plugin::factory()->create([
|
||||||
|
'name' => 'Second Plugin',
|
||||||
|
'polling_url' => null,
|
||||||
|
'data_stale_minutes' => 1,
|
||||||
|
'data_strategy' => 'polling',
|
||||||
|
'polling_verb' => 'get',
|
||||||
|
'render_markup_view' => 'trmnl',
|
||||||
|
'is_native' => false,
|
||||||
|
'data_payload_updated_at' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create playlist
|
||||||
|
$playlist = Playlist::factory()->create([
|
||||||
|
'device_id' => $device->id,
|
||||||
|
'name' => 'Two Plugins Test',
|
||||||
|
'is_active' => true,
|
||||||
|
'weekdays' => null,
|
||||||
|
'active_from' => null,
|
||||||
|
'active_until' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Add plugins to playlist in specific order
|
||||||
|
PlaylistItem::factory()->create([
|
||||||
|
'playlist_id' => $playlist->id,
|
||||||
|
'plugin_id' => $firstPlugin->id,
|
||||||
|
'order' => 1,
|
||||||
|
'is_active' => true,
|
||||||
|
'last_displayed_at' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
PlaylistItem::factory()->create([
|
||||||
|
'playlist_id' => $playlist->id,
|
||||||
|
'plugin_id' => $secondPlugin->id,
|
||||||
|
'order' => 2,
|
||||||
|
'is_active' => true,
|
||||||
|
'last_displayed_at' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// First request should show the first plugin
|
||||||
|
$firstResponse = $this->withHeaders([
|
||||||
|
'id' => $device->mac_address,
|
||||||
|
'access-token' => $device->api_key,
|
||||||
|
])->get('/api/display');
|
||||||
|
|
||||||
|
$firstResponse->assertOk();
|
||||||
|
$firstImageFilename = $firstResponse['filename'];
|
||||||
|
expect($firstImageFilename)->not->toBe('setup-logo.bmp');
|
||||||
|
|
||||||
|
// Get the first plugin's playlist item and verify it was marked as displayed
|
||||||
|
$firstPluginItem = PlaylistItem::where('plugin_id', $firstPlugin->id)->first();
|
||||||
|
expect($firstPluginItem->last_displayed_at)->not->toBeNull();
|
||||||
|
|
||||||
|
// Second request should show the second plugin
|
||||||
|
$secondResponse = $this->withHeaders([
|
||||||
|
'id' => $device->mac_address,
|
||||||
|
'access-token' => $device->api_key,
|
||||||
|
'rssi' => -70,
|
||||||
|
'battery_voltage' => 3.8,
|
||||||
|
'fw-version' => '1.0.0',
|
||||||
|
])->get('/api/display');
|
||||||
|
|
||||||
|
$secondResponse->assertOk();
|
||||||
|
expect($secondResponse['filename'])
|
||||||
|
->not->toBe($firstImageFilename)
|
||||||
|
->not->toBe('setup-logo.bmp');
|
||||||
|
|
||||||
|
// Get the second plugin's playlist item and verify it was marked as displayed
|
||||||
|
$secondPluginItem = PlaylistItem::where('plugin_id', $secondPlugin->id)->first();
|
||||||
|
expect($secondPluginItem->last_displayed_at)->not->toBeNull();
|
||||||
|
|
||||||
|
// Third request should show the first plugin again
|
||||||
|
$thirdResponse = $this->withHeaders([
|
||||||
|
'id' => $device->mac_address,
|
||||||
|
'access-token' => $device->api_key,
|
||||||
|
'rssi' => -70,
|
||||||
|
'battery_voltage' => 3.8,
|
||||||
|
'fw-version' => '1.0.0',
|
||||||
|
])->get('/api/display');
|
||||||
|
|
||||||
|
$thirdResponse->assertOk();
|
||||||
|
expect($thirdResponse['filename'])
|
||||||
|
->not->toBe($secondResponse['filename']);
|
||||||
|
})->skipOnGitHubActions();
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ beforeEach(function () {
|
||||||
|
|
||||||
test('it generates screen images and updates device', function () {
|
test('it generates screen images and updates device', function () {
|
||||||
$device = Device::factory()->create();
|
$device = Device::factory()->create();
|
||||||
$job = new GenerateScreenJob($device->id, view('trmnl')->render());
|
$job = new GenerateScreenJob($device->id, null, view('trmnl')->render());
|
||||||
$job->handle();
|
$job->handle();
|
||||||
|
|
||||||
// Assert the device was updated with a new image UUID
|
// Assert the device was updated with a new image UUID
|
||||||
|
|
@ -39,7 +39,7 @@ test('it cleans up unused images', function () {
|
||||||
Storage::disk('public')->put('/images/generated/inactive-uuid.bmp', 'test');
|
Storage::disk('public')->put('/images/generated/inactive-uuid.bmp', 'test');
|
||||||
|
|
||||||
// Run a job which will trigger cleanup
|
// Run a job which will trigger cleanup
|
||||||
$job = new GenerateScreenJob($activeDevice->id, '<div>Test</div>');
|
$job = new GenerateScreenJob($activeDevice->id, null, '<div>Test</div>');
|
||||||
$job->handle();
|
$job->handle();
|
||||||
|
|
||||||
Storage::disk('public')->assertMissing('/images/generated/uuid-to-be-replaced.png');
|
Storage::disk('public')->assertMissing('/images/generated/uuid-to-be-replaced.png');
|
||||||
|
|
@ -52,7 +52,7 @@ test('it preserves gitignore file during cleanup', function () {
|
||||||
Storage::disk('public')->put('/images/generated/.gitignore', '*');
|
Storage::disk('public')->put('/images/generated/.gitignore', '*');
|
||||||
|
|
||||||
$device = Device::factory()->create();
|
$device = Device::factory()->create();
|
||||||
$job = new GenerateScreenJob($device->id, '<div>Test</div>');
|
$job = new GenerateScreenJob($device->id, null, '<div>Test</div>');
|
||||||
$job->handle();
|
$job->handle();
|
||||||
|
|
||||||
Storage::disk('public')->assertExists('/images/generated/.gitignore');
|
Storage::disk('public')->assertExists('/images/generated/.gitignore');
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue