網頁

2018年1月31日 星期三

OpenQSignage開源迷你電子看板(Arduino LCD動畫胸牌)

我有沒有聽錯!3.3V / 8MHz 8bit MCU的Arduino Mini Pro竟然可以拿來做「動畫胸牌」,不是LED的那種(圖一左圖),而是64K彩色LCD的那種(圖一右圖)。它不只可以秀文字還可秀影像,並可選擇多張自己喜歡的影像及輸入自己想要的文字後再下載,更誇張的是竟然沒有用到SD卡,總成本不到二個小朋友(<NT$500)。各位Maker,這是真的!而最主要的功臣就是來揚科技(Lyontek Inc.)的那顆PSRAM(Pseudo Static Random Access Memory)(圖一右圖綠色板子上正中間那顆八隻腳的IC),到底什麼是PSRAM,又要怎麼才能完成這項作品,就讓我們繼續看下去。

圖一、左圖為傳統LED胸牌,右圖為本開源專案迷你電子看板【OpenQSignage】

OpenQSignage開源迷你電子看板(Arduino LCD動畫胸牌)#1_硬體設計說明

1.硬體設計


本來以為這次的專案沒三兩下就可以收工,結果一波N折,搞了快一個月才全部收工。最早想用Arduino Nano當作主控板,它自帶USB可直接下載程式又可當UART通訊埠,於是買了兩片回來試。當控制電路板都焊好了,才發現LCD模組和PSRAM都是3.3V介面,天真的我以為將LCD及PSRAM接上3.3V,Arduino Nano接5V,I/O介面就會通,寫了一些測試程後,才證實「待誌不是憨人所想的這麼簡單」(台語發音),完全不會工作,突然覺得有點崩潰。

後來又想買個3.3V和5.0V位準轉換板來解決,但發現接腳太多實在不好處理,所以也放棄這個念頭。不死心的我又在網路查了一下發現Arduino的主晶片Atmel Mega 328P是可以支援3.3V的,找到一些文章教人如何把Arduino Nano 或Pro Mini改成3.3V,但發覺改板太麻煩,只好乖乖重買板子。可是天不從人願,Arduino Nano沒有3.3V版,要Pro Mini才有,而且時脈速度要降一半,只剩8MHz,更慘的是還要另外買USB轉UART的板子才能下載程式和資料,真是大悲劇。老天對我的考驗還沒結束,上網買了3.3V的Arduino Pro Mini和USB轉UART(CH340G)的轉接板,收到貨後又發現USB轉UART轉接板竟是5V的板子,害我又得上網查資料自己改板子,才能順利下載程式。

好不容易終於把硬體採購都搞定後,寫了一個簡單PC程式和一小段Arduino SPI PSRAM和LCD顯示測試程式後,又發現原先Arduino  拿A4(PC4)和A5(PC5)當數位I/O用竟然無法正常工作,搞了好久一直沒解,最後只好放棄當數位I/O使用,保留下來後續I2C使用,並讓SPI PSRAM和LCD共用晶片選擇線。悲劇還沒結束,突然又發現下載完程式和顯示資料後,板子一離開USB就沒電了,PSRAM內的資料就全沒了,這樣等於就白作工了,所以只好再次上網找鋰電池和充電板來解決持續供電這個問題,結束硬體的問題。

希望透過本文可以讓後續想實作這個專案的人不會經過這麼多波折,按圖施工,一次就能完美達陣。

1.1 零件清單


本專案所需材料如下:

A. Arduino Pro Mini 3.3V / 8MHz 一片 (注意一般是5V / 16MHz,不要買錯)
B. USB轉UART板 (3.3V) 一片 (買不到的請參考  節CH340G如何將5V板子改成3.3V板子)
C. 2.2吋LCD模組 一片 (必須是解析度176*220,ILI9225驅動,8 Bit Bus)
D. 來揚科技SPI PSRAM LY68L6400模組 一片(8MByte, 採購請洽來揚科技)
E. 鋰電池充放電板 一片 (建議輸出電壓不要升壓成5V那種)
F. 鋰電池 一顆 (3.7V, 800mA, 電池容量可依需求增減)
G. 洞洞板 一片 (手工焊接用,未來可改成印刷電路板)
H. LED,220Ω 各一顆 (作為指示燈)
I. 按鍵,10kΩ 各一顆 (作為操作用)

以上除D項是來揚科技提供外,價格大約NT$10(來揚告知的參考價,但不知要多少量才有這個價格),其餘在淘寶或露天上都可輕鬆買到,價錢會依不同的採購管道而有不同,整體來說材料費大約NT$300 ~ 500之間。

1.2 電路圖


本次專案希望成品可真的別在胸口,所以可能不方便用麵包板插線完成,加上此次電路連結頗為複雜,建議實作時焊接要特別注意,以免不慎短路造成零件損壞。當然如果後續有機會(友情斗內)製作成印刷電路板時,就會更方便製作。完整電路圖請參閱圖三。另外本次板子在焊接時跳線太多,所以就不提供焊接參考圖,焊接好的成品圖及各個零件參考位置圖請參考圖四。

圖三、硬體電路圖

圖四、成品配置、焊接及組裝參考圖

1.3 主要零件使用補充說明


為了後續看程式能更了解重要零件用法,這裡補充說明一下。

A. Arduino Pro Mini

Arduino Pro Mini是其家族中板子最小的,但因為它是開放性設計,所以各家廠商會依自己需要增減一些I/O(如A4, A5, A6, A7),而側面保留UART的腳位及順序可能也會有些不同,所以在採購板子及USB轉UART轉接板時要注意,不然就得像本專案一樣自己加一塊轉板或焊一條專用排線,方便下載程式及影像內容。

另外本次選用的板子是3.3V版本,這是指主晶片的工作電壓,而不是外加電壓。真正的外接電源是從RAW的腳位接入,板子上有電壓調節器,自動會降成3.3V給主晶片用,同時可透過VCC(3V3)接腳引到其它需要3.3V的小零件。因此RAW輸入電壓可從3.35到12V,所以不管是USB(5V)或鋰電池(3.7~4.2V)甚至拿9V電池供電都沒問題。

B. LCD模組

