#1 12-04-2021 21:49:45

IvanAltay
Administrator
Зарегистрирован: 03-05-2018
Сообщений: 4,586

ESP32 с видеовыходом VGA. На базе библиотеки FABGL.

Купил на Авито б/у дисплей LG FLATRON E2040S(с небольшими неисправностями) за 700 рублей. Починил.
https://market.yandex.ru/product--monit … track=tabs
Монитор 20" с довольно странным разрешением 1600x900 (16:9). Показывает довольно стрёмненько с классическими видеокартами ПК, да и бог с ним, покупался он для других целей. У монитора внешний блок питания 12В.

16182389629133325334624575500812.jpg

1618239290306203619770851405915.jpg

Не в сети

#2 12-04-2021 22:12:18

IvanAltay
Administrator
Зарегистрирован: 03-05-2018
Сообщений: 4,586

Re: ESP32 с видеовыходом VGA. На базе библиотеки FABGL.

https://github.com/fdivitto/FabGL
Контроллер дисплея, контроллер мыши и клавиатуры PS / 2, графическая библиотека, звуковой движок, графический интерфейс пользователя (GUI), игровой движок и терминал ANSI / VT для ESP32.

Не в сети

#3 12-04-2021 22:22:29

IvanAltay
Administrator
Зарегистрирован: 03-05-2018
Сообщений: 4,586

Re: ESP32 с видеовыходом VGA. На базе библиотеки FABGL.

На фото эмулятор антикварного ПК Altair 8800(из примеров библиотеки fabgl).
16182406582779012623739654339627.jpg
Плата у меня получилась такая.
16182407085522390142341762269157.jpg

1618240747434846651710837132236.jpg

16182408022468378631468182977483.jpg

Не в сети

#4 12-04-2021 22:25:36

IvanAltay
Administrator
Зарегистрирован: 03-05-2018
Сообщений: 4,586

Re: ESP32 с видеовыходом VGA. На базе библиотеки FABGL.

16182408772862740020528816144101.jpg

ESP32 - DEVKIT V1(30pin).
1618240941008388811353071379690.jpg

Мышки подходят не все(под эмуляторы старых ПК). Из трёх штук мышек, у меня работает только самая древняя.


16182409931717143770429553066218.jpg
16182435929437171397517394540806.jpg

Не в сети

#5 12-04-2021 22:38:22

IvanAltay
Administrator
Зарегистрирован: 03-05-2018
Сообщений: 4,586

Re: ESP32 с видеовыходом VGA. На базе библиотеки FABGL.

