.NET 與樹(shù)莓派WS28XX 燈帶的顏色漸變動(dòng)畫效果的實(shí)現(xiàn)
在上一篇水文中,老周演示了 WS28XX 的基本使用。在文末老周說(shuō)了本篇介紹顏色漸變動(dòng)畫的簡(jiǎn)單實(shí)現(xiàn)。
在正式開(kāi)始前,說(shuō)一下題外話。
第一件事,最近樹(shù)莓派的價(jià)格猛漲,相信有關(guān)注的朋友都知道了。所以,如果你不是急著用,可以先別買?;蛘撸梢赃x擇 Raspberry Pi 400,這個(gè)配置比 4B 高一點(diǎn),這個(gè)目前價(jià)格比較正常。Pi 400 就是那個(gè)藏在鍵盤里的樹(shù)莓派。其實(shí),官網(wǎng)上面的價(jià)格已經(jīng)調(diào)回原來(lái)的價(jià)格了,只是某寶上的那些 Jian 商,還在漲價(jià)。
第二件事,樹(shù)莓派上的應(yīng)用是不是可以用 C 來(lái)寫?這是廢話了。樹(shù)莓派上運(yùn)行的是 Linux 系統(tǒng),當(dāng)然可以了。有伙伴會(huì)說(shuō),用.NET體驗(yàn)如何?老周可以告訴你:完全沒(méi)問(wèn)題,這個(gè)庫(kù)大部分API老周都做過(guò)實(shí)驗(yàn)。.net Iot 庫(kù)的性能你不用擔(dān)心,因?yàn)樽罱鼛啄?NET的性能提升很大,更何況.NET只是封裝了底層API的調(diào)用,當(dāng)指令傳遞到系統(tǒng)驅(qū)動(dòng)層,其效率和 C 是一樣的。你不妨想想,連 Python 這種性能差得沒(méi)有天敵的編程語(yǔ)言都能玩物聯(lián)網(wǎng),.NET 你還怕啥呢。盡管目前開(kāi)源的庫(kù)不多,但官方給的 Devices 也基本覆蓋各種傳感器模塊。
-------------------------------------------------------------------------------------
好了,F(xiàn)話就聊到這兒,接下來(lái)正片開(kāi)始。
讓W(xué)S28XX 控制燈帶產(chǎn)生動(dòng)畫,其本質(zhì)上就是每隔一段時(shí)間更新一下每個(gè)燈珠的顏色。由于人眼的反應(yīng)速度和處理能力比不上貓,所以我們會(huì)看到動(dòng)畫。咱們看到的是動(dòng)畫,但老周估計(jì)喵喵們看到的是PPT。
所以,所謂顏色漸變動(dòng)畫,首先,你要確定兩種顏色——起始色和最終色,比如從綠色變成紅色,綠色是起始,紅色是終點(diǎn)。
然后,我們要算出起始色與終點(diǎn)色之間,R、G、B 各值間的差值。
假設(shè),我們的延時(shí) d = 40 ms(精確到毫秒就夠,不用考慮微秒納秒,反正你眼睛看不到),然后咱們要從紅色變成藍(lán)色。
紅:R=255, G=0, B=0
藍(lán):R=0, G=0, B=255
計(jì)算差距,終點(diǎn)減去起點(diǎn),不管正負(fù)。
dif_R = 0-255 = -255
dif_G = 0-0 = 0
dif_B = 255-0 = 255
這樣我們就看到,從紅到藍(lán),R的值是遞減的,G不變,B的值是遞增的。我們先不去想算法對(duì)不對(duì),不妨繼續(xù)推算:
第一輪循環(huán),R=255-1=254, G=0,B=0+1=1,Sleep 40;
第二輪循環(huán),R=255-2=253,G=0,B=0+2=2,Sleep 40;
第三輪循環(huán),R=255-3=252,G=0,B=0+3=3,Sleep 40
……
直到把目標(biāo)值變成 R=0,G=0,B=255。每一輪循環(huán)之間,會(huì)暫停 40 ms。
可是,算法還真不能這么簡(jiǎn)單,咱們忽略了一個(gè)問(wèn)題,請(qǐng)看下面的舉例:
假設(shè)要從 R=120,G=200,B=10 變成 R=255,G=100,B=60
計(jì)算差值:difR = 255-120=135,difG=100-200=-100,difB=60-10=50。RGB之間的差值并不相等,如果我們每輪循環(huán)都 +1 或 -1,那么會(huì)存在一個(gè)問(wèn)題:有的值可能早已到達(dá)終值,而有的值還沒(méi)到達(dá)終值。這種情況燈光的漸變過(guò)程會(huì)看起來(lái)不太順暢。
所以,我們必須解決的問(wèn)題就是要在 N 輪循環(huán)之后,RGB三個(gè)值要同時(shí)到達(dá)終值。這么一來(lái),差值大的要漸變得快一些,差值小的要漸變得慢一些。跑得快的等一下跑得慢的,形成統(tǒng)一戰(zhàn)線,同時(shí)到達(dá)終點(diǎn)。
因此,漸變過(guò)程中循環(huán)的次數(shù)必須統(tǒng)一,但每次循環(huán)里面,RGB改變的量不同,但N輪循環(huán)過(guò)后會(huì)同時(shí)到達(dá)終值。
舉例,從 R1=100,G1=0,B1=230 變?yōu)?R2=20,G2=72,B2=57
那么,差值:
dR = 20-100=-80
dG = 72-0=72
dB = 57-230=-173
假如循環(huán)次數(shù)為80次,可以理解為分 80 個(gè)步長(zhǎng)來(lái)完成,設(shè) step = 80。接下來(lái)就得算出這80步中,每一步里RGB各值要變化多少(單位步長(zhǎng))。
pR = dR / 80=-80/80 = -1
pG = dG / 80 = 72 / 80 = 0.9
pB = dB / 80 = -173 / 80 = -2.16
再設(shè)某一輪循環(huán)(某一步)為 i ,于是
for i = 0; i <= 80; i++
R = R1 + i * -1;
G = G1 + i * 0.9;
B = B1 + i * -2.16;
R1、G1、B1 指的是起始顏色的值,在一次循環(huán)中,讓初始值加上 i 與單位步長(zhǎng)(pR、pG、pB)的乘積。
這么一搞,就能保證在 N 個(gè)循環(huán)后,三個(gè)值能同時(shí)到達(dá)終值。
------------------------------------------------------------------------------------------
OK,有了上面的推演過(guò)程,我們可以把它翻譯成代碼。我直接封裝為一個(gè)類。
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("延時(shí)必須大于0"); // 計(jì)算RGB的差值,不論正負(fù) float dR = (float)end.R - start.R; float dG = (float)end.G - start.G; float dB = (float)end.B - start.B; // 計(jì)算每一個(gè)步長(zhǎng)(step)要增長(zhǎng)的值 float ir = dR / steps; float ig = dG / steps; float ib = dB / steps; // 通過(guò)寬度獲取燈珠數(shù) int ledNum = _leds.Image.Width; for (var a = 0; a <= steps; a++) { // 如果運(yùn)行狀態(tài)為false,退出循環(huán) 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(); // 延時(shí) Thread.Sleep(delay_ms); } } }
在這個(gè)類中,我用到了 AppContext,如果你看過(guò)老周在幾千年前寫的博文,應(yīng)該會(huì)記得這個(gè) AppContext類,它可以用來(lái)設(shè)置一些全局開(kāi)關(guān),開(kāi)關(guān)名是字符串,值是布爾值。直接用這個(gè)類,我們不需要刻意去寫個(gè)類,再弄個(gè)靜態(tài)字段來(lái)當(dāng)全局變量了,更何況靜態(tài)成員是不能跨 AppDomain 共享值的,如果多線程還得考慮同步。
在 AppContext 中老周會(huì)設(shè)置一個(gè)開(kāi)關(guān),名為 running,如果是 true,說(shuō)明程序在運(yùn)行;若為 false,則說(shuō)明程序要退出了,就不會(huì)再漸變了。
因?yàn)檫@個(gè)漸變過(guò)程會(huì)持續(xù)幾秒時(shí)間甚至更長(zhǎng),如果程序要退出,就不要再循環(huán)了,而是趕緊終止操作。
start 和 end 表示起始顏色和終點(diǎn)顏色,steps 表示要進(jìn)行多少步(循環(huán)數(shù)),delay_ms 參數(shù)表示每一輪循環(huán)之間的延時(shí)。
回到主程序,調(diào)用測(cè)試。
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個(gè)燈珠 Ws28xx ws = new Ws2812b(device, 30); GradLeds grdled = new(ws); int steps = 90; //90個(gè)循環(huán) int delay = 25; //延時(shí)(毫秒) // 設(shè)置運(yùn)行狀態(tài) AppContext.SetSwitch("running", true); // 按Ctrl+C時(shí)程序要退出,處理一下 Console.CancelKeyPress += async (_, e) => { e.Cancel = true; //阻上程序馬上退出 // 關(guān)閉開(kāi)關(guān),表示程序不再運(yùn)行了 AppContext.SetSwitch("running", false); await Task.Delay(150); //保險(xiǎn)一點(diǎn),等一會(huì)兒 e.Cancel = false; //告訴系統(tǒng),可以退出了 }; // 主循環(huán) while (AppContext.TryGetSwitch("running", out bool b) && b) { // 從紅變藍(lán) grdled.Run(Color.Red, Color.Blue, steps, delay); // 從藍(lán)變黃 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();
最后這兩句是當(dāng)退出 while 循環(huán)后,讓所有燈珠熄燈(黑色表示燈滅)。
ws.Image.Clear(Color.Black); ws.Update();
好了,咱們來(lái)看看效果,這個(gè)效果應(yīng)該能接受。
其他動(dòng)畫算法,大伙伴們不妨自己動(dòng)手去試試。算法不一定要從網(wǎng)上抄,可以根據(jù)自己的理解去設(shè)計(jì)??梢宰龀鲎约旱膭?chuàng)意,你愛(ài)咋玩就咋玩。
相關(guān)文章
詳解Asp.Net母版頁(yè)元素ID不一致的體現(xiàn)
由于總體排版和設(shè)計(jì)的需要,我們往往創(chuàng)建母版頁(yè)來(lái)實(shí)現(xiàn)整個(gè)網(wǎng)站的統(tǒng)一性,這篇文章主要介紹了詳解Asp.Net母版頁(yè)元素ID不一致的體現(xiàn),感興趣的小伙伴們可以參考一下2018-11-11.Net 對(duì)于PDF生成以及各種轉(zhuǎn)換的操作
這篇文章主要介紹了.Net 對(duì)于PDF生成以及各種轉(zhuǎn)換的操作,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06Asp.Net服務(wù)器發(fā)送HTTP標(biāo)頭后無(wú)法設(shè)置內(nèi)容類型的問(wèn)題解決
這篇文章主要給大家介紹了Asp.Net服務(wù)器發(fā)送HTTP標(biāo)頭后無(wú)法設(shè)置內(nèi)容類型問(wèn)題的解決方法,文中介紹的非常詳細(xì),對(duì)大家具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起看看吧。2017-05-05Coolite Cool Study 3 MVC + Coolite 的實(shí)現(xiàn)代碼
啊,開(kāi)始以為MVC+Coolite結(jié)合的例子沒(méi)什么難度,但原來(lái)Coolite在MVC中需要特定設(shè)置一下某些屬性才行,費(fèi)了兩個(gè)小時(shí)才算大功告成,具體請(qǐng)看下文。還是先把這個(gè)例子的效果貼上來(lái)再說(shuō)。2009-05-05.net cs后臺(tái)刷新aspx頁(yè)面的四種方式
這篇文章主要介紹了.net cs后臺(tái)刷新aspx頁(yè)面的四種方式,需要的朋友可以參考下2014-03-03一步一步學(xué)asp.net Ajax登錄設(shè)計(jì)實(shí)現(xiàn)解析
做一個(gè)登錄,擁有自動(dòng)記住賬號(hào)和密碼的功能,要保證安全性,ajax,無(wú)刷新,良好的用戶體驗(yàn).(母板頁(yè))2012-05-05iis的http 500內(nèi)部服務(wù)器錯(cuò)誤的解決
iis的http 500內(nèi)部服務(wù)器錯(cuò)誤是我們經(jīng)常碰到的錯(cuò)誤之一,它的主要錯(cuò)誤表現(xiàn)就是asp程序不能瀏覽但htm靜態(tài)網(wǎng)頁(yè)不受影響。另外當(dāng)錯(cuò)誤發(fā)生時(shí),系統(tǒng)事件日志和安全事件日志都會(huì)有相應(yīng)的記錄2007-04-04密碼綁定至密碼文本框中(TextMode設(shè)為Password)
一般情況之下TextBox的TextMode設(shè)為Password話,我們想在后臺(tái)(.cs)綁定一個(gè)值至此文本框,是無(wú)法實(shí)現(xiàn)的,如果一定要綁定值的話,該如何實(shí)現(xiàn)呢?,本文將告訴你實(shí)現(xiàn)方法,感興趣的朋友可以參考下2013-01-01