上次OpenQCam(http://omnixri.blogspot.tw/2017/12/openqcam.html)用的是SPI(四線式)通訊方式的LCD模組,為了加快顯示資料更新速度,所以此次選用的是8bit資料匯流排形式的模組。本來這塊模組是為了另一個開源案子準備的,所以特別挑選2.2吋176*220像素解析度的,這個解析度可能有點奇怪,後續若大家有興趣的話亦可改成常用的QVGA (320*240像素)規格。一般上網買此類模組時可能會附上Arduino UTFT的函式庫方便開發,但它支援太多類型,所以驅動方式很難一下看懂,在這個專案我把它簡化到只支援ILI9225驅動晶片且橫式顯示,使大家更容易理解LCD工作原理。

目前這款LCD支援65,536(俗稱64K)色顯示,每點像素以RGB565表示,就是紅色5個位元加上綠色6個位元加上藍色5個位元,合計16位元來表示,等於2個位元組,所以一個畫面共需176 * 220 * 2 = 77,440個位元組,而LCD模組已自帶這些繪圖記憶體(GRAM),只要寫入後模組會自動刷新屏幕,直到有新值寫入或斷電後消失。

再來要說明LCD原始座標系統和顯示時的座標系統之差異,這部份一定要搞懂不然就無法正確顯示。首先說明原始LCD模組預設是直式顯示,暫存器0x03的設定值為0x1030,其座標系統原點(0,0)在左上角,掃描方向由左而右,由上而下,如圖五左圖所示。為了當胸牌(橫式顯示)使用,所以我們要將掃描方向設定改成由上而下,由右至左,這裡指的方向是對應原始定義(黃線標示),而暫存器0x03的值就必須改成0x1028。從圖五左圖中可看出綠色座標軸和洋青色座標值是我們習慣在PC上顯示的座標系統,而黃色線座標軸和洋紅色座標值則為LCD模組真正的座標值,因此使用上我們必須進行轉換。

另外為了加速畫面更新速度,LCD模組允許只對特定視窗(Window)局部進行更新,如圖五右圖中綠色方框,使用前要設定視窗起始座標(x1,y1)、結束座標(x2,y2)及預計開始填值的起點(sx,sy)。特別注意這裡用的座標都是原始定義座標,使用時填入的資料數量(影像長寬)一定要和視窗大小相同,不然圖像顯示時可能會產生捲繞或斜切問題。

LCD相關控制時序,請參考文獻[3]。或者直接對照程式碼及註解作進一步理解。

圖五、LCD預設(左)及顯示(右)座標系統及掃描方向

C. Lyontek PSRAM

終於到了本專案最重要的核心元件介紹了,首先感謝來揚科技(Lyontek)贊助一片SPI PSRAM(Pseudo Static Random Access Memory)模組,就是圖四左下圖中那塊綠色板子,上面放了一顆LY68L6400,還有一顆超亮綠色的電源指示燈(為了避免影響拍照,只好用黑膠帶把它貼住)。

什麼是PSRAM呢?我們一般只聽過靜態記憶(Static Random Access Memory, SRAM)和動態記憶體(Dynamic Random Access Memory, RAM)。SRAM速度快,價格高,耗電少,一般MCU中都會帶一些,以Arduino Pro Mini上的那顆Atmel Mega328P來說只有2K位元組的SRAM。而DRAM價格超便宜,但控制電路很複雜,且較SRAM耗電(因為要一直刷新),一般桌機或手機動不動就有1G位元組(或更高),但其控制電路非常複雜,而且要定時刷新(reflash)不然資料就不見了,所以通常要較高等級的CPU才有辦法使用,Arduino這種8bit MCU根本不可能使用。於是史提芬周(星爺)說:「爭什麼爭,把瀨尿蝦和牛丸摻在一起做瀨尿牛丸不就好了。」,所以就有廠商把DRAM包裝成SRAM,把自動刷新機制都作進IC中,這樣用的人就沒感覺,好像在用SRAM,直接讀寫就好,連小型MCU也都可以用,這樣的產品就被稱為Pseudo(假) SRAM。

後來又有廠商為了方便小型MCU沒這麼多接腳可用,將其改成用SPI串列通訊方式讀寫,只需四條線就可使用,大大滿足小型MCU使用者,此次來揚科技提供的這項產品就屬於此類。LY68L6400是一顆64Mbit (8MByte) PSRAM,比Arduino Pro Mini的記憶體(SRAM)大了4,000倍,而且一個十元硬幣(來揚提供,但我不知道要多大量才有這個價錢)就搞定。它的SPI低速時脈可達33MHz,比起Arduino Pro Mini (3.3V版本) SPI的4MHz(因為最小除頻為2)快了八倍多。它還提供了高速模式可達100MHz(新版144MHz),若用QPI模式可再加快三倍(相當四組SPI同時工作),可惜我們用不到,享受不到這種快感。就算MCU來的及,LCD也不給力,所以就只能拿法拉利來當裕隆開了。

話說回來這麼大的儲存空間是要作什麼用,要用到何年何日才用的完,這時候本專案就充分發揮了這項產品的用處了。從前面LCD介紹中我們可得知光一張畫面就要77.4K位元組,而Ardiuno的2K位元組小得連一張影像都裝不下,此時PSRAM就幫了大忙,隨便存個近百張影像也沒問題。使用時我們從PC端用UART將顯示的排程及影像傳送到Arduino中,每收到一個位元組就立即寫入PSRAM,當從PSRAM讀取到一個位元組時也是馬上寫入LCD GRAM中,這樣Arduino就幾乎沒用到什麼記憶體(只有少數變),使得本專案得已實現大量彩色影像播放功能。

這麼好用的產品,難道都沒有缺點嗎?個人覺得斷電資料就消失是最大問題,所以必須搭配電池持續供電,而這樣可能會增加一些電池及充電模組的成本。那或許會有人問,那我用SD卡儲存不就不會有這個問題嗎?我想看在這顆PSRAM才十元,比起SD卡便宜太多,如果使用的場合經常需要重新更新顯示內容的情況下,我想這項產品仍是不錯的選擇,至於要如何使用就留給各位發揮創意了。

D. 充電電池及模組

為了讓本案能真的當成LCD動畫胸牌使用,且避免PSRAM斷電後資料消失,所以額外增加了鋰電池及充電模組。一般鋰電池有的有帶保護板防止過充,有的靠充電模組自行偵測,因此選用時要注意搭配問題。一般充電模組輸入可選用Micro USB插座類型,方便取得5V電源來源。而輸出部份一般有兩種,一種是把鋰電池輸出電壓直接升壓到5V,像是行動電源作法。另一種是鋰電池電壓多少就輸出多少。原則上這兩種都可使用,本專案是選用後者。

另外大家可能會關心的是電池容量到底要多少才夠用,以實測未優化情況,800mAh的電池充滿電後,在自動連續播放模式(MCU持續讀取PSRAM再寫入LCD中,相當於耗電最大情況)下,大約可撐七個小時。這裡可能還有很多電力使用優化作法,比方說調整LCD背光、加強播放間隔時間等等,有待各位協助。

E. USB轉UART轉換板

由於Arduino Pro Mini不像Nano有自帶USB轉UART,所以必須使用額外的USB轉UART轉板。網路上有很多種板子,其中CH340G算是比較常見的(Nano上就是用這顆IC),驅動程式可和Nano共用,不用另外安裝。但各家提供的腳位排列順序都不同,有些還沒有RST腳位,通常都是5V介面的,但有些有設計切換開關或可跳接成3.3V的UART。本來上網買的時候已有指定3.3V版,但收到時發現不是可切換的那種(賣家圖文不符),還好CH340G可支援3.3V,所以只好手工改板。完整手冊可參考文獻[2]。

首先拆掉正面連接USB5V和CH340G VCC(pin 16)的電容,再來將VCC和V3(pin 4)連在一起,讓外部(Arduino Pro Mini VCC)進來的3.3V可送至CH340G的VCC,如圖六下圖所示。接著切斷背面5V 的線,另外將USB送進來的5V直接連線送到外面(排針),如圖六上圖所示,令5V不要經過板子,也不要供電給CH340G,如此即完成USB轉3.3V UART的轉接板。

為方便直接連到Arduino Pro Mini,所以這裡另外手焊了一塊轉板,如圖六右半邊圖所示。這裡多設計了一個跳接線(Jumper),主要是為了切換Arduino Pro Mini的RAW是由誰供電,當短路時由USB供電(5V)到Arduino Pro Mini,再由Arduino Pro Mini送回3.3V給CH340G,若拔掉跳接線(開路)時,則改由鋰電池供電(3.7 ~ 4.2V)。請特別注意,不可跳接線短路時又由鋰電池供電,這樣會造成短路。這裡的USB 5V並不是用來對電池充電的。

圖六、(左上及左下)USB轉UART轉接板改成3.3V版本參考圖,(右)手工轉接板

OpenQSignage開源迷你電子看板(Arduino LCD動畫胸牌)

OpenQSignage開源迷你電子看板(Arduino LCD動畫胸牌)#1_硬體設計說明

OpenQSignage開源迷你電子看板(Arduino LCD動畫胸牌)#2_Arduino程式說明

OpenQSignage開源迷你電子看板(Arduino LCD動畫胸牌)#3_PC程式說明

OpenQSignage開源迷你電子看板(Arduino LCD動畫胸牌)#2_Arduino程式說明

2. Arduino程式


當要下載程式到Arduino前請記得先關閉本專案PC端程式,以免佔用通訊埠(COM)導致程式無法下載。當按下板上重置鍵後,首先會看到LCD螢幕出現十六道彩色條紋,等待一秒後清除畫面(黑屏),接著開始等待PC端下載排程及影像資料。待完成下載後,會進入自動播放模式,即依排程設定之時間、顯示模式來顯示圖框及橫幅(文字)。
當SW1按住超過三秒時,會交換自動播放或者是手動切換模式。當按鍵時間小於三秒時會視為手動切換模式。在手動切換模式時,不依排程設定,直接秀出圖框影像及橫幅文字。再按一下則切換到下一組,全部輪完就重頭開始。

不管是自動或手動切換模式,當UART收到PC端傳來命令時,會馬上停下排程,改執行接收影像任務。

目前本專案僅支援三組圖框影像及三組橫幅文字,由於PSRAM空間還很大,未來可考慮改成支援更多影像版本。目前PSRAM的空間使用如圖七所示。

圖七、PSRAM空間使用圖

完整程式請參考下方說明,或直接到 https://github.com/OmniXRI/OpenQSignage 下載。

#include <SPI.h> // 引入驅動SPI函式庫,應用於SPI PSRAM

// LCD 顯示模式
#define LCD_DISP_FIXED  0 // 固定顯示
#define LCD_DISP_BLINK  1 // 交替閃爍
#define LCD_DISP_NONE   2 // 不顯示

#define FRAME_TOTAL_AMOUNT  6  // 圖像(含圖框及橫幅)
#define FRAME_HEADER_SIZE   10 // 排程資料位元數

// LCD常用色定義 (16bit, RGB565, 65536色)
#define VGA_BLACK    0x0000  // 黑色
#define VGA_WHITE    0xFFFF  // 白色
#define VGA_RED      0xF800  // 紅色
#define VGA_GREEN    0x0400  // 綠色
#define VGA_BLUE     0x001F  // 藍色
#define VGA_SILVER   0xC618  // 銀(亮灰)色
#define VGA_GRAY     0x8410  // 灰色
#define VGA_MAROON   0x8000  // 栗(暗紅)色
#define VGA_YELLOW   0xFFE0  // 黃色
#define VGA_OLIVE    0x8400  // 橄欖(暗綠)色
#define VGA_LIME     0x07E0  // 青檸(亮綠)色
#define VGA_AQUA     0x07FF  // 水藍(亮藍)色
#define VGA_TEAL     0x0410  // 水鳥(暗監綠)色
#define VGA_NAVY     0x0010  // 海軍(暗藍)色
#define VGA_FUCHSIA  0xF81F  // 紫紅色
#define VGA_PURPLE   0x8010  // 紫色

// 主要I/O設定
int PIN_SW1 = 2;       // 按鍵(SW)1
int PIN_LED1 = 3;      // 指示燈(LED)1

// 2.2吋 LCD 模塊I/O設定(176*220像素,ILI9225驅動晶片,8 Bit匯流排)
int PIN_LCD_CS = 10;   // LCD晶片選擇線(和SPI PSRAM晶片選擇線PIN_PSRAM_CE共用)
int PIN_LCD_RS = 9;    // LCD命令暫存器選擇線
int PIN_LCD_WR = 8;    // LCD寫入線
int PIN_LCD_DB0 = 14;  // LCD資料匯流排位元0, Arduino PC0/A0
int PIN_LCD_DB1 = 15;  // LCD資料匯流排位元1, Arduino PC1/A1
int PIN_LCD_DB2 = 16;  // LCD資料匯流排位元2, Arduino PC2/A2
int PIN_LCD_DB3 = 17;  // LCD資料匯流排位元3, Arduino PC3/A3
int PIN_LCD_DB4 = 4;   // LCD資料匯流排位元4, Arduino PD4
int PIN_LCD_DB5 = 5;   // LCD資料匯流排位元5, Arduino PD5
int PIN_LCD_DB6 = 6;   // LCD資料匯流排位元6, Arduino PD6
int PIN_LCD_DB7 = 7;   // LCD資料匯流排位元7, Arduino PD7

// 來揚科技(Lyontek) SPI PSRAM LY68L6400 I/O設定
int PIN_PSRAM_CE = 10;  // 晶片選擇線, Arduino PB2(和LCD晶片選擇線PIN_LCD_CS共用)
int PIN_PSRAM_SI = 11;  // 串列資料輸入線, Arduino PB3
int PIN_PSRAM_SO = 12;  // 串列資料輸出線, Arduino PB4
int PIN_PSRAM_SCK = 13; // 時脈線, Arduino PB5

int SW1State = 0;       // 按鍵1狀態
int color_table[16] = {VGA_BLACK, VGA_WHITE, VGA_RED, VGA_GREEN,
                       VGA_BLUE,  VGA_SILVER, VGA_GRAY, VGA_MAROON,
                       VGA_YELLOW, VGA_OLIVE, VGA_LIME, VGA_AQUA,
                       VGA_TEAL, VGA_NAVY, VGA_FUCHSIA, VGA_PURPLE}; // 色彩索引表
int auto_loop = 0;       // 0為手動操作,非0為自動依排程播放
int frame_index = 0;     // 目前播放圖框(含橫幅)編號
int frame_updated = 0;   // 0為LCD尚未更新顯示內容,1為已更新
int download_finish = 0; // 下載顯示及排程資料完成
unsigned long startTime; // 計時器起始時間
unsigned long duration;  // 計時器經過時間

// 顯示排程資料格式
// [0] 圖框寬度高位元組(d15 ~ d8)
// [1] 圖框寬度低位元組(d0  ~ d7)
// [2] 圖框高度高位元組(d15 ~ d8)
// [3] 圖框高度低位元組(d0  ~ d7)
// [4] LCD顯示起始位置X高位元組(d15 ~ d8)
// [5] LCD顯示起始位置X低位元組(d0  ~ d7)
// [6] LCD顯示起始位置Y高位元組(d15 ~ d8)
// [7] LCD顯示起始位置Y低位元組(d0  ~ d7)
// [8] LCD顯示模式,0:固定不變,1:交替閃爍,2:不顯示
// [9] LCD顯示計時器, 0 ~ 255 表示 0.0 ~ 25.5 秒
char frame_header[10];  // 顯  

// Arduino初始化程式
void setup() { 
  // 初始化主要I/O
  pinMode(PIN_LED1, OUTPUT);  // 設定LED腳位為輸出
  pinMode(PIN_SW1, INPUT);    // 設定SW腳位為輸入
 
  // 初始化UART通訊埠(COM Port)
  Serial.begin(57600);        // 設定通訊鮑率為57,600bps
 
  // 初始化SPI串列埠為控制來揚科技(Lyontek) SPI PSRAM LY68L6400
  //  pinMode(PIN_PSRAM_CE, OUTPUT); // 和LCD晶片選擇線共用,所以不用重覆設定
  SPI.begin();                          // 啟動SPI
  SPI.setBitOrder(MSBFIRST);            // 設定SPI為高位元先送模式
  SPI.setDataMode(SPI_MODE0);           // 設定SPI為模式0(CPOL,CPHA皆為零,即資料SI/SO備妥後CLK再以正緣觸發)
  SPI.setClockDivider(SPI_CLOCK_DIV2);  // 為外頻再除以2,3.3V Arduino外頻為8MHz,所以SPI時脈為4MHz
 
  // 初始化2.2吋LCD模塊相關I/O
  pinMode(PIN_LCD_CS, OUTPUT);   // 設定晶片選擇線為輸出,和SPI PSRAM晶片選擇線共用
  pinMode(PIN_LCD_RS, OUTPUT);   // 設定暫存器選擇線為輸出
  pinMode(PIN_LCD_WR, OUTPUT);   // 設定寫入線為輸出
  pinMode(PIN_LCD_DB0, OUTPUT);  // 設定資料匯流排DB0為輸出
  pinMode(PIN_LCD_DB1, OUTPUT);  // 設定資料匯流排DB1為輸出 
  pinMode(PIN_LCD_DB2, OUTPUT);  // 設定資料匯流排DB2為輸出 
  pinMode(PIN_LCD_DB3, OUTPUT);  // 設定資料匯流排DB3為輸出 
  pinMode(PIN_LCD_DB4, OUTPUT);  // 設定資料匯流排DB4為輸出 
  pinMode(PIN_LCD_DB5, OUTPUT);  // 設定資料匯流排DB5為輸出 
  pinMode(PIN_LCD_DB6, OUTPUT);  // 設定資料匯流排DB6為輸出 
  pinMode(PIN_LCD_DB7, OUTPUT);  // 設定資料匯流排DB7為輸出 
  ILI9225_init();                // LCD工作暫存器初始化
  ILI9225_LCD_Test();            // LCD顯示測試
}

// Arduino無限循環程式段
void loop() {
  if (digitalRead(PIN_SW1) == HIGH) {    // 當SW1被按下時
    startTime = millis();                // 開始啟動計時器(ms)
   
    while(digitalRead(PIN_SW1) == HIGH); // 等待按鍵放開

    duration = millis() - startTime;     // 計算按鍵按下時間長度
   
    if(duration > 3000){                 // 若按超過3秒
      if(auto_loop == 1)                 // 若目前是自動播放模式
        auto_loop = 0;                   // 則切換到手動切換模式
      else
        auto_loop = 1;                   // 若否則切換到自動模式
    } 
    else{                                // 若按鍵按下不到3秒
      auto_loop = 0;                     // 則切換到手動切換模式
      frame_updated = 0;                 // 畫面設為未更新
    }   
  }   

  // 若顯示資料已下載完成且為手動切換模式且畫面尚未更新
  if (download_finish != 0 && auto_loop == 0 && frame_updated == 0) {
     ShowFrame(frame_index, 0);          // 正常顯示圖框內容到LCD(偶數為圖框)
     frame_index++;                      // 目前圖框編號加一
     ShowFrame(frame_index, 0);          // 正常顯示橫幅內容到LCD(奇時為橫幅)
     frame_updated = 1;                  // 設定LCD顯示已更新
     frame_index++;                      // 圖框(橫幅)編號加一
   
     if(frame_index >= 6)                // 圖框(橫幅)編號大於等於6
       frame_index = 0;                  // 圖框(橫幅)編號歸零
  }
  else if (download_finish != 0 && auto_loop == 1) { // 若顯示資料已下載完成且為自動播放模式
     ShowFrame(frame_index, 0);          // 正常顯示圖框內容到LCD(偶數為圖框)
     frame_index++;                      // 圖框編號加一
     GetFrameHeader(frame_index);        // 取得圖框(橫幅)顯示排程資料
     frame_updated = 0;                  // 設定LCD顯示未更新
     startTime = millis();               // 啟動計時器
    
     while((millis() - startTime) < (frame_header[9]*100)){ // 若計時器未達排程設定時間
       switch(frame_header[8])               // 依顯示模式執行顯示內容
       {
        case LCD_DISP_FIXED:                 // 固定顯示模式
               if(frame_updated == 0)        // 若LCD尚未更新
                 ShowFrame(frame_index, 0);  // 則正常顯示目前圖框編號內容
                 
               frame_updated = 1;            // 設定為LCD已更新內容
               break;
        case LCD_DISP_BLINK:                 // 交替顯示模式
               ShowFrame(frame_index, 0);    // 正常顯示目前圖框(橫幅)編號內容
               delay(500);                   // 延時500ms
               ShowFrame(frame_index, 1);    // 反相顯示目前圖框(橫幅)編號內容
               delay(500);                   // 延時500ms
               break;
        case LCD_DISP_NONE:                  // 不顯式模式(直接跳過)
        default:
             break;
       }
     }
    
     frame_index++;                      // 圖框(橫幅)編號加一
   
     if(frame_index >= 6)                // 圖框(橫幅)編號大於等於6
       frame_index = 0;                  // 圖框(橫幅)編號歸零
  }
 
  int rx_data;                      // 儲存接收到的位元組
 
  if (Serial.available() > 0) {     // 假若UART未接收到任何資料則略過
    download_finish = 0;            // 清除已下載完成旗標
   
    rx_data = Serial.read();        // 取得動作命令
   
    if(rx_data == 0xA5){            // 若命令為0xA5
      digitalWrite(PIN_LED1, HIGH); // 則點亮LED1
      delay(100);                   // 延時100ms
    } 
    else if(rx_data == 0xB4){       // 若命令為0xB4
      ILI9225_Clr_Screen();         // 則清除LCD顯示內容(屏幕全黑)
      digitalWrite(PIN_LED1, LOW);  // 並熄滅LED1
    } 
    else if(rx_data == 0xC3){       // 若命令為0xC3則開始接收PC透過UART送過來的資料       
      for(int f_idx=0; f_idx<FRAME_TOTAL_AMOUNT; f_idx++){ // 預計接收六組資料(圖框加橫幅各三組)       
        digitalWrite(PIN_LCD_CS, LOW);  // SPI PSRAM 晶片選擇線設為低電位
        // 設定 SPI PSRAM寫入起始位置
        SPI.transfer(0x02);             // 寫入PSRAM寫入命令0x02
        SPI.transfer((f_idx+1)*2);      // 寫入PSRAM位址 bit 23 ~ 16,每個圖框(橫幅)儲存間隔20000h
        SPI.transfer(0);                // 寫入PSRAM位址 bit 15 ~ 8
        SPI.transfer(0);                // 寫入PSRAM位址 bit 7 ~ 0          
    
        // 取得圖框排程資料
        for(int i=0; i<FRAME_HEADER_SIZE; i++){  // 預計接收10位元組資料
          while(!Serial.available());            // 等待UART已備妥接收到資料信號
         
          rx_data = Serial.read();               // 接收一個位元組資料
          frame_header[i] = (char)rx_data;       // 將資料讀入frame_header中指定位置
          SPI.transfer(frame_header[i]);         // 將讀入資料寫入PSRAM(位址自動加一)
        }
       
        Serial.write(0x5A);                      // 透過UART回傳0x5A回應已接收到資料
       
        // 依接收到的資料,設定顯示圖框(橫幅)的寬(fw)、高(fh)及起始位置(sx,sy)       
        int fw = (frame_header[0] * 256) + (unsigned char)frame_header[1];
        int fh = (frame_header[2] * 256) + (unsigned char)frame_header[3];
        //int sx = (frame_header[4] * 256) + frame_header[5]);
        //int sy = (frame_header[6] * 256) + frame_header[7];    

        // PC指定的LCD顯示位置(SXdisp,SYdisp)與LCD GRAM和起始位置(SXgram,SYgram)的換算公式
        // SXgram = 176 - SYdisp - 1; LCD width = 176
        // SYgram = SXdisp;       
        // SXlcd,  SYlcd  (Left,Top)-(Right,Bottom), Frame (0  ,0)-(219,175)
        // SXgram, SYgram (Left,Top)-(Right,Bottom), GRAM  (175,0)-(0,  219)
        // x1 <= sx <= x2, y1 <= sy <= y2, if x1>x2 or y1>y2 must be swap
        // ILI9225_SetSY(x1,y1,x2,y2,sx,sy);  // 設定LCD GRAM讀寫區域大小

        if(f_idx % 2 == 0)                       // 若圖框編號為偶數
          ILI9225_SetXY(0, 0, 175, 219, 175, 0); // 則設定顯示區域為(0, 0, 175, 219, 175, 0)
        else                                     // 若為奇數則為橫幅 
          ILI9225_SetXY(0, 0, 35, 219, 35, 0);   // 則設定顯示區域為(0, 0, 35, 219, 35, 0)
         
        digitalWrite(PIN_LCD_CS, LOW);  // SPI PSRAM 晶片選擇線設為低電位
        // 設定 SPI PSRAM寫入起始位置
        SPI.transfer(0x02);             // 寫入PSRAM寫入命令0x02
        SPI.transfer((f_idx+1)*2);      // 寫入PSRAM位址 bit 23 ~ 16,每個圖框(橫幅)儲存間隔20000h
        SPI.transfer(0);                // 寫入PSRAM位址 bit 15 ~ 8
        SPI.transfer(10);               // 寫入PSRAM位址 bit 7 ~ 0,圖框資料從位置10開始
   
        for(int i=0; i<fh; i++){        // 設定迴圈數為圖框高度
          for(int j=0; j<fw*2; j++){    // 設定迴圈數為圖框寬度乘2,每個點為2個位元組(RGB565)
            while(!Serial.available()); // 等待UART已備妥接收到資料信號
         
            rx_data = Serial.read();           // 接收一個位元組資料
            ILI9225_WR_Data8((char) rx_data);  // 寫一個位元組資料到LCD繪圖記憶區(GRAM)先高再低位元組
            SPI.transfer((char) rx_data);      // 寫一個位元組資料到SPI PSRAM
          }

          Serial.write(0x5B);                  // 透過UART回傳0x5B回應已接收到資料
        }
     
        digitalWrite(PIN_LCD_CS, HIGH);        // 將晶片選擇線設為高電位結束接收圖框資料
        digitalWrite(PIN_LED1, HIGH);          // 點亮LED1表目前圖框已下載完成
      }
     
      frame_index = 0;                         // 圖框編號歸零
      download_finish = 1;                     // 設定已完成接收旗標為1
      auto_loop = 1;                           // 預設下載完成後為自動依排程播放
    }
  }
}

// 讀取圖框(橫幅)排程資訊
// [in] idx 圖框(橫幅)編號
void GetFrameHeader(int idx)
{            
      digitalWrite(PIN_LCD_CS, LOW);  // SPI PSRAM 晶片選擇線設為低電位
      // 設定 SPI PSRAM讀取起始位置
      SPI.transfer(0x03);             // 寫入PSRAM低速讀取命令0x03
      SPI.transfer((idx+1)*2);        // 寫入PSRAM位址 bit 23 ~ 16,每個圖框(橫幅)儲存間隔20000h
      SPI.transfer(0);                // 寫入PSRAM位址 bit 15 ~ 8
      SPI.transfer(0);                // 寫入PSRAM位址 bit 7 ~ 0
         
      for(int i=0; i<FRAME_HEADER_SIZE; i++){
        frame_header[i] = SPI.transfer(0);  // 從SPI PSRAM讀取資料寫入frame_header對應位置
      }
     
      digitalWrite(PIN_LCD_CS, HIGH);  // SPI PSRAM 晶片選擇線設為高電位
}

// 顯示圖框(橫幅)於LCD屏幕上
// [in] idx 圖框(橫幅)編號
// [in] inv 0為正常顯示,1為色彩反相顯示
void ShowFrame(int idx, int inv)
{
      digitalWrite(PIN_LED1, LOW);     // 晶片選擇線設為低電位
      GetFrameHeader(idx);             // 讀取圖框(橫幅)排程資訊
     
      int fw = (frame_header[0] * 256) + (unsigned char)frame_header[1];  // 計算圖框寬度
      int fh = (frame_header[2] * 256) + (unsigned char)frame_header[3];  // 計算圖框高度
     
      if(frame_index%2 == 0)                    // 若圖框編號為偶數    
        ILI9225_SetXY(0, 0, 175, 219, 175, 0);  // 則設定顯示區域為(0, 0, 175, 219, 175, 0)
      else                                      // 若為奇數則為橫幅
        ILI9225_SetXY(0, 0, 35, 219, 35, 0);    // 則設定顯示區域為(0, 0, 35, 219, 35, 0)
      
      digitalWrite(PIN_LCD_CS, LOW);    // ILI9225_SetXY()會使晶片選擇線設為高電位,所以要重新設低電位
      // 設定 SPI PSRAM讀取起始位置
      SPI.transfer(0x03);               // 寫入PSRAM低速讀取命令0x03
      SPI.transfer((idx+1)*2);          // 寫入PSRAM位址 bit 23 ~ 16,每個圖框(橫幅)儲存間隔20000h
      SPI.transfer(0);                  // 寫入PSRAM位址 bit 15 ~ 8
      SPI.transfer(10);                 // 寫入PSRAM位址 bit 7 ~ 0,圖框資料從位置10開始
        
      for(int i=0;i<fh;i++){            // 設定迴圈數為圖框高度
        for(int j=0;j<fw*2;j++){        // 設定迴圈數為圖框寬度乘2,每個點為2個位元組(RGB565)
          char dataT = SPI.transfer(0); // 從SPI PSRAM讀取一個位元組資料        
          if(inv==1)                    // 若設定為色彩反相
            dataT = dataT ^ 0xFF;       // 則令數值反相(作XOR運算)
           
          ILI9225_WR_Data8(dataT);      // 將資料寫入LCD GRAM中                
        }
      }
     
      digitalWrite(PIN_LCD_CS, HIGH);   // 晶片選擇線設為高電位
      digitalWrite(PIN_LED1, HIGH);     // 點亮LED1  
}

//*************************************************************************************
// LCD 模塊驅動程式
// 176*220像素,65,536(2^16)色(RGB565),8 bit資料匯流排, ILI9225驅動晶片
// 完整資訊可參考http://www.displayfuture.com/Display/datasheet/controller/ILI9225.pdf
//*************************************************************************************

// LCD初始化
// 【注意】不熟悉參數設定的人請勿任意調整下列參數,以免造成LCD損壞或無法正常顯示
void ILI9225_init()
{
 digitalWrite(PIN_LCD_CS, HIGH);
 digitalWrite(PIN_LCD_RS, HIGH);
 digitalWrite(PIN_LCD_WR, HIGH); 
 delay(50);
 ILI9225_WR_Cmd_Data(0x10, 0x0000); // Power Control 1
 ILI9225_WR_Cmd_Data(0x11, 0x0000); // Power Control 2
 ILI9225_WR_Cmd_Data(0x12, 0x0000); // Power Control 3
 ILI9225_WR_Cmd_Data(0x13, 0x0000); // Power Control 4
 ILI9225_WR_Cmd_Data(0x14, 0x0000); // Power Control 5
 delay(40);
 ILI9225_WR_Cmd_Data(0x11, 0x0018); // Power Control 2
 ILI9225_WR_Cmd_Data(0x12, 0x6121); // Power Control 3
 ILI9225_WR_Cmd_Data(0x13, 0x006F); // Power Control 4
 ILI9225_WR_Cmd_Data(0x14, 0x495F); // Power Control 5
 ILI9225_WR_Cmd_Data(0x10, 0x0800); // Power Control 1
 delay(10);
 ILI9225_WR_Cmd_Data(0x11, 0x103B); // Power Control 2
 delay(50);
 ILI9225_WR_Cmd_Data(0x01, 0x011C); // Driver Output Control
 ILI9225_WR_Cmd_Data(0x02, 0x0100); // LCD AC Driving Control
 ILI9225_WR_Cmd_Data(0x03, 0x1028); // Entry Mode 螢幕橫式顯示,由左而右掃描
 ILI9225_WR_Cmd_Data(0x07, 0x0000); // Display Control 1
 ILI9225_WR_Cmd_Data(0x08, 0x0808); // Blank Period Control 1
 ILI9225_WR_Cmd_Data(0x0b, 0x1100); // Frame Cycle Control
 ILI9225_WR_Cmd_Data(0x0c, 0x0000); // Interface Control
 ILI9225_WR_Cmd_Data(0x0f, 0x0D01); // Oscillation Control
 ILI9225_WR_Cmd_Data(0x15, 0x0020); // ??
 ILI9225_WR_Cmd_Data(0x20, 0x0000); // RAM Address Set 1
 ILI9225_WR_Cmd_Data(0x21, 0x0000); // RAM Address Set 2
 ILI9225_WR_Cmd_Data(0x30, 0x0000); // Gate Scan Control
 ILI9225_WR_Cmd_Data(0x31, 0x00DB); // Vertical Scroll Control 1
 ILI9225_WR_Cmd_Data(0x32, 0x0000); // Vertical Scroll Control 2
 ILI9225_WR_Cmd_Data(0x33, 0x0000); // Vertical Scroll Control 3
 ILI9225_WR_Cmd_Data(0x34, 0x00DB); // Partial Driving Position -1
 ILI9225_WR_Cmd_Data(0x35, 0x0000); // Partial Driving Position -2
 ILI9225_WR_Cmd_Data(0x36, 0x00AF); // Horizontal Window Address -1
 ILI9225_WR_Cmd_Data(0x37, 0x0000); // Horizontal Window Address -2
 ILI9225_WR_Cmd_Data(0x38, 0x00DB); // Vertical Window Address -1
 ILI9225_WR_Cmd_Data(0x39, 0x0000); // Vertical Window Address -2
 ILI9225_WR_Cmd_Data(0x50, 0x0000); // Gamma Control 1
 ILI9225_WR_Cmd_Data(0x51, 0x0808); // Gamma Control 2
 ILI9225_WR_Cmd_Data(0x52, 0x080A); // Gamma Control 3
 ILI9225_WR_Cmd_Data(0x53, 0x000A); // Gamma Control 4       
 ILI9225_WR_Cmd_Data(0x54, 0x0A08); // Gamma Control 5
 ILI9225_WR_Cmd_Data(0x55, 0x0808); // Gamma Control 6
 ILI9225_WR_Cmd_Data(0x56, 0x0000); // Gamma Control 7
 ILI9225_WR_Cmd_Data(0x57, 0x0A00); // Gamma Control 8
 ILI9225_WR_Cmd_Data(0x58, 0x0710); // Gamma Control 9
 ILI9225_WR_Cmd_Data(0x59, 0x0710); // Gamma Control 10
 ILI9225_WR_Cmd_Data(0x07, 0x0012); // Display Control 1
 delay(50);
 ILI9225_WR_Cmd_Data(0x07, 0x1017); // Display Control 1
 digitalWrite(PIN_LCD_CS, LOW);
 ILI9225_WR_Cmd(0x22);              // Write Data to GRAM
 digitalWrite(PIN_LCD_CS, HIGH); 
}

// 設定LCD讀寫視窗範圍
// [in] x1,y1 為左上座標
// [in] x2,y2 為右下座標
// [in] sx,sy 為起始座標
// 必須符合 x1 < sx < x2, y1 < sy < y2,
//          x1 >= 0 && x2 < 176, y1 >= 0 && y2 < 220
// 座標是以直式顯示時表示,當橫式顯示時座標須逆時針轉90度
void ILI9225_SetXY(int x1, int y1, int x2, int y2, int sx, int sy)
{
 ILI9225_WR_Cmd_Data(0x36,x2); // HEX
 ILI9225_WR_Cmd_Data(0x37,x1); // HSX
 ILI9225_WR_Cmd_Data(0x38,y2); // VEY
 ILI9225_WR_Cmd_Data(0x39,y1); // VSY
 ILI9225_WR_Cmd_Data(0x20,sx); // GRAM start address low  byte
 ILI9225_WR_Cmd_Data(0x21,sy); // GRAM start address high byte 

 digitalWrite(PIN_LCD_CS, LOW);
 ILI9225_WR_Cmd(0x22);         // Write Data to GRAM
 digitalWrite(PIN_LCD_CS, HIGH);
}

// LCD寫入控制命令
// [in] VL 只送低位元組,高位元組一律填0x00
void ILI9225_WR_Cmd(char VL)
{
 digitalWrite(PIN_LCD_RS, LOW);   // 晶片選擇線設為低電位
 ILI9225_WR_Data16(0,VL);         // LCD寫入2個位元組(16 bit)資料
 digitalWrite(PIN_LCD_RS, HIGH);  // 晶片選擇線設為高電位 
}

// LCD寫入2個位元組(16 bit)資料
// [in] VH 為資料高位元組
// [in] VL 為資料低位元組
void ILI9225_WR_Data16(char VH, char VL)
{
 ILI9225_WR_Data8(VH);            // LCD寫入1個(高)位元組資料
 ILI9225_WR_Data8(VL);            // LCD寫入1個(低)位元組資料
}

// LCD寫入1個位元組(8 bit)資料
// [in] VL 為資料
// 寫入時由於I/O腳位分散於Arduino Port C & D,為加快速度採埠直接位元運算方式變更I/O
void ILI9225_WR_Data8(char VL)
{
 PORTC = (PORTC & 0xF0) | (VL & 0x0F);  // 取得資料低4位元送到 PC0 ~ 3
 PORTD = (PORTD & 0x0F) | (VL & 0xF0);  // 取得資料高4位元送到 PD4 ~ 7
 digitalWrite(PIN_LCD_WR, LOW);         // 晶片選擇線設為低電位
 digitalWrite(PIN_LCD_WR, HIGH);        // 晶片選擇線設為高電位  
}

// LCD寫入一組完整命令(暫存器編號加16bit資料)
// [in] cmd  命令暫存器編號
// [in] data 資料
void ILI9225_WR_Cmd_Data(char cmd, int data)
{
  digitalWrite(PIN_LCD_CS, LOW);        // 晶片選擇線設為低電位 
  ILI9225_WR_Cmd(cmd);                  // 寫入命令 
  ILI9225_WR_Data16(data>>8, data);     // 寫入資料(高位元組,低位元組)
  digitalWrite(PIN_LCD_CS, HIGH);       // 晶片選擇線設為高電位
}

// LCD清除畫面(全部黑屏)
// 即將GRAM全部填入0x00
void ILI9225_Clr_Screen()
{
  ILI9225_SetXY(0, 0, 175, 219, 175, 0); // 設定視窗範圍為全螢幕
 
  PORTC = (PORTC & 0xF0);                // 清除 PC0 ~ 3
  PORTD = (PORTD & 0x0F);                // 清除 PD4 ~ 7
  digitalWrite(PIN_LCD_CS, LOW);         // 晶片選擇線設為低電位
 
  for(int i=0; i<220; i++){              // 設定迴圈數為圖框高度
    for(int j=0; j<176*2; j++){          // 設定迴圈數為圖框寬度乘2,每個點為2個位元組(RGB565)
      digitalWrite(PIN_LCD_WR, LOW);     // 設定LCD寫入線為低電位
      digitalWrite(PIN_LCD_WR, HIGH);    // 設定LCD寫入線為高電位
    }
  }
 
  digitalWrite(PIN_LCD_CS, HIGH);        // 晶片選擇線設為高電位
}

// LCD測試
// 顯示十六色橫條紋於畫面上
void ILI9225_LCD_Test()
{
    ILI9225_SetXY(0, 0, 175, 219, 175, 0);  // 設定視窗範圍為全螢幕
    digitalWrite(PIN_LCD_CS, LOW);          // 晶片選擇線設為低電位
   
    for(int y=0; y<176; y++){               // 設定迴圈數為圖框高度
      int index = (y/11) % 16;              // 取得顏色表索引值
      int color = color_table[index];       // 取得欲寫入顏色內容
     
      for(int x=0; x<220; x++){             // 設定迴圈數為圖框寬度,每個點為2個位元組(RGB565)
        ILI9225_WR_Data16(color>>8, color); // 寫入LCD GRAM中
      }
   }
   
    digitalWrite(PIN_LCD_CS, HIGH);         // 晶片選擇線設為高電位
    delay(1000);                            // 延時1秒
    ILI9225_Clr_Screen();                   // 清除LCD畫面
    digitalWrite(PIN_LED1, HIGH);           // 點亮LED1
}

OpenQSignage開源迷你電子看板(Arduino LCD動畫胸牌)

OpenQSignage開源迷你電子看板(Arduino LCD動畫胸牌)#1_硬體設計說明

OpenQSignage開源迷你電子看板(Arduino LCD動畫胸牌)#2_Arduino程式說明

OpenQSignage開源迷你電子看板(Arduino LCD動畫胸牌)#3_PC程式說明

OpenQSignage開源迷你電子看板(Arduino LCD動畫胸牌)#3_PC程式說明

3. PC程式

為了配合Arduino能正確依排程設定顯示指定的圖框影像及文字橫幅,所以必須在PC端開發一套影像轉檔及下載程式。本專案在這裡選用Windows環境加上QT 5.6版(方便跨平台)及OpenCV 3.2版來進行開發視窗程式。QT及OpenCV的版本原則上不太重要,較舊的版本應該也可以,因為並沒有用到太多功能。

目前這版程式離真正的電子看板排程程式還差得滿遠的,但作為展示、學習用,還是相當合適的。為了簡化程式開發,本專案只提供三組圖框影像及三組橫幅文字設定。如圖八所示,可依下列步驟操作。
1. 載入所需圖框影像,若影像大於220*176則會自動縮小(不論原圖長寬比例)。
2. 設定顯示停留時間,單位為0.1秒。
3. 再來輸入文字內容,這裡支援任何文字輸入(中英文混合亦可)
4. 按[C]鍵指定文字及背景色,亦可按[字體]鍵選定指定字體及尺寸。
5. 指定橫幅文字顯示模式,固定顯示、交替閃爍及不顯示。
完成三組圖框影像及橫幅文字設定後就可準備下載工作了。

一般來說在電腦上的彩色影像是全彩(紅、綠、藍各以8bit表示,俗稱RGB888),但這裡我們利用OpenCV讀取影像檔案時,它會變成BGR888(24bit),紅綠通道會相反,而LCD顯示時因為只用了16bit,所以需降低色彩數變成RGB565,明顯可看出色彩數量及表示方式有很大差異。因此影像要下載到Arduino前必須進行格式轉換,不然無法正確顯示在LCD上。詳細作法可參見ColorBGR8882RGB565函式。

圖八、載入圖框影像、設定排程、編輯橫幅文字及設定顯示模式

要下載資料前,首先按下Arduino Pro Mini的重置鍵,讓系統回到等待下載模式。接著用USB轉UART轉接板連接Arduino Pro Mini和PC端。接下來就如圖九所示,按下【檢查】鍵確認USB轉UART轉接板是否已啟動,若正常工作時,會出現埠名(COM)及描述,此時【開啟通訊埠】鍵也會被致能,按下後就會開啟通訊埠準備輸出資料到Arduino Pro Mini。此時若想試一下是否通訊正常,可按【LED On】和【LED Off】來點亮及熄滅板子上的LED。再來按下【下載】鍵,等個約五十秒就可下載完所有資料。

目前傳輸時間看起來有點久,主要是因為通訊埠(COM)通訊鮑率設定只有57,600bps。因為不知那個地方產生干擾,導致設為115,200bps時會接收到亂碼,希望後續找到問題並解決後,下載速度就可大幅提升。若要再更快那可能就換掉Arduino,改用具有USB大量傳輸(Bluk)能力MCU才能徹底改善。

圖九、通訊埠檢查、開啟及排程、圖像下載

完整程式碼及註解如下所示,或可直接到https://github.com/OmniXRI/OpenQSignage下載。


// *********** OpenQSignageConverter.pro ******************


#-------------------------------------------------
#
# Project created by QtCreator 2018-01-09T14:38:59
#
#-------------------------------------------------

QT       += core gui
QT += widgets serialport

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

TARGET = OpenQSignageConverter
TEMPLATE = app
CONFIG += qt warn_on release

LIBS += -L C:\OpenCV-3.2.0\mingw\install\x86\mingw\lib\libopencv_*.a

INCLUDEPATH += C:\OpenCV-3.2.0\mingw\install\include\
               C:\OpenCV-3.2.0\mingw\install\include\opencv
               C:\OpenCV-3.2.0\mingw\install\include\opencv2


SOURCES += main.cpp\
        mainwindow.cpp

HEADERS  += mainwindow.h

FORMS    += mainwindow.ui


// *********** main.cpp ******************

#include "mainwindow.h"
#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();

    return a.exec();
}


// *********** mainwindow.h ******************

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QFileDialog>
#include <QLabel>
#include <QMessageBox>
#include <qevent.h>
#include <QCoreApplication>
#include <QPainter>
#include <QFontDialog>
#include <QColorDialog>
#include <QtSerialPort/QSerialPort>
#include <QtSerialPort/QSerialPortInfo>
#include <time.h>

#include <opencv2/opencv.hpp>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

protected:

private slots:
    void on_btnDownload_clicked();
    void on_btnPortOpen_clicked();
    void on_btnPortClose_clicked();
    void on_btnCheckComPort_clicked();
    void on_btnLoadF1_clicked();
    void on_btnLoadF2_clicked();
    void on_btnLoadF3_clicked();
    void on_btnLED0_clicked();
    void on_btnLED1_clicked();
    void on_btnGoB1_clicked();
    void on_btnFontB1_clicked();
    void on_btnBGColorB1_clicked();
    void on_btnFGColorB1_clicked();
    void on_btnBGColorB2_clicked();
    void on_btnFGColorB2_clicked();
    void on_btnBGColorB3_clicked();
    void on_btnFGColorB3_clicked();
    void on_btnGoB2_clicked();
    void on_btnFontB2_clicked();
    void on_btnGoB3_clicked();
    void on_btnFontB3_clicked();
    void on_spbF1_valueChanged(double arg1);
    void on_spbF2_valueChanged(double arg1);
    void on_spbF3_valueChanged(double arg1);
    void on_rdbFixB1_clicked();
    void on_rdbBlinkB1_clicked();
    void on_rdbNoneB1_clicked();
    void on_rdbFixB2_clicked();
    void on_rdbBlinkB2_clicked();
    void on_rdbNoneB2_clicked();
    void on_rdbFixB3_clicked();
    void on_rdbBlinkB3_clicked();
    void on_rdbNoneB3_clicked();

private:
    void SetLabelColor(QLabel *lab, QColor bg_color);
    void ShowFrame(cv::Mat imgS, QLabel *imgT);
    void ColorBGR8882RGB565(cv::Mat &imgSrc, cv::Mat &imgTrg);

    Ui::MainWindow *ui;
    QSerialPort *myport; // UART通訊埠

    QString fileName; // 載入影像之檔名
    cv::Mat imgSrc1;  // 原始影像(BGR888)
    cv::Mat imgSrc2;
    cv::Mat imgSrc3;
    cv::Mat imgF1;    // 原始影像縮小後之圖框影像(BGR888)
    cv::Mat imgF2;
    cv::Mat imgF3;
    cv::Mat imgB1;    // 橫幅原始影像(BGR888)
    cv::Mat imgB2;
    cv::Mat imgB3;
    cv::Mat imgLCDF1; // LCD用圖框影像(RGB565)
    cv::Mat imgLCDF2;
    cv::Mat imgLCDF3;
    cv::Mat imgLCDB1; // LCD用橫幅影像(RGB565)
    cv::Mat imgLCDB2;
    cv::Mat imgLCDB3;

    QFont fontB1; // 橫幅字體
    QFont fontB2;
    QFont fontB3;

    QColor BGColorB1; // 橫幅背景色
    QColor BGColorB2;
    QColor BGColorB3;
    QColor FGColorB1; // 橫幅前景(文字)色
    QColor FGColorB2;
    QColor FGColorB3;

    int disp_timerF1; // 顯示停留時間
    int disp_timerB1;
    int disp_timerF2;
    int disp_timerB2;
    int disp_timerF3;
    int disp_timerB3;

    int disp_modeF1;  // 圖框顯示模式
    int disp_modeB1;  // 橫幅顯示模式
    int disp_modeF2;
    int disp_modeB2;
    int disp_modeF3;
    int disp_modeB3;
};

