mirror of
https://github.com/usetrmnl/byos_laravel.git
synced 2026-01-14 07:27:47 +00:00
add features
* feat: autojoin toggle * feat: auto add devices * feat: proxy feature * feat: support puppeteer in docker * feat: toggle to activate cloud proxy * feat: relay device information * feat: relay logs to cloud * feat: migrate on start * feat: calculate battery state, wifi signal * feat: eye candy for configure view * feat: update via api
This commit is contained in:
parent
d4eb832186
commit
715e6a2562
53 changed files with 1459 additions and 460 deletions
18
app/Console/Commands/FetchProxyCloudResponsesCommand.php
Normal file
18
app/Console/Commands/FetchProxyCloudResponsesCommand.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Jobs\FetchProxyCloudResponses;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class FetchProxyCloudResponsesCommand extends Command
|
||||
{
|
||||
protected $signature = 'trmnl:cloud:proxy';
|
||||
|
||||
protected $description = 'Fetch Cloud Screens';
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
FetchProxyCloudResponses::dispatchSync();
|
||||
}
|
||||
}
|
||||
|
|
@ -2,11 +2,8 @@
|
|||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Device;
|
||||
use App\Jobs\GenerateScreenJob;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Spatie\Browsershot\Browsershot;
|
||||
|
||||
class ScreenGeneratorCommand extends Command
|
||||
{
|
||||
|
|
@ -22,101 +19,28 @@ class ScreenGeneratorCommand extends Command
|
|||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = '';
|
||||
protected $description = 'Generate a screen for a terminal device';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
|
||||
$deviceId = $this->argument('deviceId');
|
||||
$view = $this->argument('view');
|
||||
|
||||
$uuid = Uuid::uuid4()->toString();
|
||||
$pngPath = Storage::disk('public')->path('/images/generated/'.$uuid.'.png');
|
||||
$bmpPath = Storage::disk('public')->path('/images/generated/'.$uuid.'.bmp');
|
||||
|
||||
// Generate PNG
|
||||
try {
|
||||
Browsershot::html(view($view)->render())
|
||||
->windowSize(800, 480)
|
||||
->save($pngPath);
|
||||
} catch (\Exception $e) {
|
||||
$this->error('Failed to generate PNG: '.$e->getMessage());
|
||||
$markup = view($view)->render();
|
||||
} catch (\Throwable $e) {
|
||||
$this->error('Failed to render view: '.$e->getMessage());
|
||||
|
||||
return;
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->convertToBmpImageMagick($pngPath, $bmpPath);
|
||||
GenerateScreenJob::dispatchSync($deviceId, $markup);
|
||||
|
||||
} catch (\ImagickException $e) {
|
||||
$this->error('Failed to convert image to BMP: '.$e->getMessage());
|
||||
}
|
||||
$this->info('Screen generation job finished.');
|
||||
|
||||
Device::find($deviceId)->update(['current_screen_image' => $uuid]);
|
||||
|
||||
$this->cleanupFolder();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \ImagickException
|
||||
*/
|
||||
public function convertToBmpImageMagick(string $pngPath, string $bmpPath): void
|
||||
{
|
||||
$imagick = new \Imagick($pngPath);
|
||||
$imagick->setImageType(\Imagick::IMGTYPE_GRAYSCALE);
|
||||
$imagick->quantizeImage(2, \Imagick::COLORSPACE_GRAY, 0, true, false);
|
||||
$imagick->setImageDepth(1);
|
||||
$imagick->stripImage();
|
||||
$imagick->setFormat('BMP3');
|
||||
$imagick->writeImage($bmpPath);
|
||||
$imagick->clear();
|
||||
}
|
||||
|
||||
// TODO retuns 8-bit BMP
|
||||
|
||||
// public function convertToBmpGD(string $pngPath, string $bmpPath): void
|
||||
// {
|
||||
// // Load the PNG image
|
||||
// $image = imagecreatefrompng($pngPath);
|
||||
//
|
||||
// // Create a new true color image with the same dimensions
|
||||
// $bwImage = imagecreatetruecolor(imagesx($image), imagesy($image));
|
||||
//
|
||||
// // Convert to black and white
|
||||
// imagefilter($image, IMG_FILTER_GRAYSCALE);
|
||||
//
|
||||
// // Copy the grayscale image to the new image
|
||||
// imagecopy($bwImage, $image, 0, 0, 0, 0, imagesx($image), imagesy($image));
|
||||
//
|
||||
// // Create a 1-bit palette
|
||||
// imagetruecolortopalette($bwImage, true, 2);
|
||||
//
|
||||
// // Save as BMP
|
||||
//
|
||||
// imagebmp($bwImage, $bmpPath, false);
|
||||
//
|
||||
// // Free up memory
|
||||
// imagedestroy($image);
|
||||
// imagedestroy($bwImage);
|
||||
// }
|
||||
public function cleanupFolder(): void
|
||||
{
|
||||
$activeImageUuids = Device::pluck('current_screen_image')->filter()->toArray();
|
||||
|
||||
$files = Storage::disk('public')->files('/images/generated/');
|
||||
foreach ($files as $file) {
|
||||
if (basename($file) === '.gitignore') {
|
||||
continue;
|
||||
}
|
||||
// Get filename without path and extension
|
||||
$fileUuid = pathinfo($file, PATHINFO_FILENAME);
|
||||
// If the UUID is not in use by any device, move it to archive
|
||||
if (! in_array($fileUuid, $activeImageUuids)) {
|
||||
Storage::disk('public')->delete($file);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
95
app/Jobs/FetchProxyCloudResponses.php
Normal file
95
app/Jobs/FetchProxyCloudResponses.php
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Device;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class FetchProxyCloudResponses implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
Device::where('proxy_cloud', true)->each(function ($device) {
|
||||
try {
|
||||
$response = Http::withHeaders([
|
||||
'id' => $device->mac_address,
|
||||
'access-token' => $device->api_key,
|
||||
'width' => 800,
|
||||
'height' => 480,
|
||||
'rssi' => $device->last_rssi_level,
|
||||
'battery_voltage' => $device->last_battery_voltage,
|
||||
'refresh-rate' => $device->default_refresh_interval,
|
||||
'fw-version' => $device->last_firmware_version,
|
||||
'accept-encoding' => 'identity;q=1,chunked;q=0.1,*;q=0',
|
||||
'user-agent' => 'ESP32HTTPClient',
|
||||
])->get(config('services.trmnl.proxy_base_url').'/api/display');
|
||||
|
||||
$device->update([
|
||||
'proxy_cloud_response' => $response->json(),
|
||||
]);
|
||||
|
||||
$imageUrl = $response->json('image_url');
|
||||
$filename = $response->json('filename');
|
||||
|
||||
\Log::info('Response data: '.$imageUrl);
|
||||
if (isset($imageUrl)) {
|
||||
try {
|
||||
$imageContents = Http::get($imageUrl)->body();
|
||||
if (! Storage::disk('public')->exists("images/generated/{$filename}.bmp")) {
|
||||
Storage::disk('public')->put(
|
||||
"images/generated/{$filename}.bmp",
|
||||
$imageContents
|
||||
);
|
||||
}
|
||||
|
||||
$device->update([
|
||||
'current_screen_image' => $filename,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Failed to download and save image for device: {$device->mac_address}", [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Log::info("Successfully updated proxy cloud response for device: {$device->mac_address}");
|
||||
|
||||
if ($device->last_log_request) {
|
||||
Http::withHeaders([
|
||||
'id' => $device->mac_address,
|
||||
'access-token' => $device->api_key,
|
||||
'width' => 800,
|
||||
'height' => 480,
|
||||
'rssi' => $device->last_rssi_level,
|
||||
'battery_voltage' => $device->last_battery_voltage,
|
||||
'refresh-rate' => $device->default_refresh_interval,
|
||||
'fw-version' => $device->last_firmware_version,
|
||||
'accept-encoding' => 'identity;q=1,chunked;q=0.1,*;q=0',
|
||||
'user-agent' => 'ESP32HTTPClient',
|
||||
])->post(config('services.trmnl.proxy_base_url').'/api/log', $device->last_log_request);
|
||||
|
||||
$device->update([
|
||||
'last_log_request' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error("Failed to fetch proxy cloud response for device: {$device->mac_address}", [
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
89
app/Jobs/GenerateScreenJob.php
Normal file
89
app/Jobs/GenerateScreenJob.php
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Device;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Spatie\Browsershot\Browsershot;
|
||||
|
||||
class GenerateScreenJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly int $deviceId,
|
||||
private readonly string $markup
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
$uuid = Uuid::uuid4()->toString();
|
||||
$pngPath = Storage::disk('public')->path('/images/generated/'.$uuid.'.png');
|
||||
$bmpPath = Storage::disk('public')->path('/images/generated/'.$uuid.'.bmp');
|
||||
|
||||
// Generate PNG
|
||||
try {
|
||||
Browsershot::html($this->markup)
|
||||
->setOption('args', config('app.puppeteer_docker') ? ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu'] : [])
|
||||
->windowSize(800, 480)
|
||||
->save($pngPath);
|
||||
} catch (\Exception $e) {
|
||||
throw new \RuntimeException('Failed to generate PNG: '.$e->getMessage(), 0, $e);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->convertToBmpImageMagick($pngPath, $bmpPath);
|
||||
} catch (\ImagickException $e) {
|
||||
throw new \RuntimeException('Failed to convert image to BMP: '.$e->getMessage(), 0, $e);
|
||||
}
|
||||
Device::find($this->deviceId)->update(['current_screen_image' => $uuid]);
|
||||
\Log::info("Device $this->deviceId: updated with new image: $uuid");
|
||||
|
||||
$this->cleanupFolder();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \ImagickException
|
||||
*/
|
||||
private function convertToBmpImageMagick(string $pngPath, string $bmpPath): void
|
||||
{
|
||||
$imagick = new \Imagick($pngPath);
|
||||
$imagick->setImageType(\Imagick::IMGTYPE_GRAYSCALE);
|
||||
$imagick->quantizeImage(2, \Imagick::COLORSPACE_GRAY, 0, true, false);
|
||||
$imagick->setImageDepth(1);
|
||||
$imagick->stripImage();
|
||||
$imagick->setFormat('BMP3');
|
||||
$imagick->writeImage($bmpPath);
|
||||
$imagick->clear();
|
||||
}
|
||||
|
||||
private function cleanupFolder(): void
|
||||
{
|
||||
$activeImageUuids = Device::pluck('current_screen_image')->filter()->toArray();
|
||||
|
||||
$files = Storage::disk('public')->files('/images/generated/');
|
||||
foreach ($files as $file) {
|
||||
if (basename($file) === '.gitignore') {
|
||||
continue;
|
||||
}
|
||||
// Get filename without path and extension
|
||||
$fileUuid = pathinfo($file, PATHINFO_FILENAME);
|
||||
// If the UUID is not in use by any device, move it to archive
|
||||
if (! in_array($fileUuid, $activeImageUuids)) {
|
||||
Storage::disk('public')->delete($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
37
app/Livewire/Actions/DeviceAutoJoin.php
Normal file
37
app/Livewire/Actions/DeviceAutoJoin.php
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Actions;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class DeviceAutoJoin extends Component
|
||||
{
|
||||
public bool $deviceAutojoin = false;
|
||||
|
||||
public bool $isFirstUser = false;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->deviceAutojoin = auth()->user()->assign_new_devices;
|
||||
$this->isFirstUser = auth()->user()->id === 1;
|
||||
|
||||
}
|
||||
|
||||
public function updating($name, $value)
|
||||
{
|
||||
$this->validate([
|
||||
'deviceAutojoin' => 'boolean',
|
||||
]);
|
||||
|
||||
if ($name === 'deviceAutojoin') {
|
||||
auth()->user()->update([
|
||||
'assign_new_devices' => $value,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.actions.device-auto-join');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Device;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
|
||||
class DeviceManager extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
public $showDeviceForm = false;
|
||||
|
||||
public $name;
|
||||
|
||||
public $mac_address;
|
||||
|
||||
public $api_key;
|
||||
|
||||
public $default_refresh_interval = 900;
|
||||
|
||||
public $friendly_id;
|
||||
|
||||
protected $rules = [
|
||||
'mac_address' => 'required',
|
||||
'api_key' => 'required',
|
||||
'default_refresh_interval' => 'required|integer',
|
||||
];
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.device-manager', [
|
||||
'devices' => auth()->user()->devices()->paginate(10),
|
||||
]);
|
||||
}
|
||||
|
||||
public function createDevice(): void
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
Device::factory([
|
||||
'name' => $this->name,
|
||||
'mac_address' => $this->mac_address,
|
||||
'api_key' => $this->api_key,
|
||||
'default_refresh_interval' => $this->default_refresh_interval,
|
||||
'friendly_id' => $this->friendly_id,
|
||||
'user_id' => auth()->id(),
|
||||
])->create();
|
||||
|
||||
$this->reset();
|
||||
\Flux::modal('create-device')->close();
|
||||
session()->flash('message', 'Device created successfully.');
|
||||
}
|
||||
}
|
||||
|
|
@ -10,4 +10,44 @@ class Device extends Model
|
|||
use HasFactory;
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected $casts = [
|
||||
'proxy_cloud' => 'boolean',
|
||||
'last_log_request' => 'json',
|
||||
];
|
||||
|
||||
public function getBatteryPercentAttribute()
|
||||
{
|
||||
$volts = $this->last_battery_voltage;
|
||||
|
||||
// Define min and max voltage for Li-ion battery (3.0V empty, 4.2V full)
|
||||
$min_volt = 3.0;
|
||||
$max_volt = 4.2;
|
||||
|
||||
// Ensure the voltage is within range
|
||||
if ($volts <= $min_volt) {
|
||||
return 0;
|
||||
} elseif ($volts >= $max_volt) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
// Calculate percentage
|
||||
$percent = (($volts - $min_volt) / ($max_volt - $min_volt)) * 100;
|
||||
|
||||
return round($percent);
|
||||
}
|
||||
|
||||
public function getWifiStrenghAttribute()
|
||||
{
|
||||
$rssi = $this->last_rssi_level;
|
||||
if ($rssi >= 0) {
|
||||
return 0; // No signal (0 bars)
|
||||
} elseif ($rssi <= -80) {
|
||||
return 1; // Weak signal (1 bar)
|
||||
} elseif ($rssi <= -60) {
|
||||
return 2; // Moderate signal (2 bars)
|
||||
} else {
|
||||
return 3; // Strong signal (3 bars)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ class User extends Authenticatable // implements MustVerifyEmail
|
|||
'name',
|
||||
'email',
|
||||
'password',
|
||||
'assign_new_devices',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -46,6 +47,7 @@ class User extends Authenticatable // implements MustVerifyEmail
|
|||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'assign_new_devices' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ class AppServiceProvider extends ServiceProvider
|
|||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
if (app()->isProduction() && config('app.force_https')) {
|
||||
\URL::forceScheme('https');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue