イスタンブール行きたい

たまに書きたくなります

RaspberryPiでLEDライトが点かなくて詰まった

IoTに興味があり、RaspberryPi 4Bを買った。半導体不足でかなり割高だったが、まだしばらく割高な状態が続きそうで、いつまでも待っているのも勿体無いので、持っていた米国株をいくつか売って資金にし買うことにした。

あわせて、SunFounderのスターターキットも買ってみた。 SunFounderのキットは公式なドキュメントもあり、早速これに沿って勉強してみた。

最初のレッスンは、LEDを点滅させるというものだ。 参考書として「ラズパイ4対応 カラー図解 最新 Raspberry Piで学ぶ電子工作 作る、動かす、しくみがわかる! 」の本も買ってみたが、 この本でもLEDライトを点滅させるというのが最初の実践として載っていたので、LED点滅は基礎の基礎という位置づけになっていると思われる。

そんな基礎の基礎であるLED点滅だが、SunFounderのドキュメントの通りにやっているつもりだったが全く点滅してくれなかった。 ブレッドボードにワイヤや抵抗、LEDを挿す位置や順番がおかしいのかと思ったが、電流の流れを理解したうえでも問題はなさそうだった。

結論を言うと、なんてことはなく、ワイヤや抵抗器、LEDをブレッドボードに深く挿しすぎていたようで、ブレッドボード内部の線に当たっていなかったことが原因のようだった。 当初、しっかり挿そうと深めに挿していたが、それだとうまくブレッドボードの内部の線と交わっていなかったようだ。 浅めに挿すことで、LEDの点滅を確認できた。 たしかに、適度な長さにニッパでワイヤや抵抗器、LEDの線を切ると良いとは参考書に書いてあったが、線を切ってしまうという行為は初心者にはなかなか勇気が要る。 本来は、浅めに挿すよりもニッパで適度な長さに切って深く挿すのが適切なのだろう。

わかる人からすればそんなの当たり前じゃないかと言うレベルな話だと思うが、深く挿しすぎるとブレッドボードの内部の線と交わらないという情報は意外に見かけなかった。 それぐらいに当たり前のことかもしれないが、素養がないとそれもわからないのでこの記事でメモ。 基礎の基礎でうまくいかずちょっと心が折れそうになったが、とりあえずスタート地点に立つことができて一安心。

InfluxDBを触ってみるメモ

IoTにちょっと興味あって(全く未経験)、Rasberry Piを買ってみようかと思ったけど昨今の情勢もあってか値段が高騰している。 そのうち買ってみるつもりだが、安くなるのを待ちつつIoT関連の技術にちょっと触れてみたい。

IoTのプロダクトだと時系列データベースを使っているものが多いらしい。 センサーデータみたいなものをどんどん格納していくのだと思うが、時系列データベースというものが全く想像つかないので、代表格と思われるInfluxDBをちょっと触ってみる。

https://hub.docker.com/_/influxdb のAutomated Setupの部分を参考に、とりあえずDockerでInfluxDBのセットアップ込みですぐに使える状態のものを使う。

docker run -d -p 8086:8086 --name influxdb \
      -v $PWD/data:/var/lib/influxdb2 \
      -v $PWD/config:/etc/influxdb2 \
      -e DOCKER_INFLUXDB_INIT_MODE=setup \
      -e DOCKER_INFLUXDB_INIT_USERNAME=my-user \
      -e DOCKER_INFLUXDB_INIT_PASSWORD=my-password \
      -e DOCKER_INFLUXDB_INIT_ORG=my-org \
      -e DOCKER_INFLUXDB_INIT_BUCKET=my-bucket \
      -e DOCKER_INFLUXDB_INIT_RETENTION=1w \
      -e DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=my-super-secret-auth-token \
      influxdb:latest

BUCKETというものがRDBでいうところのdatabaseっぽい。

コンテナの中に入ってみる。

docker container exec -it influxdb sh

BUCKETのリスト出力。 Docker起動時にDOCKER_INFLUXDB_INIT_BUCKETで指定したmy-bucketが確認できる。

# influx bucket list
ID          Name        Retention   Shard group duration    Organization ID     Schema Type
07c17e53661ef6bf    _monitoring 168h0m0s    24h0m0s         b1326f6776364a21    implicit
bad1a9eb45b1fcb0    _tasks      72h0m0s     24h0m0s         b1326f6776364a21    implicit
213640f9d9ad6b19    my-bucket   168h0m0s    24h0m0s         b1326f6776364a21    implicit

InfluxDBが起動していると、ブラウザで http://localhost:8086/signin にアクセスできるようになっている。 以下のようなログイン画面になっている。

Docker起動時に指定したUSERNAMEとPASSWORDを入力してログインすると、以下のような画面に

とりあえずデータを入れてみる。 CLIからも入れられるみたいだが、せっかくGUIの画面を見ているので、Exampleでも紹介されているCSVをちょっといじった以下のようなCSVをアップロードする。 (後にデータ参照のためダッシュボードを作るときにデータが少なくて面白くなかったので、同じようなデータを適当に追加した)

#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,string,string,double,string,string
#group,false,false,true,true,false,true,false,false,true,true
#default,,,,,,,,,,
,result,table,_start,_stop,_time,region,host,_value,_measurement,_field
,,0,2023-02-22T20:50:00Z,2023-02-22T20:51:00Z,2023-02-22T20:50:00Z,east,A,15.43,cpu,usage_system
,,0,2023-02-23T20:50:00Z,2023-02-23T20:51:00Z,2023-02-23T20:50:20Z,east,B,42.25,cpu,usage_system
,,0,2023-02-24T20:50:00Z,2023-02-24T20:51:00Z,2023-02-24T20:50:40Z,east,C,39.62,cpu,usage_system
,,1,2023-02-25T20:50:00Z,2023-02-25T20:51:00Z,2023-02-25T20:50:00Z,west,A,62.73,cpu,usage_system
,,1,2023-02-26T20:50:00Z,2023-02-26T20:51:00Z,2023-02-26T20:50:20Z,west,B,3.83,cpu,usage_system
,,1,2023-02-27T20:50:00Z,2023-02-27T20:51:00Z,2023-02-27T20:50:40Z,west,C,83.62,cpu,usage_system

#datetypeは文字通りデータ型を指す。 #groupIndicates the column is part of the group key.とのこと。グループキーのメンバーかどうかということだが、どういうことかよくわからない。 RDSでいうところのGROUP BYでキーになるような項目的な感じ? #defaultはデフォルト値

トップ画面から「LOAD YOUR DATA」を選ぶと、データロード方法を選ぶ画面に。 「Flux Annotated CSV」をクリックすると、ファイルアップロード画面に遷移するので上記のCSVをアップロードする。

アップロードできた

データを入れられたので、入れたデータを参照してみる。

トップページに戻り、「BUILD A DASHBOARD」からダッシュボード作成画面を開く。

「ADD CELL」より要素を追加。 GraphやGaugeなど図のタイプを選べるが、デフォルトではGraphになっている。

画面左下でBucketを選択し、GUIでmeasurement(測定データ)、その他項目を絞り込めるようになっている。 右側のSUBMITを押すと、グラフが表示される。

ちなみに、「SCRIPT EDITOR」を押すと、GUIで選んだ条件をクエリとして表すとどうなるのか確認できた。

from(bucket: "my-bucket")
  |> range(start: v.timeRangeStart, stop: v.timeRangeStop)
  |> filter(fn: (r) => r["_measurement"] == "cpu")
  |> filter(fn: (r) => r["_field"] == "usage_system")
  |> filter(fn: (r) => r["host"] == "A" or r["host"] == "B" or r["host"] == "C")
  |> filter(fn: (r) => r["region"] == "east" or r["region"] == "west")
  |> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
  |> yield(name: "mean")

ごくシンプルだがInfluxDBがどのようなものか何となくのイメージはできた。 センサーデータなどを取得して異常検知などをするIoTとの相性が良いことはわかった。

【Laravel】テストでコンストラクタのパラメータをmockにする

以下のようなコンストラクタにパラメータのクラス(FunctionParamA)が存在するクラス(ClassX)があったとする。 パラメータのクラスを実行するメソッドのテストを書きたかったが、パラメータのクラスをmockにする方法がわからず調べたのでメモ。

class ClassX
{
    private $functionParamA;
    
    public function __construct(FunctionParamA $functionParamA)
    {
        $this->functionParamA = $functionParamA;
    }

    public function testMethod(int $id)
    {
        // 色々な処理
        call_user_func($this->functionParamA, $id);
        // 色々な処理
    }
}
class FunctionParamA
{
    public function __invoke(int $id)
    {
        // 色々な処理
    }
}

テストコードで以下のようにすればよかった

class ClassXTest extends BaseTestCase
{
    public function test_正常系()
    {
        // 略
    
        $mockFunctionParamA = Mockery::mock(FunctionParamA::class); // パラメータのクラスのMock作成
        $mockFunctionParamA->shouldReceive('__invoke'); // パラメータのクラスのメソッド設置
        $this->app->instance(FunctionParamA::class, $mockFunctionParamA); // パラメータのクラスのインスタンスをセット
        $this->app->make(ClassX::class); // そのうえでテスト対象のクラスを上書き

        $response = $this->postJson('/api/XXXXX', [
            'id' => 1
        ]);
        $response->assertStatus(200);
    }
}

【React】テキストからURL、改行を探してaタグ、brタグに変換

地味にちょっと苦労したのでメモ チャットUIみたいなやつで、テキスト中にURLが存在すれば色を変えたかったのでclassをつけるためaタグにしたかったのと、\nが存在すれば<br>に変換したかった

const URL_REGEXP_CONTAINS = /(https?:\/\/[\w/:%#\$&\?\(\)~\.=\+\-]+)/g
const NEW_LINE_REGEXP = /(\n)/g

const textArray = text.split(URL_REGEXP_CONTAINS).map(t => {
            return t.split(NEW_LINE_REGEXP)
}).flat()

 return textArray.map(t => {
    if (NEW_LINE_REGEXP.test(t)) {
        return React.createElement('br')
    }
    if (URL_REGEXP.test(t)) {
        return React.createElement('a', {
            href: t,
            target: "_blank",
            referrer: "noreferrer",
                style: {
                    "color": "#f32840",
                    "text-decoration": "underline",
                    "text-decoration-color": "#f32840",
                }
        }, t)
    }
    return t
})

【Go】並行処理の「拘束」で安全に並行処理を扱う

オライリー「Go言語による並行処理」を参考に、 実務で使ってみてなるほどと思った内容です。 今日初めて並行処理を書いたのでまだまだ浅い理解ですがメモとしてまとめてみます。

拘束とは

拘束は、チャネルの読み書きを制限することにより、安全にチャネルを扱えるようにするという考え方です。 チャネルの扱いには注意が必要で、例えばチャネルに対する不適切な書き込みや、 チャネルを閉じる作業の漏れや重複により、デッドロックやpanicが起きてしまう可能性があります。 そうしたことを防ぐために、並行処理を関数にまとめてチャネルに対する権限を制限し、 関数の呼び出し側が安全に並行処理を扱えるようにするという考え方です。

コード

「拘束」を使わない場合

package main

import (
    "fmt"
    "time"
)

type processAResult struct {
    Message string
    Error   error
}

type processBResult struct {
    Message string
    Error   error
}

func main() {
    start := time.Now()
 
    channelA := make(chan processAResult, 1) // 関数の外でchannelを定義
    go func() {
        time.Sleep(time.Second * 5)
        channelA <- processAResult{
            Message: "AAAAA",
            Error:   nil,
        }
    }()

    channelB := make(chan processBResult, 1)
    go func() {
        time.Sleep(time.Second * 3)
        channelB <- processBResult{
            Message: "BBBBB",
            Error:   nil,
        }
    }()

    // 関数の外でchannelの受信
    resultA := <-channelA
    resultB := <-channelB
  
    // channelを閉じる
    close(channelA)
    close(channelB)

    if resultA.Error != nil {
        fmt.Println(resultA.Error.Error())
    }
    fmt.Println(resultA.Message)
 
    if resultA.Error != nil {
        fmt.Println(resultB.Error.Error())
    }
    fmt.Println(resultB.Message)
 
    fmt.Println(time.Since(start))
}

ちょっと誇張した書き方かもしれませんが、関数の外側でチャネルを定義し、 関数の呼び出し側が自分でchannelを受け取って、自分でchannelを受け取らなければなりません。 デッドロックやpanicなど思わぬ結果になりやすい構造と言えます。 次に、「拘束」を使ってみます。

「拘束」を使った場合

package main

import (
    "fmt"
    "time"
)

type processAResult struct {
    Message string
    Error   error
}

type processBResult struct {
    Message string
    Error   error
}

func main() {
    start := time.Now()
 
    processA := func() <-chan processAResult { // 読み込み専用のチャネルを返す
        channelA := make(chan processAResult, 1)  // チャネルの初期化
        go func() {
            defer close(channelA)  // チャネルを閉じる
            time.Sleep(time.Second * 5)
            channelA <- processAResult{
                Message: "AAAAA",
                Error:   nil,
            }
        }()
        return channelA    
    }
 
    processB := func() <-chan processBResult {
        channelB := make(chan processBResult, 1)
        go func() {
            defer close(channelB)
            time.Sleep(time.Second * 5)
            channelB <- processBResult{
                Message: "BBBBB",
                Error:   nil,
            }
        }()
        return channelB
    }
 
    // 読み込み専用のチャネルを引数に取る
    getProcessAFinalResult := func(resultA <-chan processAResult) processAResult {
        var result processAResult
        for v := range resultA {
            result = v
        }
        return result
    }
 
    getProcessBFinalResult := func(resultB <-chan processBResult) processBResult {
        var result processBResult
        for v := range resultB {
            result = v
        }
        return result
    }
 
    processAResult := processA()
    processBResult := processB()
 
    finalResultA := getProcessAFinalResult(processAResult)
    finalResultB := getProcessBFinalResult(processBResult)

    if finalResultA.Error != nil {
        fmt.Println(finalResultA.Error.Error())
    }
    fmt.Println(finalResultA.Message)
 
    if finalResultB.Error != nil {
        fmt.Println(finalResultB.Error.Error())
    }
    fmt.Println(finalResultB.Message)
 
    fmt.Println(time.Since(start))
}

先ほどの「拘束」を使わない例とほぼ同じ処理を、「拘束」を使って書き直してみました。

以下のチャネルに対する4つの操作が関数に閉じ込められ、関数の呼び出し側はチャネルに対する操作をしていません。 (1) チャネルの生成 (2) チャネルの書き込み (3) チャネルのクローズ (4) チャネルの読み込み

(1) チャネルの生成、(2) チャネルの書き込み、(3) チャネルのクローズ はprocessA, processBという関数に閉じ込められました。

processA := func() <-chan processAResult { // 読み込み専用のチャネルを返す
        channelA := make(chan processAResult, 1)  // (1) チャネルの生成
        go func() {
            defer close(channelA)  // (3) チャネルのクローズ
            time.Sleep(time.Second * 5)
            channelA <- processAResult{ // (2) チャネルの書き込み
                Message: "AAAAA",
                Error:   nil,
            }
        }()
        return channelA 
}

(4) チャネルの読み込みはgetProcessAFinalResult, getProcessBFinalResultという関数に閉じ込められました。

getProcessAFinalResult := func(resultA <-chan processAResult) processAResult {
        var result processAResult
        for v := range resultA { // (4) チャネルの読み込み
            result = v
        }
        return result
}

チャネルに対する操作が関数に閉じ込められたことは、「拘束」、すなわちチャネルに対する権限の制限です。 これにより、関数の呼び出し側は一切チャネルを操作する必要がなくなります。

具体的には以下のような権限の制限となります。 - 関数processA, processBの中でチャネルの初期化を行うことで、チャネルへの書き込み権限を制限、 他のgo routineが意図せずチャネルへの書き込みをしてしまうことを防いでいる - 関数processA, processBは読み込み専用のチャネルを返すことで、呼び出し側はチャネルの呼び出ししかできない - 関数getProcessAFinalResult, getProcessBFinalResultは読み込み専用のチャネルを引数に取るので、 チャネルに対しては読み込みしかしないことが明示的になる

このように、「拘束」を使うことで一定程度安全に並行処理を扱うことができます。

Node12系 AWS LambdaでHTMLをPDFに変換しようとしたらいろいろハマった

Node8系でwkhtmktopdfを使ってHTMLをPDFに変換するLambdaを使っていたのだが、Node8系で動いていた。 Node8系がサポートされなくなるということで、12系にそのままあげたら動かなくなってしまったのでNode12系でHTMLをPDFに変換するLambdaを作り直す必要が出てきた。

HTMLをPDFに変換するLambdaについては結構多くの記事が見つかったが、なかなか上手くいかなかった。

やりたかったこと

Lambdaで日本語を含むHTMLをPDFに変換し、S3に保存する

試したが上手くいかなかった方法

  • wkhtmlpdf
    • 自分が見つけられなかっただけかもしれないが、Node12系でも問題なく動くソースを見つけられなかった
  • html-pdf
    • phantomjsの128エラーでちっとも動かなかった
  • puppeteer
    • 動きそうな気配はあったが、node_modulesのサイズが大きすぎてLambdaの最大ソースサイズをオーバーしてしまった

上手く行った方法

使用モジュール

chrome-aws-lambda を使った。 puppeteer-coreもnpm installする必要があるが、puppeteerだと大きすぎるから必要なcoreだけ使ってるっぽい。

注意点

  • Lambdaのメモリ設定を512MBにする必要があった
    • 最初256MBにしていて、browserがlaunchできなかったいうようなエラーが出て、chrome-aws-lambdaもダメかあと思っていたところBUG報告で512MBにしたらできたで、って書いてあった
  • .fontsをちゃんとzipファイルに含める
    • Lambda環境に日本語フォントはない。自分で.fontsとして読み込ませる必要があるが、日本語が反映されへんな〜と思ったら.fontsをzipファイルに含むのが漏れていたというオチだった

コード

構造

┣ pdfGenerator.js
┣ package.json
┣ package-lock.json
┣ .fonts
  ┣ ipaexg.ttf
  ┣ ipaexm.ttf

コード

/* Lambda環境でも日本語フォントを使えるようにするには.fontsを読み込ませるためにHOMEを設定する必要ある */
process.env['HOME'] = "/var/task";
process.env['PATH'] = process.env['PATH'] + ':' + process.env['LAMBDA_TASK_ROOT'];

const AWS = require("aws-sdk");
const S3 = new AWS.S3({ signatureVersion: "v4" });
const chromium = require("chrome-aws-lambda");

exports.handler = async function(event, context, callback) {
    try {
        if (!event.html) {
            callback("unable to get html");
            return;
        }

        const fileName = event.filename,
            tmpFileName = `/tmp/${Math.random().toString(36).slice(2)}.pdf`,
            bucket =  event.bucket,
            pageSize = event.pageSize || "A4",
            html = event.html;

        const executablePath = await chromium.executablePath,
            browser = await chromium.puppeteer.launch({
                args: chromium.args,
                defaultViewport: chromium.defaultViewport,
                executablePath,
                headless: chromium.headless,
                ignoreHTTPSErrors: true
            });

        const page = await browser.newPage();
        await page.setContent(html);
        const pdf = await page.pdf({
            path: tmpFileName,
            format: pageSize
        });

        browser.close();

        S3.putObject({
            Bucket: bucket,
            Key: fileName,
            Body: pdf,
            ContentType: "application/pdf"
        }, (error) => {
            callback(error);
        });
    } catch(e) {
        callback(e);
    }
};

参考リンク

https://github.com/alixaxel/chrome-aws-lambda/issues/82

Visual Studio Codeで入れてるExtensionメモ

昔はvimを使ってたけど、vimキーバインドが使えればなんでもいいかと思い、VSCodeを使うようになった 仕事では有償のWebStormを使ってるけど、重いけどさすがにインデックス力とかが強くて気が利き、リファクタリングがめちゃやりやすい。 だがプライベートで有償のは使えないのでVSコードのExtensitonを使っていい感じにやろう

  • 言語、フレームワーク
  • GitLens
    • 行単位でいつの誰のどのコミットかわかるようになる
  • Vim
    • 当然入れる 職場のエンジニアは誰もVimキーバインドを使ってないのだが、めんどくさくないのだろうか
  • IntelliJ IDEA Keybindings
    • あんま使いこなせてない。cmd+b で関数定義元に行きたいときぐらいしか使ってない
  • RainbowCSV
    • CSVの列がレインボー色になって見やすくなる
  • Auto Close Tag
    • HTMLとかXMLの閉じタグが勝手に出てきてくれる
  • Auto Rename Tag
    • HTMLのタグスペルミスを修正すると対応する閉じタグや始まりタグも治ってくれる
  • Bracket Pair Colorizer
    • {}に色をつけて対応する括弧がわかりやすくなる
  • indent-rainbow
    • インデントが七色になる
  • Path Interllisense
    • importのパスの予測とかだっけ
  • Prettier - Code formatter
    • コードのフォーマット
  • Trailing Spaces
    • 余計な空白を見つけてくれる