#endif // MAINWINDOW_H


// *********** mainwindow.cpp ******************

#include "mainwindow.h"
#include "ui_mainwindow.h"

using namespace std;
using namespace cv;

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    qApp->installEventFilter(this);

    ui->pbxF1->setScaledContents(true); // 影像自動縮放至Label尺寸
    ui->pbxF2->setScaledContents(true); // 影像自動縮放至Label尺寸
    ui->pbxF3->setScaledContents(true); // 影像自動縮放至Label尺寸

    fontB1 = fontB2 = fontB3 = QFont("Times", 14, QFont::Bold); // 橫幅字體預設值為Times 14 粗體字
    BGColorB1 = BGColorB2 = BGColorB3 = Qt::white;              // 橫幅背景色預設為白色
    FGColorB1 = FGColorB2 = FGColorB3 = Qt::black;              // 橫幅前景(文字)色預設為黑色
    SetLabelColor(ui->labBGColorB1, Qt::white); SetLabelColor(ui->labFGColorB1, Qt::black); // 預設橫幅一標籤為白底黑字
    SetLabelColor(ui->labBGColorB2, Qt::white); SetLabelColor(ui->labFGColorB2, Qt::black); // 預設橫幅二標籤為白底黑字
    SetLabelColor(ui->labBGColorB3, Qt::white); SetLabelColor(ui->labFGColorB3, Qt::black); // 預設橫幅三標籤為白底黑字

    disp_timerF1 = disp_timerF2 = disp_timerF3 = disp_timerB1 = disp_timerB2 = disp_timerB3 = 30; // 預設排程停留時間3.0 sec
    disp_modeF1 = disp_modeF2 = disp_modeF3 = disp_modeB1 = disp_modeB2 = disp_modeB3 = 0;        // 預設顯示模式皆為固定顯示模式
}

