.NET 與樹莓派WS28XX 燈帶的顏色漸變動畫效果的實現

在上一篇水文中,老周演示瞭 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();

好瞭,咱們來看看效果,這個效果應該能接受。

其他動畫算法,大夥伴們不妨自己動手去試試。算法不一定要從網上抄,可以根據自己的理解去設計。可以做出自己的創意,你愛咋玩就咋玩。

推薦閱讀: