在上一篇水文中,老周演示了 WS28XX 的基本使用。在文末老周說了本篇介紹顏色漸變動畫的簡單實現。
在正式開始前,說一下題外話。
第一件事,最近樹莓派的價格猛漲,相信有關注的朋友都知道了。所以,如果你不是急著用,可以先別買。或者,可以選擇 Raspberry Pi 400,這個配置比 4B 高一點,這個目前價格比較正常。Pi 400 就是那個藏在鍵盤裡的樹莓派。其實,官網上面的價格已經調回原來的價格了,只是某寶上的那些 Jian 商,還在漲價。
第二件事,樹莓派上的應用是不是可以用 C 來寫?這是廢話了。樹莓派上執行的是 Linux 系統,當然可以了。有夥伴會說,用.NET體驗如何?老周可以告訴你:完全沒問題,這個庫大部分API老周都做過實驗。.net Iot 庫的效能你不用擔心,因為最近幾年.NET的效能提升很大,更何況.NET只是封裝了底層API的呼叫,當指令傳遞到系統驅動層,其效率和 C 是一樣的。你不妨想想,連 Python 這種效能差得沒有天敵的程式語言都能玩物聯網,.NET 你還怕啥呢。儘管目前開源的庫不多,但官方給的 Devices 也基本覆蓋各種感測器模組。
-------------------------------------------------------------------------------------
好了,F話就聊到這兒,接下來正片開始。
讓WS28XX 控制燈帶產生動畫,其本質上就是每隔一段時間更新一下每個燈珠的顏色。由於人眼的反應速度和處理能力比不上貓,所以我們會看到動畫。我們們看到的是動畫,但老周估計喵喵們看到的是PPT。
所以,所謂顏色漸變動畫,首先,你要確定兩種顏色——起始色和最終色,比如從綠色變成紅色,綠色是起始,紅色是終點。
然後,我們要算出起始色與終點色之間,R、G、B 各值間的差值。
假設,我們的延時 d = 40 ms(精確到毫秒就夠,不用考慮微秒納秒,反正你眼睛看不到),然後我們們要從紅色變成藍色。
紅:R=255, G=0, B=0
藍:R=0, G=0, B=255
計算差距,終點減去起點,不管正負。
dif_R = 0-255 = -255
dif_G = 0-0 = 0
dif_B = 255-0 = 255
這樣我們就看到,從紅到藍,R的值是遞減的,G不變,B的值是遞增的。我們先不去想演算法對不對,不妨繼續推算:
第一輪迴圈,R=255-1=254, G=0,B=0+1=1,Sleep 40;
第二輪迴圈,R=255-2=253,G=0,B=0+2=2,Sleep 40;
第三輪迴圈,R=255-3=252,G=0,B=0+3=3,Sleep 40
……
直到把目標值變成 R=0,G=0,B=255。每一輪迴圈之間,會暫停 40 ms。
可是,演算法還真不能這麼簡單,我們們忽略了一個問題,請看下面的舉例:
假設要從 R=120,G=200,B=10 變成 R=255,G=100,B=60
計算差值:difR = 255-120=135,difG=100-200=-100,difB=60-10=50。RGB之間的差值並不相等,如果我們每輪迴圈都 +1 或 -1,那麼會存在一個問題:有的值可能早已到達終值,而有的值還沒到達終值。這種情況燈光的漸變過程會看起來不太順暢。
所以,我們必須解決的問題就是要在 N 輪迴圈之後,RGB三個值要同時到達終值。這麼一來,差值大的要漸變得快一些,差值小的要漸變得慢一些。跑得快的等一下跑得慢的,形成統一戰線,同時到達終點。
因此,漸變過程中迴圈的次數必須統一,但每次迴圈裡面,RGB改變的量不同,但N輪迴圈過後會同時到達終值。
舉例,從 R1=100,G1=0,B1=230 變為 R2=20,G2=72,B2=57
那麼,差值:
dR = 20-100=-80
dG = 72-0=72
dB = 57-230=-173
假如迴圈次數為80次,可以理解為分 80 個步長來完成,設 step = 80。接下來就得算出這80步中,每一步裡RGB各值要變化多少(單位步長)。
pR = dR / 80=-80/80 = -1
pG = dG / 80 = 72 / 80 = 0.9
pB = dB / 80 = -173 / 80 = -2.16
再設某一輪迴圈(某一步)為 i ,於是
for i = 0; i <= 80; i++
R = R1 + i * -1;
G = G1 + i * 0.9;
B = B1 + i * -2.16;
R1、G1、B1 指的是起始顏色的值,在一次迴圈中,讓初始值加上 i 與單位步長(pR、pG、pB)的乘積。
這麼一搞,就能保證在 N 個迴圈後,三個值能同時到達終值。
------------------------------------------------------------------------------------------
OK,有了上面的推演過程,我們可以把它翻譯成程式碼。我直接封裝為一個類。
public class GradLeds { Ws28xx _leds; public GradLeds(Ws28xx ws) => _leds = ws; public void Run(Color start, Color end, int steps = 120, int delay_ms = 30) { if (steps <= 0) throw new Exception("steps 不能小於/等於0"); if (delay_ms <= 0) throw new Exception("延時必須大於0"); // 計算RGB的差值,不論正負 float dR = (float)end.R - start.R; float dG = (float)end.G - start.G; float dB = (float)end.B - start.B; // 計算每一個步長(step)要增長的值 float ir = dR / steps; float ig = dG / steps; float ib = dB / steps; // 通過寬度獲取燈珠數 int ledNum = _leds.Image.Width; for (var a = 0; a <= steps; a++) { // 如果執行狀態為false,退出迴圈 if(AppContext.TryGetSwitch("running",out bool b) && !b) { break; } Color tc = Color.FromArgb( (int)(start.R + a * ir), (int)(start.G + a * ig), (int)(start.B + a * ib) ); // 填充所有燈珠 for (var n = 0; n < ledNum; n++) { _leds.Image.SetPixel(n, 0, tc); } _leds.Update(); // 延時 Thread.Sleep(delay_ms); } } }
在這個類中,我用到了 AppContext,如果你看過老周在幾千年前寫的博文,應該會記得這個 AppContext類,它可以用來設定一些全域性開關,開關名是字串,值是布林值。直接用這個類,我們不需要刻意去寫個類,再弄個靜態欄位來當全域性變數了,更何況靜態成員是不能跨 AppDomain 共享值的,如果多執行緒還得考慮同步。
在 AppContext 中老週會設定一個開關,名為 running,如果是 true,說明程式在執行;若為 false,則說明程式要退出了,就不會再漸變了。
因為這個漸變過程會持續幾秒時間甚至更長,如果程式要退出,就不要再迴圈了,而是趕緊終止操作。
start 和 end 表示起始顏色和終點顏色,steps 表示要進行多少步(迴圈數),delay_ms 參數列示每一輪迴圈之間的延時。
回到主程式,呼叫測試。
using System.Device.Spi; using Iot.Device.Ws28xx; using Grdtest; using System.Drawing; // 初始化SPI匯流排 SpiConnectionSettings settings = new(0) { Mode = SpiMode.Mode0, DataBitLength = 8, ClockFrequency = 2400_000 }; using SpiDevice device = SpiDevice.Create(settings); // WS28XX,30個燈珠 Ws28xx ws = new Ws2812b(device, 30); GradLeds grdled = new(ws); int steps = 90; //90個迴圈 int delay = 25; //延時(毫秒) // 設定執行狀態 AppContext.SetSwitch("running", true); // 按Ctrl+C時程式要退出,處理一下 Console.CancelKeyPress += async (_, e) => { e.Cancel = true; //阻上程式馬上退出 // 關閉開關,表示程式不再執行了 AppContext.SetSwitch("running", false); await Task.Delay(150); //保險一點,等一會兒 e.Cancel = false; //告訴系統,可以退出了 }; // 主迴圈 while (AppContext.TryGetSwitch("running", out bool b) && b) { // 從紅變藍 grdled.Run(Color.Red, Color.Blue, steps, delay); // 從藍變黃 grdled.Run(Color.Blue, Color.Yellow, steps, delay); // 從黃變深粉色 grdled.Run(Color.Yellow, Color.DeepPink, steps, delay); // 從深粉色變白色 grdled.Run(Color.DeepPink, Color.White, steps, delay); // 從白變回紅 grdled.Run(Color.White, Color.Red, steps, delay); } // 黑燈收工 ws.Image.Clear(Color.Black); ws.Update();
最後這兩句是當退出 while 迴圈後,讓所有燈珠熄燈(黑色表示燈滅)。
ws.Image.Clear(Color.Black);
ws.Update();
好了,我們們來看看效果,這個效果應該能接受。
其他動畫演算法,大夥伴們不妨自己動手去試試。演算法不一定要從網上抄,可以根據自己的理解去設計。可以做出自己的創意,你愛咋玩就咋玩。