MainWindow::~MainWindow()
{
    if(myport->isOpen()) // 若COM埠還開著
        myport->close(); // 就關閉

    delete ui;
}

// 將cv::Mat BGR888轉換為RGB888再顯示在QLabel上
// imgS 輸入影像
// imgT 輸出標籤
void MainWindow::ShowFrame(cv::Mat imgS, QLabel *imgT)
{
    Mat imgC;

    cvtColor(imgS,imgC,cv::COLOR_BGR2RGB); // 轉換色彩由BGR888到RGB888

    QImage tmp(imgC.data,
               imgC.cols,
               imgC.rows,
               //imgC.step,
               QImage::Format_RGB888);
    imgT->setPixmap(QPixmap::fromImage(tmp)); // 設定QImage影像到QLabel
}

// 設定標籤背景及文字顏色(色彩反相)
void MainWindow::SetLabelColor(QLabel *lab, QColor bg_color)
{
    QPalette palette;
    QColor fg_color;
    int r,g,b;

    bg_color.getRgb(&r,&g,&b);                         // 取得標籤目前背景顏色
    fg_color = QColor(255-r,255-g,255-b);              // 產生標籤前景(文色)反相顏色
    palette = lab->palette();                          // 為標籤指定色盤
    palette.setColor(lab->backgroundRole(), bg_color); // 設定色盤背景色
    palette.setColor(lab->foregroundRole(), fg_color); // 設定色盤前景色
    lab->setAutoFillBackground(true);                  // 設定自動填滿背景色(一定要設)
    lab->setPalette(palette);                          // 設定調色盤,變更標籤前背景色
}

// 將來源影像cv::Mat(BGR888)轉成LCD顯示用影像(RGB565)
// imrSrc: BGR888_24bit:[B7][B6][B5][B4][B3][B2][B1][B0]
// (CV_8UC3)            [G7][G6][G5][G4][G3][G2][G1][G0]
//                      [R7][R6][R5][R4][R3][R2][R1][R0]
// imgTrg: RGB565_16bit:[R7][R6][R5][R4][R3][G7][G6][G5]
// (CV_8UC2)            [G4][G3][G2][B7][B6][B5][B4][B3]
void MainWindow::ColorBGR8882RGB565(Mat &imgSrc, Mat &imgTrg)
{
 unsigned char *ptrS;
 unsigned char *ptrT;
 int posS, posT;

 if((imgSrc.cols != imgTrg.cols) || (imgSrc.rows != imgTrg.rows)){ // 確認輸入和輸出影像尺寸要相符
     QMessageBox::critical(this,"Error","Image size different!");
     return;
 }

 for(int i=0; i<imgSrc.rows; i++){        // 設定迴圈數為影像高度
     ptrS = imgSrc.ptr<unsigned char>(i); // 取得來源影像第i列(row)起始位置指標
     ptrT = imgTrg.ptr<unsigned char>(i); // 取得目標影像第i列(row)起始位置指標
    
     for(int j=0,posS=0,posT=0; j<imgSrc.cols; j++,posS+=3, posT+=2){
         ptrT[posT] = (ptrS[posS+2] & 0xF8) | (ptrS[posS+1] >> 5);        // RGB565高位元組(R5+G3)
         ptrT[posT+1] = ((ptrS[posS+1] & 0x1C) << 3) | (ptrS[posS] >> 3); // RGB565低位元組(G3+B5)
     }  
 }
}

// 以UART傳送點亮LED命令
void MainWindow::on_btnLED1_clicked()
{
    char cmd = 0xA5;     // 點亮LED命令
    myport->write(&cmd); // 透過UART送出命令
}

// 以UART傳送熄滅LED命令
void MainWindow::on_btnLED0_clicked()
{
    char cmd = 0xB4;     // 熄滅LED命令
    myport->write(&cmd); // 透過UART送出命令
}

// 以UART將圖框排程及影像內容傳送到Arduino
void MainWindow::on_btnDownload_clicked()
{
 double t0, t1, t2;
 QString str;
 vector<cv::Mat> vecS;
 vector<cv::Mat> vecT;
 int total_frame;
 int frame_w = ui->txbFrameW->text().toInt(); // 取得圖框寬度
 int frame_h = ui->txbFrameH->text().toInt(); // 取得圖框高度
 int d_mode[6] = {disp_modeF1, disp_modeB1, disp_modeF2, disp_modeB2, disp_modeF3, disp_modeB3}; // 圖框(橫幅)顯示模式
 int d_timer[6] = {disp_timerF1, disp_timerB1, disp_timerF2, disp_timerB2, disp_timerF3, disp_timerB3}; // 圖框(橫幅)顯示停留時間

 t0 = (double)clock();    // 啟動計時器
 str = "display mode = ";

 for(int i=0; i<6; i++){
     str += (QString::number(d_mode[i]) + ", ");
 }
 ui->txbRxData->append(str); // 將待傳送「顯示模式」內容顯示在狀態框

 str = "dwell time = ";
 for(int i=0; i<6; i++){
     str += (QString::number(d_timer[i]) + ", ");
 }
 ui->txbRxData->append(str); // 將待傳送「停留時間」內容顯示在狀態框

 cv::resize(imgSrc1,imgF1,imgF1.size()); // 重新取得圖框內容並縮放到LCD尺寸(像素)
 cv::resize(imgSrc2,imgF2,imgF2.size()); // 避免橫幅蓋掉圖框
 cv::resize(imgSrc3,imgF3,imgF3.size());

 vecS.push_back(imgF1);    // 將圖框(橫幅)推入影像陣列vecS
 vecS.push_back(imgB1);
 vecS.push_back(imgF2);
 vecS.push_back(imgB2);
 vecS.push_back(imgF3);
 vecS.push_back(imgB3);

 vecT.push_back(imgLCDF1); // 將LCD轉換結果影像推入影像陣列vecT
 vecT.push_back(imgLCDB1);
 vecT.push_back(imgLCDF2);
 vecT.push_back(imgLCDB2);
 vecT.push_back(imgLCDF3);
 vecT.push_back(imgLCDB3);

 total_frame = vecT.size(); // 取得圖框(橫幅)數量
 t1 = (double)clock();      // 取得時間1

 // 計算所有傳輸資料量(位元組)
 long total_size = (imgF1.cols * imgF1.rows * 2 * (total_frame/2)) + (imgB1.cols * imgB1.rows * 2 * (total_frame/2));
 long curr_size = 0;  // 目前已傳輸資料量(位元組)
 char cmd = 0xC3;     // UART開始傳送資料命令
 char ack;            // Arduino回應

 myport->clear();     // 清除通訊埠(COM)
 myport->write(&cmd); // 透過UART送出命令

 for(int f=0; f<total_frame; f++){  // 迴圈設為欲傳送之圖框數量   
    char frame_format[10];          // 配置圖框排程資料區

    frame_format[0] = vecT[f].cols >> 8;     // 圖框(橫幅)寬度高位元組
    frame_format[1] = vecT[f].cols & 0x00ff; // 圖框(橫幅)寬度低位元組
    frame_format[2] = vecT[f].rows >> 8;     // 圖框(橫幅)高度高位元組
    frame_format[3] = vecT[f].rows & 0x00ff; // 圖框(橫幅)高度低位元組
    frame_format[4] = (frame_w - vecT[f].cols) >> 8;     // 顯示起始位置sx高位元組
    frame_format[5] = (frame_w - vecT[f].cols) & 0x00ff; // 顯示起始位置sx低位元組
    frame_format[6] = (frame_h - vecT[f].rows) >> 8;     // 顯示起始位置sy高位元組
    frame_format[7] = (frame_h - vecT[f].rows) & 0x00ff; // 顯示起始位置sy低位元組
    frame_format[8] = d_mode[f];  // 顯示模式, 0:固定顯示, 1:交替閃爍, 2:不顯示
    frame_format[9] = d_timer[f]; // 顯示停留時間,0.0 ~ 25.5 sec轉換成 0 ~ 255

    myport->write(frame_format, 10); // 透過UART寫入10位元組

    ack = 0; // 清除回應值

    if(myport->waitForReadyRead(3000)){  // 等待回應(最多等3秒)
        myport->read(&ack,1);            // 取得回應值
    }

    char *ptrS;

    ColorBGR8882RGB565(vecS[f], vecT[f]);   // 轉換RGB888成為RGB565

    for(int i=0; i<vecT[f].rows; i++){      // 設定迴圈數量為影像高度
        ptrS = vecT[f].ptr<char>(i);        // 取得LCD影像第i列(row)起始位置指標
        myport->write(ptrS,vecT[f].cols*2); // 一次送出一列資料 (最大不可超過1K位元組)

        ack = 0; // 清除回應值

        if(myport->waitForReadyRead(3000)){  // 等待回應(最多等3秒)
            myport->read(&ack,1);            // 取得回應值
        }

        curr_size += (vecT[f].cols*2);       // 目前傳送值加上影像寬度*2

        ui->pgbDownload->setValue(((i+1)/(vecT[f].rows*1.0))*100); // 計算單張影像傳送百分比
        ui->pgbDownload->update();                                 // 更新單張影像下載進度條

        ui->pgbTotal->setValue((curr_size/(total_size*1.0))*100);  // 計算所有影像傳送百分比
        ui->pgbTotal->update();                                    // 更新所有影像下載進度條
    }
 }

 t2 = (double)clock(); // 取得目前時間
 str = "Image convert time = " + QString::number((t1-t0)/1000.0,'f',3) + "second.";  // 計算影像轉換時間
 ui->txbRxData->append(str);                                                         // 顯示於狀態框
 str = "Image download time = " + QString::number((t2-t1)/1000.0,'f',3) + "second."; // 計算影像下載時間
 ui->txbRxData->append(str);                                                         // 顯示於狀態框
 ui->txbRxData->append("Transfer done!");                                            // 顯示完成訊息於狀態框
 QMessageBox::information(this,tr("訊息"),tr("圖像下載完成!"));                      // 顯示完成訊息盒
}