По сути, я собрал аналог платы TTGO VGA32_V1.4
VGA32 V1.4 контроллер PS/2 VGA Мышь Клавиатура графическая библиотека игровой двигатель ANSI/VT терминальное управление для ESP32
https://aliexpress.ru/item/100500176272 … 7469039873
Только у меня на плате ещё два понижающих DC/DC. 12..24В--12В и 12..24В--5В.
Для аккумуляторного питания. Монитор оказался довольно экономичный и яркий. От 12В, потребляет примерно 1.5 А(18..20 Вт всего монитор 20" кушает) на яркости 70%. Делал микрокомпьютер с автономным/аккумуляторным питанием 12..24В и большим ярким дисплеем.

Не в сети

#6 12-04-2021 22:45:13

IvanAltay
Administrator
Зарегистрирован: 03-05-2018
Сообщений: 4,586

Re: ESP32 с видеовыходом VGA. На базе библиотеки FABGL.

Эмулятор Commodore VIC20.
16182419858744982758769454972796.jpg

16182420407742228656025250184163.jpg

16182420909533034821368392006819.jpg

16182421547235197306465077429598.jpg

Не в сети

#7 12-04-2021 23:18:32

IvanAltay
Administrator
Зарегистрирован: 03-05-2018
Сообщений: 4,586

Re: ESP32 с видеовыходом VGA. На базе библиотеки FABGL.

Вообще, дисплей LG FLATRON E2040S даёт очень чёткую и яркую картинку совместно с моей платой. Мышка и клавиатура, работают стабильно. Мне результат эксперимента понравился.

Не в сети

#8 12-04-2021 23:53:21

IvanAltay
Administrator
Зарегистрирован: 03-05-2018
Сообщений: 4,586

Re: ESP32 с видеовыходом VGA. На базе библиотеки FABGL.

Немного допилил пример Modelinestudio(из библиотеки Fabgl.h). Теперь, каждые 15 секунд меняется видеорежим монитора.
16182460600168095936981230146818.jpg

16182461476076607163838210467600.jpg

16182462010514627007463513424103.jpg

1618246250495811187263032440067.jpg

Не в сети

#9 12-04-2021 23:59:51

IvanAltay
Administrator
Зарегистрирован: 03-05-2018
Сообщений: 4,586

Re: ESP32 с видеовыходом VGA. На базе библиотеки FABGL.

Код.

#include "fabgl.h"
const char * PresetResolutions[] = {
  VGA_256x384_60Hz,
  VGA_320x200_75Hz,
  VGA_320x200_75HzRetro,
  QVGA_320x240_60Hz,
  VGA_400x300_60Hz,
  VGA_480x300_75Hz,
  VGA_512x192_60Hz,
  VGA_512x384_60Hz,
  VGA_512x448_60Hz,
  VGA_512x512_58Hz,
  VGA_640x200_70Hz,
  VGA_640x200_70HzRetro,
  VGA_640x240_60Hz,
  VGA_640x350_70Hz,
  VGA_640x350_70HzAlt1,
  VESA_640x350_85Hz,
  VGA_640x382_60Hz,
  VGA_640x384_60Hz,
  VGA_640x400_70Hz,
  VGA_640x400_60Hz,
  VGA_640x480_60Hz,
  VGA_640x480_60HzAlt1,
  VGA_640x480_60HzD,
  VGA_640x480_73Hz,
  VESA_640x480_75Hz,
  VESA_720x400_85Hz,
  PAL_720x576_50Hz,
  VESA_768x576_60Hz,
  SVGA_800x300_60Hz,
  SVGA_800x600_56Hz,
  SVGA_800x600_60Hz,
  SVGA_960x540_60Hz,
  SVGA_1024x768_60Hz,
  SVGA_1024x768_70Hz,
  SVGA_1024x768_75Hz,
  SVGA_1280x600_60Hz,
  SVGA_1280x720_60Hz,
  SVGA_1280x720_60HzAlt1,
  SVGA_1280x768_50Hz,
};
int currentResolution = 0;  // VESA_640x480_75Hz
int moveX = 0;
int moveY = 0;
int shrinkX = 0;
int shrinkY = 0;
fabgl::VGA2Controller VGAController;
void printHelp()
{
  Serial.printf("Modeline studio\n");
  Serial.printf("Chip Revision: %d   Chip Frequency: %d MHz\n\n", ESP.getChipRevision(), ESP.getCpuFreqMHz());
  Serial.printf("%s\n", VGAController.getResolutionTimings()->label);
  Serial.printf("\nScreen move:\n");
  Serial.printf("  w = Move Up   z = Move Down   a = Move Left   s = Move Right\n");
  Serial.printf("Screen shrink:\n");
  Serial.printf("  n = Dec Horiz N = Inc Horiz   m = Dec Vert    M = Inc Vert\n");
  Serial.printf("Resolutions:\n");
  Serial.printf("  + = Next resolution  - = Prev resolution   x = Restore modeline\n");
  Serial.printf("Modeline change:\n");
  Serial.printf("  ! = Insert modline. Example: !\"640x350@70Hz\" 25.175 640 656 752 800 350 366 462 510 -HSync -VSync\n");
  /*
  Serial.printf("  e = Decrease horiz. Front Porch   E = Increase horiz. Front Porch\n");
  Serial.printf("  r = Decrease horiz. Sync Pulse    R = Increase horiz. Sync Pulse\n");
  Serial.printf("  t = Decrease horiz. Bach Porch    T = Increase horiz. Back Porch\n");
  Serial.printf("  y = Decrease vert. Front Porch    Y = Increase vert. Front Porch\n");
  Serial.printf("  u = Decrease vert. Sync Pulse     U = Increase vert. Sync Pulse\n");
  Serial.printf("  i = Decrease vert. Bach Porch     I = Increase vert. Back Porch\n");
  */
  Serial.printf("  o = Decrease frequency by 5KHz    O = Increase frequency by 5KHz\n");
  Serial.printf("  . = Change horiz. signals order\n");
  Serial.printf("Various:\n");
  Serial.printf("  h = Print This help\n\n");
}
void printInfo()
{
  auto t = VGAController.getResolutionTimings();
  Serial.printf("Modeline \"%s\" %2.8g %d %d %d %d %d %d %d %d %cHSync %cVSync %s %s\n",
                t->label,
                t->frequency / 1000000.0,
                t->HVisibleArea,
                t->HVisibleArea + t->HFrontPorch,
                t->HVisibleArea + t->HFrontPorch + t->HSyncPulse,
                t->HVisibleArea + t->HFrontPorch + t->HSyncPulse + t->HBackPorch,
                t->VVisibleArea,
                t->VVisibleArea + t->VFrontPorch,
                t->VVisibleArea + t->VFrontPorch + t->VSyncPulse,
                t->VVisibleArea + t->VFrontPorch + t->VSyncPulse + t->VBackPorch,
                t->HSyncLogic, t->VSyncLogic,
                t->scanCount == 2 ? "DoubleScan" : "",
                t->HStartingBlock == VGAScanStart::FrontPorch ? "FrontPorchBegins" :
                (t->HStartingBlock == VGAScanStart::Sync ? "SyncBegins" :
                (t->HStartingBlock == VGAScanStart::BackPorch ? "BackPorchBegins" : "VisibleBegins")));

  //Serial.printf("VFront = %d, VSync = %d, VBack = %d\n", t->VFrontPorch, t->VSyncPulse, t->VBackPorch);

  if (moveX || moveY)
    Serial.printf("moveScreen(%d, %d)\n", moveX, moveY);
  if (shrinkX || shrinkY)
    Serial.printf("shrinkScreen(%d, %d)\n", shrinkX, shrinkY);
  Serial.printf("Screen size   : %d x %d\n", VGAController.getScreenWidth(), VGAController.getScreenHeight());
  Serial.printf("Viewport size : %d x %d\n", VGAController.getViewPortWidth(), VGAController.getViewPortHeight());
  Serial.printf("Free memory (total, min, largest): %d, %d, %d\n\n", heap_caps_get_free_size(MALLOC_CAP_32BIT),
                                                                   heap_caps_get_minimum_free_size(MALLOC_CAP_32BIT),
                                                                   heap_caps_get_largest_free_block(MALLOC_CAP_32BIT));
}


void updateScreen()
{
  Canvas cv(&VGAController);
  cv.setPenColor(Color::White);
  cv.setBrushColor(Color::Black);
  cv.clear();
  cv.fillRectangle(0, 0, cv.getWidth() - 1, cv.getHeight() - 1);
  cv.drawRectangle(0, 0, cv.getWidth() - 1, cv.getHeight() - 1);

  cv.selectFont(&fabgl::FONT_8x8);
  cv.setGlyphOptions(GlyphOptions().FillBackground(true).DoubleWidth(1));
  cv.drawText(40, 20, VGAController.getResolutionTimings()->label);

  cv.setGlyphOptions(GlyphOptions());
  cv.drawTextFmt(40, 40, "Screen Size   : %d x %d", VGAController.getScreenWidth(), VGAController.getScreenHeight());
  cv.drawTextFmt(40, 60, "Viewport Size : %d x %d", cv.getWidth(), cv.getHeight());
  cv.drawText(40, 80,    "Commands (More on UART):");
  cv.drawText(40, 100,   "  w = Move Up    z = Move Down");
  cv.drawText(40, 120,   "  a = Move Left  s = Move Right");
  cv.drawText(40, 140,   "  + = Next Resolution");
  cv.drawRectangle(35, 15, 310, 155);
}
void setup()
{
  Serial.begin(115200);
 // avoid garbage into the UART
  delay(500);
  VGAController.begin();
  VGAController.setResolution(PresetResolutions[currentResolution]);
  updateScreen();
  printHelp();
  printInfo();
}
void loop()
{
  fabgl::VGATimings t;
currentResolution++;
 VGAController.setResolution(PresetResolutions[currentResolution]);
        updateScreen();
        printInfo();
delay(15000);
}

Не в сети

#10 13-04-2021 01:31:49

IvanAltay
Administrator
Зарегистрирован: 03-05-2018
Сообщений: 4,586

Re: ESP32 с видеовыходом VGA. На базе библиотеки FABGL.

Тест режимов монитора с цветом.
16182517471087233683758522005179.jpg

16182517891128026829279552554899.jpg

16182518329172479521132170067671.jpg
Монитор не поддерживает этот режим.
16182518790755932476644895116258.jpg

16182519205241932804890542455407.jpg

16182519537733618658133005915114.jpg

16182520339902565761433384487330.jpg

Не в сети

#11 13-04-2021 01:39:51

IvanAltay
Administrator
Зарегистрирован: 03-05-2018
Сообщений: 4,586

Re: ESP32 с видеовыходом VGA. На базе библиотеки FABGL.

Код.

#include "fabgl.h"
const char * PresetResolutions[] = {
  VGA_256x384_60Hz,
  VGA_320x200_75Hz,
  VGA_320x200_75HzRetro,
  QVGA_320x240_60Hz,
  VGA_400x300_60Hz,
  VGA_480x300_75Hz,
  VGA_512x192_60Hz,
  VGA_512x384_60Hz,
  VGA_512x448_60Hz,
  VGA_512x512_58Hz,
  VGA_640x200_70Hz,
  VGA_640x200_70HzRetro,
  VGA_640x240_60Hz,
  VGA_640x350_70Hz,
  VGA_640x350_70HzAlt1,
  VESA_640x350_85Hz,
  VGA_640x382_60Hz,
  VGA_640x384_60Hz,
  VGA_640x400_70Hz,
  VGA_640x400_60Hz,
  VGA_640x480_60Hz,
  VGA_640x480_60HzAlt1,
  VGA_640x480_60HzD,
  VGA_640x480_73Hz,
  VESA_640x480_75Hz,
  VESA_720x400_85Hz,
  PAL_720x576_50Hz,
  VESA_768x576_60Hz,
  SVGA_800x300_60Hz,
  SVGA_800x600_56Hz,
  SVGA_800x600_60Hz,
  SVGA_960x540_60Hz,
  SVGA_1024x768_60Hz,
  SVGA_1024x768_70Hz,
  SVGA_1024x768_75Hz,
  SVGA_1280x600_60Hz,
  SVGA_1280x720_60Hz,
  SVGA_1280x720_60HzAlt1,
  SVGA_1280x768_50Hz,};
int currentResolution = 0;  // VESA_640x480_75Hz
int moveX = 0;
int moveY = 0;
int shrinkX = 0;
int shrinkY = 0;
fabgl::VGAController VGAController;
void printInfo()
{
  auto t = VGAController.getResolutionTimings();
  Serial.printf("Modeline \"%s\" %2.8g %d %d %d %d %d %d %d %d %cHSync %cVSync %s %s\n",
                t->label,
                t->frequency / 1000000.0,
                t->HVisibleArea,
                t->HVisibleArea + t->HFrontPorch,
                t->HVisibleArea + t->HFrontPorch + t->HSyncPulse,
                t->HVisibleArea + t->HFrontPorch + t->HSyncPulse + t->HBackPorch,
                t->VVisibleArea,
                t->VVisibleArea + t->VFrontPorch,
                t->VVisibleArea + t->VFrontPorch + t->VSyncPulse,
                t->VVisibleArea + t->VFrontPorch + t->VSyncPulse + t->VBackPorch,
                t->HSyncLogic, t->VSyncLogic,
                t->scanCount == 2 ? "DoubleScan" : "",
                t->HStartingBlock == VGAScanStart::FrontPorch ? "FrontPorchBegins" :
                (t->HStartingBlock == VGAScanStart::Sync ? "SyncBegins" :
                (t->HStartingBlock == VGAScanStart::BackPorch ? "BackPorchBegins" : "VisibleBegins")));
  //Serial.printf("VFront = %d, VSync = %d, VBack = %d\n", t->VFrontPorch, t->VSyncPulse, t->VBackPorch);
 if (moveX || moveY)
    Serial.printf("moveScreen(%d, %d)\n", moveX, moveY);
  if (shrinkX || shrinkY)
    Serial.printf("shrinkScreen(%d, %d)\n", shrinkX, shrinkY);
  Serial.printf("Screen size   : %d x %d\n", VGAController.getScreenWidth(), VGAController.getScreenHeight());
  Serial.printf("Viewport size : %d x %d\n", VGAController.getViewPortWidth(), VGAController.getViewPortHeight());
  Serial.printf("Free memory (total, min, largest): %d, %d, %d\n\n", heap_caps_get_free_size(MALLOC_CAP_32BIT),
                                                                   heap_caps_get_minimum_free_size(MALLOC_CAP_32BIT),
                                                                   heap_caps_get_largest_free_block(MALLOC_CAP_32BIT));
}
void updateScreen()
{
  Canvas cv(&VGAController);
  cv.setPenColor(Color::White);
  cv.setBrushColor(Color::Black);
  cv.clear();
  cv.fillRectangle(0, 0, cv.getWidth() - 1, cv.getHeight() - 1);
  cv.drawRectangle(0, 0, cv.getWidth() - 1, cv.getHeight() - 1);
  cv.selectFont(&fabgl::FONT_8x8);
  cv.setGlyphOptions(GlyphOptions().FillBackground(true).DoubleWidth(1));
  cv.drawText(40, 20, VGAController.getResolutionTimings()->label);
  cv.setGlyphOptions(GlyphOptions());
  cv.drawTextFmt(40, 40, "Screen Size   : %d x %d", VGAController.getScreenWidth(), VGAController.getScreenHeight());
  cv.drawTextFmt(40, 60, "Viewport Size : %d x %d", cv.getWidth(), cv.getHeight());
  cv.setPenColor(Color::Green);
  cv.drawRectangle(35, 15, 310, 155);
cv.setPenColor(Color::Red);
cv.drawText(1, 160,   "Red");
cv.setPenColor(Color::Green);
cv.drawText(50, 160,   "Green");
cv.setPenColor(Color::Blue);
cv.drawText(100, 160,   "Blue");
cv.setPenColor(Color::Yellow);
cv.drawText(150, 160,   "Yellow");
}
void setup()
{
  Serial.begin(115200);
  delay(500);
  VGAController.begin();
  VGAController.setResolution(PresetResolutions[currentResolution]);
  updateScreen();
  printInfo();
}
void loop()
{
  fabgl::VGATimings t;
currentResolution++;
VGAController.setResolution(PresetResolutions[currentResolution]);
        updateScreen();
        printInfo();

delay(15000);
}

Не в сети

#12 13-04-2021 01:54:12

IvanAltay
Administrator
Зарегистрирован: 03-05-2018
Сообщений: 4,586

Re: ESP32 с видеовыходом VGA. На базе библиотеки FABGL.

Даже 1280x720_60Hz, Esp32 показывать может! Однако, в таком режиме, процессор вычислять уже другое не способен. Или нужно юзать оба ядра процессора, что довольно сложно.

Не в сети

#13 13-04-2021 05:11:58

IvanAltay
Administrator
Зарегистрирован: 03-05-2018
Сообщений: 4,586

Re: ESP32 с видеовыходом VGA. На базе библиотеки FABGL.

NTP часы.

16182664457846532751139514442006.jpg

Не в сети

#14 13-04-2021 05:36:35

IvanAltay
Administrator
Зарегистрирован: 03-05-2018
Сообщений: 4,586

Re: ESP32 с видеовыходом VGA. На базе библиотеки FABGL.

Код NTP часов.

#include <stdio.h>
#include "fabgl.h"
fabgl::VGAController DisplayController;
fabgl::Canvas        canvas(&DisplayController);
#define DOUBLEBUFFERING 1
char hour[9]="";
char minute[9]="";
char DAT[9]="";
char MES[9]="";
char GOD[9]="";
char sec[9]="";
char str1[30]="";
#include <WiFi.h>
#include <time.h>
char* time_output="";
char* lastNTPtime1="";
const char* ssid = "tele2";
const char* password = "ta20242024";
const char* NTP_SERVER = "ch.pool.ntp.org";
const char* TZ_INFO    = "MSK-6MSD,M3.5.0/2,M10.5.0/3";  //"MSK-6MSD,M3.5.0/2,M10.5.0/3"-ЧАСОВОЙ ПОЯС АЛТАЙСКОГО КРАЯ
// enter your time zone (https://remotemonitoringsystems.ca/time-zone-abbreviations.php)
tm timeinfo;
time_t now;
long unsigned lastNTPtime;
unsigned long lastEntryTime;
int  delayInMillis = 0;
int  idle = 0;
//////////////////////////////
void setup()
{
Serial.begin(115200);
  WiFi.begin(ssid, password);
  int counter = 0;
  while (WiFi.status() != WL_CONNECTED) {
    delay(200);
    if (++counter > 20) ESP.restart();
    Serial.print ( "." );
  }
  Serial.println("\n\nWiFi connected\n\n");
  configTime(0, 0, NTP_SERVER);
  // See https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv for Timezone codes for your region
  setenv("TZ", TZ_INFO, 1);
  if (getNTPtime(10)) {  // wait up to 10sec to sync
  } else {
    Serial.println("Time not set");
    ESP.restart();
  }
  showTime(&timeinfo);
  lastNTPtime = time(&now);
  lastEntryTime = millis();
  // avoid garbage into the UART
  delay(500);  
DisplayController.begin();

DisplayController.setResolution(VGA_640x200_70Hz);
// DisplayController.setResolution(VGA_320x200_75Hz);
canvas.setGlyphOptions(GlyphOptions().FillBackground(false).DoubleWidth(3));
}
void loop()
{
getNTPtime(1000);
  showTime(&timeinfo);
 canvas.clear();
  //canvas.setPenColor(Color::Yellow);
  canvas.setPenColor(Color::Green);
canvas.setGlyphOptions(GlyphOptions().FillBackground(false).DoubleWidth(3));
canvas.drawTextFmt(0, 8, hour);
canvas.drawText(60,8,":");
canvas.drawTextFmt(100, 8, minute);
canvas.drawText(160,8,":");
canvas.drawTextFmt(160, 8, sec);
canvas.setGlyphOptions(GlyphOptions().FillBackground(false).DoubleWidth(2));
canvas.drawTextFmt(0, 1, hour);
canvas.drawText(60, 1,":");
canvas.drawTextFmt(100, 1, minute);
canvas.drawText(160, 1,":");
canvas.drawTextFmt(160, 1, sec);
/////////////////////////////////
canvas.setPenColor(Color::Yellow);
canvas.setGlyphOptions(GlyphOptions().FillBackground(false).DoubleWidth(3));
canvas.drawTextFmt(400, 8, DAT);
canvas.drawText(460,8,"/");
canvas.drawTextFmt(500, 8, MES);
canvas.drawText(520,8,"/");
canvas.drawTextFmt(550, 8, GOD);
canvas.setGlyphOptions(GlyphOptions().FillBackground(false).DoubleWidth(2));
canvas.drawTextFmt(400, 1, DAT);
canvas.drawText(460, 1,"/");
canvas.drawTextFmt(500, 1, MES);
canvas.drawText(520, 1,"/");
canvas.drawTextFmt(550, 1, GOD);
 canvas.setGlyphOptions(GlyphOptions().FillBackground(false).DoubleWidth(1));
canvas.drawTextFmt(0, 50, str1);
canvas.setPenColor(Color::Red);
canvas.drawTextFmt(220, 80, "IvanAltay (C) 13.04.2021");
if (DOUBLEBUFFERING)
    canvas.swa pBuffers();
}
bool getNTPtime(int sec) {
  {
    uint32_t start = millis();
    do {
      time(&now);
      localtime_r(&now, &timeinfo);
      Serial.print(".");
      delay(10);
    } while (((millis() - start) <= (1000 * sec)) && (timeinfo.tm_year < (2016 - 1900)));
    if (timeinfo.tm_year <= (2016 - 1900)) return false;  // the NTP call was not successful
    Serial.print("now ");  Serial.println(now);
      char time_output[30];
    strftime(time_output, 30, "%a  %d-%m-%y %T", localtime(&now));
   sprintf(str1, "%s", time_output);
    Serial.println(time_output);
    Serial.println();
  }
  return true;
}
///////////////////////////
void showTime(tm *localTime) {
//
sprintf(hour, "%d",localTime->tm_hour);
sprintf(minute, "%d",localTime->tm_min);
sprintf(sec, "%d",localTime->tm_sec);
sprintf(DAT, "%d",localTime->tm_mday);
sprintf(MES, "%d",localTime->tm_mon + 1);
sprintf(GOD, "%d",localTime->tm_year + 1900);  
}

Не в сети

Подвал раздела

Работает на FluxBB (перевод Laravel.ru)