// 檢查系統COM數量
void MainWindow::on_btnCheckComPort_clicked()
{
    // 取得所有可用的COM埠名稱表列
    QList<QSerialPortInfo> serialPortInfoList = QSerialPortInfo::availablePorts();

    if(serialPortInfoList.size() == 0)              // 若找不到任何COM埠
        ui->txbRxData->append("No find COM Port!"); // 在狀態框顯示找不到訊息
    else{                                           // 若有找到COM埠
        for (int i = 0; i < serialPortInfoList.size(); i++) {              // 設定迴圈數為COM埠數量
            ui->txbPortName->setText(serialPortInfoList.at(i).portName()); // 將COM埠名稱顯示於文字盒
            ui->txbPortDescription->setText(serialPortInfoList.at(i).description()); // 將COM埠描述顯示於文字盒
        }

        ui->btnPortOpen->setEnabled(true);  // 致能[開啟通訊埠]按鍵
    }
}

// 開啟通訊埠
void MainWindow::on_btnPortOpen_clicked()
{
    myport = new QSerialPort(ui->txbPortName->text());  // 依檢查到的埠名開啟COM埠

    myport->setBaudRate(QSerialPort::Baud57600);        // 設定通訊鮑率 57,600bps
    myport->setDataBits(QSerialPort::Data8);            // 設定傳輸字元為8位元
    myport->setParity(QSerialPort::NoParity);           // 設定為無同位元
    myport->setStopBits(QSerialPort::OneStop);          // 設定停止位元為1
    myport->setFlowControl(QSerialPort::NoFlowControl); // 設定無流量控制

    if (!myport->open(QIODevice::ReadWrite)){               // 若COM埠開啟失敗
        ui->txbRxData->append("serial port open failed");   // 顯示訊息於狀態框
        ui->btnPortOpen->setEnabled(false);                 // 禁能[開啟通訊埠]按鍵
        ui->btnPortClose->setEnabled(false);                // 禁能[關閉通訊埠]按鍵
        ui->btnLED0->setEnabled(false);                     // 禁能[LED Off]按鍵
        ui->btnLED1->setEnabled(false);                     // 禁能[LED On]按鍵
        ui->btnDownload->setEnabled(false);                 // 禁能[下載」按鍵
    }
    else{                                                   // 若COM埠開啟成功
        ui->txbRxData->append("serial port open sucessed"); // 顯示訊息於狀態框
        ui->btnPortOpen->setEnabled(false);                 // 禁能[開啟通訊埠]按鍵(不可重覆開啟)
        ui->btnPortClose->setEnabled(true);                 // 致能[關閉通訊埠]按鍵
        ui->btnLED0->setEnabled(true);                      // 致能[LED Off]按鍵
        ui->btnLED1->setEnabled(true);                      // 致能[LED On]按鍵
        ui->btnDownload->setEnabled(true);                  // 致能[下載」按鍵
    }
}

// 關閉通訊埠
void MainWindow::on_btnPortClose_clicked()
{
    if(myport->isOpen()){                                     // 若通訊埠已開啟
        myport->close();                                      // 關閉通訊埠
        ui->txbRxData->append("serial port close sucessed");  // 顯示訊息於狀態框
        ui->btnPortOpen->setEnabled(true);                    // 致能[開啟通訊埠]按鍵
        ui->btnPortClose->setEnabled(false);                  // 禁能[關閉通訊埠]按鍵
        ui->btnLED0->setEnabled(false);                       // 禁能[LED Off]按鍵
        ui->btnLED1->setEnabled(false);                       // 禁能[LED On]按鍵
        ui->btnDownload->setEnabled(false);                   // 禁能[下載」按鍵
    }
}

// 載入圖框影像一
void MainWindow::on_btnLoadF1_clicked()
{
    fileName = QFileDialog::getOpenFileName(this,tr("Open File")); // 開啟檔案對話盒並取得檔名
    imgSrc1 = cv::imread(fileName.toStdString(),IMREAD_COLOR);     // 讀入影像

     if(!imgSrc1.empty()) {                       // 若影像不空
         int fw = ui->txbFrameW->text().toInt();  // 取得圖框寬度
         int fh = ui->txbFrameH->text().toInt();  // 取得圖框高度
         int bw = ui->txbBannerW->text().toInt(); // 取得橫幅寬度
         int bh = ui->txbBannerH->text().toInt(); // 取得橫幅高度

         cv::resize(imgSrc1,imgF1,cvSize(fw,fh));        // 將原始影像縮到指定圖框尺寸
         imgLCDF1 = Mat::zeros(cv::Size(fw,fh),CV_8UC2); // 清除圖框影像緩衝區
         imgLCDB1 = Mat::zeros(cv::Size(bw,bh),CV_8UC2); // 清除橫幅影像緩衝區
         ShowFrame(imgF1, ui->pbxF1);                    // 將圖框影像秀在標籤上
         ui->btnGoB1->setEnabled(true);                  // 致能[更新]按鍵
     }
}

// 載入圖框影像二
void MainWindow::on_btnLoadF2_clicked()
{
    fileName = QFileDialog::getOpenFileName(this,tr("Open File")); // 開啟檔案對話盒並取得檔名
    imgSrc2 = cv::imread(fileName.toStdString(),IMREAD_COLOR);     // 讀入影像

     if(!imgSrc2.empty()) {                       // 若影像不空
         int fw = ui->txbFrameW->text().toInt();  // 取得圖框寬度
         int fh = ui->txbFrameH->text().toInt();  // 取得圖框高度
         int bw = ui->txbBannerW->text().toInt(); // 取得橫幅寬度
         int bh = ui->txbBannerH->text().toInt(); // 取得橫幅高度

         cv::resize(imgSrc2,imgF2,cvSize(fw,fh));        // 將原始影像縮到指定圖框尺寸
         imgLCDF2 = Mat::zeros(cv::Size(fw,fh),CV_8UC2); // 清除圖框影像緩衝區
         imgLCDB2 = Mat::zeros(cv::Size(bw,bh),CV_8UC2); // 清除橫幅影像緩衝區
         ShowFrame(imgF2, ui->pbxF2);                    // 將圖框影像秀在標籤上
         ui->btnGoB2->setEnabled(true);                  // 致能[更新]按鍵
     }
}

// 載入圖框影像三
void MainWindow::on_btnLoadF3_clicked()
{
    fileName = QFileDialog::getOpenFileName(this,tr("Open File")); // 開啟檔案對話盒並取得檔名
    imgSrc3 = cv::imread(fileName.toStdString(),IMREAD_COLOR);     // 讀入影像

     if(!imgSrc3.empty()) {                       // 若影像不空
         int fw = ui->txbFrameW->text().toInt();  // 取得圖框寬度
         int fh = ui->txbFrameH->text().toInt();  // 取得圖框高度
         int bw = ui->txbBannerW->text().toInt(); // 取得橫幅寬度
         int bh = ui->txbBannerH->text().toInt(); // 取得橫幅高度

         cv::resize(imgSrc3,imgF3,cvSize(fw,fh));        // 將原始影像縮到指定圖框尺寸
         imgLCDF3 = Mat::zeros(cv::Size(fw,fh),CV_8UC2); // 清除圖框影像緩衝區
         imgLCDB3 = Mat::zeros(cv::Size(bw,bh),CV_8UC2); // 清除橫幅影像緩衝區
         ShowFrame(imgF3, ui->pbxF3);                    // 將圖框影像秀在標籤上
         ui->btnGoB3->setEnabled(true);                  // 致能[更新]按鍵
     }
}

// 變更橫幅一背景色彩
void MainWindow::on_btnBGColorB1_clicked()
{
    QColorDialog colorDlg;

    colorDlg.exec();                            // 開啟色彩拾取盒
    BGColorB1 = colorDlg.selectedColor();       // 設定橫幅背景色彩
    SetLabelColor(ui->labBGColorB1, BGColorB1); // 設定標籤色彩
    ui->btnGoB1->click();                       // 代理按下[更新]鍵
}

// 變更橫幅一前景(文字)色彩
void MainWindow::on_btnFGColorB1_clicked()
{
    QColorDialog colorDlg;

    colorDlg.exec();                            // 開啟色彩拾取盒
    FGColorB1 = colorDlg.selectedColor();       // 設定橫幅前景(文字)色彩
    SetLabelColor(ui->labFGColorB1, FGColorB1); // 設定標籤色彩
    ui->btnGoB1->click();                       // 代理按下[更新]鍵
}

// 變更橫幅二背景色彩
void MainWindow::on_btnBGColorB2_clicked()
{
    QColorDialog colorDlg;

    colorDlg.exec();                            // 開啟色彩拾取盒
    BGColorB2 = colorDlg.selectedColor();       // 設定橫幅背景色彩
    SetLabelColor(ui->labBGColorB2, BGColorB2); // 設定標籤色彩
    ui->btnGoB2->click();                       // 代理按下[更新]鍵
}

// 變更橫幅二前景(文字)色彩
void MainWindow::on_btnFGColorB2_clicked()
{
    QColorDialog colorDlg;

    colorDlg.exec();                            // 開啟色彩拾取盒
    FGColorB2 = colorDlg.selectedColor();       // 設定橫幅前景(文字)色彩
    SetLabelColor(ui->labFGColorB2, FGColorB2); // 設定標籤色彩
    ui->btnGoB2->click();                       // 代理按下[更新]鍵
}

// 變更橫幅三背景色彩
void MainWindow::on_btnBGColorB3_clicked()
{
    QColorDialog colorDlg;

    colorDlg.exec();                            // 開啟色彩拾取盒
    BGColorB3 = colorDlg.selectedColor();       // 設定橫幅背景色彩
    SetLabelColor(ui->labBGColorB3, BGColorB3); // 設定標籤色彩
    ui->btnGoB3->click();                       // 代理按下[更新]鍵
}

// 變更橫幅三前景(文字)色彩
void MainWindow::on_btnFGColorB3_clicked()
{
    QColorDialog colorDlg;

    colorDlg.exec();                            // 開啟色彩拾取盒
    FGColorB3 = colorDlg.selectedColor();       // 設定橫幅前景(文字)色彩
    SetLabelColor(ui->labFGColorB3, FGColorB3); // 設定標籤色彩
    ui->btnGoB3->click();                       // 代理按下[更新]鍵
}

// 更新橫幅一(含色彩及字體)
void MainWindow::on_btnGoB1_clicked()
{
    int bw = ui->txbBannerW->text().toInt();              // 取得橫幅寬度
    int bh = ui->txbBannerH->text().toInt();              // 取得橫幅高度
    QImage imgB1Q = QImage(bw,bh, QImage::Format_RGB888); // 宣告影像暫存區(RGB888)
    QPainter painter(&imgB1Q);                            // 宣告繪圖區

    painter.setBrush(BGColorB1);     // 設定繪圖區筆刷(背景)色
    painter.setPen(BGColorB1);       // 設定繪圖區彩筆(前景)色,去除邊框
    painter.drawRect(imgB1Q.rect()); // 設定繪圖區尺寸
    painter.setPen(FGColorB1);       // 設定繪圖區彩筆(前景)色
    painter.setFont(fontB1);         // 設定繪圖區字體
    painter.drawText(imgB1Q.rect(), Qt::AlignCenter,ui->txbB1->text()); // 將文字繪至繪圖區

    ui->pbxB1->setPixmap(QPixmap::fromImage(imgB1Q)); // 將繪好的文字區設定至標籤
    ui->pbxB1->setAlignment(Qt::AlignCenter);         // 設定置中對齊

    Mat matROI = imgF1(Rect(0,140,220,36));           // 設定圖框影像預備放橫幅位置
    //設定cv::Mat空白繪圖區,從QImage轉到cv::Mat
    Mat matPainter = cv::Mat(imgB1Q.height(), imgB1Q.width(), CV_8UC3, imgB1Q.bits(), imgB1Q.bytesPerLine());

    cvtColor(matPainter, matPainter, CV_RGB2BGR); // 將色彩從BGR888轉到RGB888
    matPainter.copyTo(imgB1);                     // 將橫幅繪圖區複製到橫幅影像區
    painter.end();                                // 結束繪圖區

    if(ui->txbB1->text() != 0 && !ui->rdbNoneB1->isChecked()){ // 若文字內容不為空且橫幅不是選擇不顯示模式
        imgB1.copyTo(matROI);                                  // 則將橫幅影像貼到圖框影像中指定位置
    }
    else{
        cv::resize(imgSrc1,imgF1,imgF1.size());                // 否則重新以原始影像縮至圖框影像中
    }   

    ShowFrame(imgF1, ui->pbxF1); // 顯示圖框(含橫幅)影像
}

// 變更橫幅一字體
void MainWindow::on_btnFontB1_clicked()
{
    QFontDialog fontDlg;

    fontDlg.exec();                  // 開啟字體選擇對話盒
    fontB1 = fontDlg.selectedFont(); // 取得字體參數
    ui->btnGoB1->click();            // 代理按下[更新]鍵
}

// 更新橫幅二(含色彩及字體)
void MainWindow::on_btnGoB2_clicked()
{
    int bw = ui->txbBannerW->text().toInt();               // 取得橫幅寬度
    int bh = ui->txbBannerH->text().toInt();               // 取得橫幅高度
    QImage imgB2Q = QImage(bw, bh, QImage::Format_RGB888); // 宣告影像暫存區(RGB888)
    QPainter painter(&imgB2Q);                             // 宣告繪圖區

    painter.setBrush(BGColorB2);     // 設定繪圖區筆刷(背景)色
    painter.setPen(BGColorB2);       // 設定繪圖區彩筆(前景)色,去除邊框
    painter.drawRect(imgB2Q.rect()); // 設定繪圖區尺寸
    painter.setPen(FGColorB2);       // 設定繪圖區彩筆(前景)色
    painter.setFont(fontB2);         // 設定繪圖區字體
    painter.drawText(imgB2Q.rect(), Qt::AlignCenter,ui->txbB2->text()); // 將文字繪至繪圖區

    ui->pbxB2->setPixmap(QPixmap::fromImage(imgB2Q)); // 將繪好的文字區設定至標籤
    ui->pbxB2->setAlignment(Qt::AlignCenter);         // 設定置中對齊

    Mat matROI = imgF2(Rect(0,140,220,36));           // 設定圖框影像預備放橫幅位置
    //設定cv::Mat空白繪圖區,從QImage轉到cv::Mat
    Mat matPainter = cv::Mat(imgB2Q.height(), imgB2Q.width(), CV_8UC3, imgB2Q.bits(), imgB2Q.bytesPerLine());

    cvtColor(matPainter, matPainter, CV_RGB2BGR); // 將色彩從BGR888轉到RGB888
    matPainter.copyTo(imgB2);                     // 將橫幅繪圖區複製到橫幅影像區
    painter.end();                                // 結束繪圖區

    if(ui->txbB2->text() != 0 && !ui->rdbNoneB2->isChecked()){ // 若文字內容不為空且橫幅不是選擇不顯示模式
        imgB2.copyTo(matROI);                                  // 則將橫幅影像貼到圖框影像中指定位置
    }
    else{
        cv::resize(imgSrc2,imgF2,imgF2.size());                // 否則重新以原始影像縮至圖框影像中
    }

    ShowFrame(imgF2, ui->pbxF2); // 顯示圖框(含橫幅)影像
}

// 變更橫幅二字體
void MainWindow::on_btnFontB2_clicked()
{
    QFontDialog fontDlg;

    fontDlg.exec();                  // 開啟字體選擇對話盒
    fontB2 = fontDlg.selectedFont(); // 取得字體參數
    ui->btnGoB2->click();            // 代理按下[更新]鍵
}

// 更新橫幅三(含色彩及字體)
void MainWindow::on_btnGoB3_clicked()
{
    int bw = ui->txbBannerW->text().toInt();               // 取得橫幅寬度
    int bh = ui->txbBannerH->text().toInt();               // 取得橫幅高度
    QImage imgB3Q = QImage(bw, bh, QImage::Format_RGB888); // 宣告影像暫存區(RGB888)
    QPainter painter(&imgB3Q);                             // 宣告繪圖區

    painter.setBrush(BGColorB3);     // 設定繪圖區筆刷(背景)色
    painter.setPen(BGColorB3);       // 設定繪圖區彩筆(前景)色,去除邊框
    painter.drawRect(imgB3Q.rect()); // 設定繪圖區尺寸
    painter.setPen(FGColorB3);       // 設定繪圖區彩筆(前景)色
    painter.setFont(fontB3);         // 設定繪圖區字體
    painter.drawText(imgB3Q.rect(), Qt::AlignCenter,ui->txbB3->text()); // 將文字繪至繪圖區

    ui->pbxB3->setPixmap(QPixmap::fromImage(imgB3Q)); // 將繪好的文字區設定至標籤
    ui->pbxB3->setAlignment(Qt::AlignCenter);         // 設定置中對齊

    Mat matROI = imgF3(Rect(0,140,220,36));           // 設定圖框影像預備放橫幅位置
    //設定cv::Mat空白繪圖區,從QImage轉到cv::Mat
    Mat matPainter= cv::Mat(imgB3Q.height(), imgB3Q.width(), CV_8UC3, imgB3Q.bits(), imgB3Q.bytesPerLine());

    cvtColor(matPainter, matPainter, CV_RGB2BGR); // 將色彩從BGR888轉到RGB888
    matPainter.copyTo(imgB3);                     // 將橫幅繪圖區複製到橫幅影像區
    painter.end();                                // 結束繪圖區

    if(ui->txbB3->text() != 0 && !ui->rdbNoneB3->isChecked()){ // 若文字內容不為空且橫幅不是選擇不顯示模式
        imgB3.copyTo(matROI);                                  // 則將橫幅影像貼到圖框影像中指定位置
    }
    else{
        cv::resize(imgSrc3,imgF3,imgF3.size());                // 否則重新以原始影像縮至圖框影像中
    }

    ShowFrame(imgF3, ui->pbxF3); // 顯示圖框(含橫幅)影像
}

// 變更橫幅三字體
void MainWindow::on_btnFontB3_clicked()
{
    QFontDialog fontDlg;

    fontDlg.exec();                  // 開啟字體選擇對話盒
    fontB3 = fontDlg.selectedFont(); // 取得字體參數
    ui->btnGoB3->click();            // 代理按下[更新]鍵
}

// 圖框一顯示停留時間變更
void MainWindow::on_spbF1_valueChanged(double arg1)
{
    disp_timerF1 = disp_timerB1 = (int)(arg1*10); // 取得圖框顯示時間(單位乘10)
}

// 圖框二顯示停留時間變更
void MainWindow::on_spbF2_valueChanged(double arg1)
{
    disp_timerF2 = disp_timerB2 = (int)(arg1*10); // 取得圖框顯示時間(單位乘10)
}

// 圖框三顯示停留時間變更
void MainWindow::on_spbF3_valueChanged(double arg1)
{
    disp_timerF3 = disp_timerB3 = (int)(arg1*10); // 取得圖框顯示時間(單位乘10)
}

// 橫幅一顯示模式切換到固定模式
void MainWindow::on_rdbFixB1_clicked()
{
    disp_modeB1 = 0; // 顯示模式編號設定為0
}

// 橫幅一顯示模式切換到交替閃爍模式
void MainWindow::on_rdbBlinkB1_clicked()
{
    disp_modeB1 = 1; // 顯示模式編號設定為1
}

// 橫幅一顯示模式切換到不顯示模式
void MainWindow::on_rdbNoneB1_clicked()
{
    disp_modeB1 = 2; // 顯示模式編號設定為2
}

// 橫幅二顯示模式切換到固定模式
void MainWindow::on_rdbFixB2_clicked()
{
    disp_modeB2 = 0; // 顯示模式編號設定為0
}

// 橫幅二顯示模式切換到交替閃爍模式
void MainWindow::on_rdbBlinkB2_clicked()
{
    disp_modeB2 = 1; // 顯示模式編號設定為1
}

// 橫幅二顯示模式切換到不顯示模式
void MainWindow::on_rdbNoneB2_clicked()
{
    disp_modeB2 = 2; // 顯示模式編號設定為2
}

// 橫幅三顯示模式切換到固定模式
void MainWindow::on_rdbFixB3_clicked()
{
    disp_modeB3 = 0; // 顯示模式編號設定為0
}

// 橫幅三顯示模式切換到交替閃爍模式
void MainWindow::on_rdbBlinkB3_clicked()
{
    disp_modeB3 = 1; // 顯示模式編號設定為1
}

// 橫幅三顯示模式切換到不顯示模式
void MainWindow::on_rdbNoneB3_clicked()
{
    disp_modeB3 = 2; // 顯示模式編號設定為2
}

OpenQSignage開源迷你電子看板(Arduino LCD動畫胸牌)

OpenQSignage開源迷你電子看板(Arduino LCD動畫胸牌)#1_硬體設計說明

OpenQSignage開源迷你電子看板(Arduino LCD動畫胸牌)#2_Arduino程式說明

OpenQSignage開源迷你電子看板(Arduino LCD動畫胸牌)#3_PC程式說明