A Day In The Life

とあるプログラマの備忘録

最近なにやってったっけ

最近の個人活動

忘れないようにメモ

Webサービス開発

Web Tools

Texを出力するサービスとか地域標準メッシュやQuadkey、Geohashの可視化サービスなど。世の中インフレだけど年収が上がる気配が1mmもないので収入増を狙って作ってみた。しばらくはこのサービスを拡充したい。

tools.9revolution9.com

OSS開発

anonypy

Pythonのk-匿名化ライブラリ。勢いで開発して放置してたにも関わらず思ったよりもスターがついていてびっくり

osrm-server-demo

OSRMというOpen Street Mapの道路情報を使ったマップマッチや経路探索ができるライブラリを利用してマップマッチサーバを立てるデモ。Node.js(Java Script)で開発

quadkey-tilemath

TypeScriptで開発した緯度経度をQuadkeyに変換するライブラリ。個人的にはQuadkeyもっと日本で流行ってほしい。

xgo

Goの便利関数を集めたライブラリ。開発自体はそこそこ前だけど細々と機能追加を続けている。

試験

2022年の秋に情報処理安全確保支援士試験を受けた。人生初の高度試験合格だったのでものすごく嬉しかった。登録セキュスペになるかどうかはまだなにも考えていない。

zenn.dev

自己研鑽的な

ここ1年くらい高校数学の勉強をしている。やっと数3までいった。早く大学数学が勉強できるようになりたい。今読んでる本はこちら

k-匿名化ライブラリを開発しています

k-匿名化ライブラリ

個人活動でPythonでk-匿名化するライブラリを開発しています。l-多様化とt-近似化にも対応しています。まだまだ改善点たくさんありますが少しづつ良いライブラリにしていきたいとおもいます。 github.com

開発にあたりこちらの書籍がめちゃくちゃ参考になりました。

フロー情報はブログに、ストック情報はZenn.devに投稿することにしました。

今後技術記事はZenn.devに投稿します。

長年ブログに技術記事をかいてきましたが、フロー情報とストック情報を分けて投稿することにしました。 今後、技術記事はZenn.devに投稿します。

最近投稿したZenn.devの記事

最近は主にGoの記事を書いています。

引き続き、本ブログをよろしくお願いします。

引き続き、お気持ち表明とか、近況報告、おすすめの本情報なんかはこちらのブログに書いていきます。

GCPマネージドサービス向けLoggerライブラリを開発しました。

GCPマネージドサービス(GAE/GKE/Cloud Run)向けのGoのLoggerライブラリを開発しました。 github.com

Google App Engine 第1世代のロガー(google.golang.org/appengine/log)からの移行ができるように関数のインフタフェースを揃えてあります。

CABTMIDILocalPeripheralViewControllerを使わずにMIDI over BLEでアドバータイズする方法

ワイヤレスMIDI接続をしてアプリから他の端末にMIDIデータを送信したい

CoreAudioKit の CABTMIDILocalPeripheralViewController を使うとiPhone(またはiPad)アプリ上に MIDI over Bluetooth LE のアドバータイズの設定画面を表示することができます。

CABTMIDILocalPeripheralViewControllerを使うと設定画面が立ち上がりアドバータイズ設定ができるようになります

上記画面の Advertise MIDI Service の項目をオンにするとアプリ側を BLE ペリフェラルとしてアドバータイズすることができます。

CABTMIDILocalPeripheralViewControllerを使わずにアドバータイズしたい

SwiftUI や SpriteKit で画面構築をしていると CABTMIDILocalPeripheralViewController のような UIViewController を継承したクラスを使うのは少し大変です。そこで CABTMIDILocalPeripheralViewController を使わずに直接 CoreBluetooth を使ってMIDI over BLE のアドバータイズができないか調べてみました。その結果サービスのIDとキャラクタリスティックのIDに特定の値を指定するとできることがわかりました。 以下は SpriteKit のシーンで BLE のアドバータイジングをするコードです。

import SpriteKit
import CoreBluetooth

class GameScene: SKScene {
    var manager: CBPeripheralManager!
    var service: CBMutableService!
    // MIDI Service UUID
    let serviceID = CBUUID(string: "03B80E5A-EDE8-4B33-A751-6CE34EC4C700")
    // MIDI I/O Characteristic UUID
    let characteristicID = CBUUID(string: "7772E5DB-3868-4112-A1A9-F2669D106BF3")

    override func didMove(to view: SKView) {
        super.didMove(to: view)
        self.manager = CBPeripheralManager(delegate : self, queue : nil, options: nil)
    }
}

extension GameScene: CBPeripheralManagerDelegate {
    // ペリフェラルの状態通知
    func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
        guard peripheral.state == .poweredOn else {
            print("error: \(peripheral.state)")
            return
        }
        
        print("bluetooth pwoer on")
        // サービスとキャラクタリスティックの追加
        self.service = CBMutableService(type: self.serviceID,
                                       primary: true)

        let properties: CBCharacteristicProperties = [.notify, .read, .writeWithoutResponse]
        let permissions: CBAttributePermissions = [.readable, .writeable]
        let characteristic = CBMutableCharacteristic(type: self.characteristicID,
                                                     properties: properties,
                                                     value: nil, permissions: permissions)
        
        self.service.characteristics = [characteristic]
        self.manager.add(self.service)
    }
    // サービス追加の成功失敗通知
    func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error?) {
        guard (error == nil) else {
            print("Add service failed")
            return
        }
        print("Add service succeeded")
        
        // アドバタイズ開始
        let advertisementData = [CBAdvertisementDataLocalNameKey: "Penguin Drums",
                                 CBAdvertisementDataServiceUUIDsKey: [self.serviceID]] as [String : Any]
        manager.startAdvertising(advertisementData)
        
        print("Service starts advertising!")
    }
}

サービスのIDには 03B80E5A-EDE8-4B33-A751-6CE34EC4C700 をキャラクタリスティックのIDには 7772E5DB-3868-4112-A1A9-F2669D106BF3 を指定します。またキャラクタリスティックのプロパティに Read, Write, Notify を指定します。

参考記事

SpriteKit SceneをiPhone/iPadのホームバーに対応させる

3年間放置していたiOSアプリをホームバーに対応させた話

かれこれ3年くらい放置していた iPhone/iPad 両対応のアプリをホームバーに対応させようと思ったら結構大変だったのでその記録です。 ちなみに放置していたアプリはこちらです。

ピアノ - 即興ピアノ

ピアノ - 即興ピアノ

  • taisuke fujita
  • ミュージック
  • 無料
apps.apple.com

対応前の設定はこんな感じ

4.7インチの iPhone(6, 6S, 7, 8, 第2世代SEなど1334 * 750ピクセル)をベースにiPadにも対応させるため、SKScene の幅を1334ピクセル高さを1000.5ピクセル(4:3)に設定し、アンカーポイントを(0, 0)に設定してました。 以前の画面 4:3の iPad を表示領域として、スプライトを16:9の iPhone の領域におさまるように配置するイメージです。この設定だと iPhone だとぴったりで iPad だと上下に余白ができます。 シーンのスケールモードの設定は aspectFill に設定してました。

class GameViewController: UIViewController {
    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        
        if let view = self.view as! SKView? {
            // Load the SKScene from 'GameScene.sks'
            if let scene = GameScene(fileNamed: "GameScene") { 
                // Set the scale mode to scale to fit the window
                scene.scaleMode = .aspectFill
                // Present the scene
                view.presentScene(scene)
            }
        }
    }
}

ホームバーの無い iPhone 8iPad Pro 第2世代では問題なく表示できていました。しかし iPhone X 以降(またはiPad Pro 11, 12.9インチ)のホームバー付きの端末で表示させる以下のようになりました。 はみ出た 全体が拡大されてスプライトがはみ出ています。

ホームバーに対応する手順

iPhone X 以降(またはiPad Pro 11, 12.9インチ)のホームバーに対応する手順は以下のようになります。

  1. SKSceneのサイズを16:9に合わせて変更する
  2. アンカーポイントを(0, 0)から(0.5, 0.5)に変更する
  3. presentSceneを呼び出すタイミングでSceneのサイズを調整する

手順1と2でシーンエディタの修正をします。手順3でプログラムの修正をします。

SKSceneのサイズを16:9に合わせて変更する

SKScene のサイズを16:9に合うように変更します。私の場合は1334 * 1000.5だった設定を1334 * 750に変更しました。16:9であれば960 * 540だったり、2208 * 1242だったりでも問題ないようです。作成したスプライトの画像サイズに合うように適宜変更すれば良いようです。ポイントは縦横比が16:9になるように SKScene のサイズを設定することです。

Sceneのアンカーポイントを中央にする

16:9 の iPhone の画面にぴったり合うようにスプライトを配置すると iPhone 11 系では左右に余白ができ、iPad 系では上下に余白ができます。この余白に影響を受けないようにするにはアンカーポイントを中央(0.5, 0.5)に設定する必要があります。シーンのアンカーポイントを変更するとスプライトの位置が変わってしまうので適宜スプライトの位置も変更します(地味にこの作業が一番めんどくさい)。

ここまでの修正を反映するとこんな感じです

修正後の画面

presentSceneを呼び出すタイミングでSceneのサイズを調整する

UIView の presentScene メソッドを呼ぶタイミングで、UIView のサイズ(機種によってサイズがかわる)と SKScene のサイズの比率を計算して SKScene のサイズを調整してあげます。スケールモードの設定は aspectFill から aspectFit に変更します。

class GameViewController: UIViewController {
    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        
        if let view = self.view as! SKView? {
            // Load the SKScene from 'GameScene.sks'
            if let scene = GameScene(fileNamed: "GameScene") {
                var factor = view.frame.size.height / scene.size.height
                // iPad
                if view.frame.size.width / factor < scene.size.width {
                    factor = view.frame.size.width / scene.size.width
                }
                scene.size = CGSize(width: view.frame.size.width / factor,
                                       height: view.frame.size.height / factor)
                
                // Set the scale mode to scale to fit the window
                scene.scaleMode = .aspectFit
                // Present the scene
                view.presentScene(scene)
            }
        }
    }
}

手順を適用した結果はこちら

手順1から3を適用して修正した結果は以下のようになります。

iPhone 8

ホームバーなし(ホームボタンあり)の従来型の iPhone 画面です。 iPhone8

iPhone 11

ホームバーありの新型の iPhone 画面です。左右に余白ができています。 iPhone11

iPad Pro 11インチ

ホームバーありの新型の iPad 画面です。上下に余白ができています。 iPad Pro

参考記事

Time型をJSONに変換するとtime.Locationの情報が欠落する場合の対処法

Time 型のフィールドを含む構造体を JSONエンコードJSON からデコードすると特定条件下で Time 型オブジェクトの time.Location の情報が失われることがあります。 以下のプログラムを見てください。

package main

import (
    "fmt"
    "time"
    "encoding/json"
)

type Hoge struct {
    Name string
    CreatedAt time.Time
}

func main() {
    time.Local = time.UTC
 
    // Hoge構造体を生成する
    h1 := Hoge{
        Name: "aaa", 
        CreatedAt: time.Unix(1593077427, 0),
    }

    // Jsonsに変換
    j, _ := json.Marshal(h1)
 
    // Jsonから復元する
    h2 := Hoge{}
    json.Unmarshal(j, &h2)
 
    fmt.Printf("h1の出力結果\n%#v\n", h1)
    fmt.Printf("h2の出力結果\n%#v\n", h2)
}

出力結果を確認すると、JSON 変換前の Hoge オブジェクト(変数名 h1)の CreatedAt フィールドは loc に (*time.Location)(0x5c7a40) がセットされているのに対し、JSON 変換後に復元した Hoge オブジェクト(変数名 h2)の CreatedAt フィールドは loc が nil になっています。

h1の出力結果
main.Hoge{Name:"aaa", CreatedAt:time.Time{wall:0x0, ext:63728674227, loc:(*time.Location)(0x5c7a40)}}
h2の出力結果
main.Hoge{Name:"aaa", CreatedAt:time.Time{wall:0x0, ext:63728674227, loc:(*time.Location)(nil)}}

これ %+v で出力しても違いがわからないので注意が必要です。%#v で出力してみないとわからないです。

time.Localにtime.UTCをセットした場合のみ起きる現象

JSON に変換すると Time 型フィールドの loc 情報が nil になってしまう現象は time.Localにtime.UTCをセットした場合のみ発生します。

func main() {
    time.Local = time.UTC
    ...
    ... その他、処理
    ...
}

以下のように UTC 以外のローカルタイムを設定している場合は発生しません。

func main() {
    loc, _ := time.LoadLocation("Asia/Tokyo")
    time.Local = loc
    ...
    ... その他、処理
    ...
}

ローカル時間をUTCにセットするとreflect.DeepEqualが常にfalse判定されてしまう

time.Location の情報が失われると、Time 型フィールドを含む構造体を reflect.DeepEqual で比較した時に意図通り判定されなくなります。以下のプログラムを見てください。

package main

import (
    "fmt"
    "time"
    "encoding/json"
    "reflect"
)

// Hoge構造体の定義は省略

func main() {
    time.Local = time.UTC
 
    // Hoge構造体を生成する
    h1 := Hoge{
        Name: "aaa", 
        CreatedAt: time.Unix(1593077427, 0),
    }

    // Jsonsに変換
    j, _ := json.Marshal(h1)
 
    // Jsonから復元する
    h2 := Hoge{}
    json.Unmarshal(j, &h2)
 
    result := reflect.DeepEqual(h1, h2)
    fmt.Printf("reflect.DeepEqual: %v", result)
}

result の出力結果は true ではなく false になります。

reflect.DeepEqual: false

対処方法

これに対処する方法はいくつか存在します。

  • Time.Equal メソッドを使って判定する
  • UTC に変換する
  • go-cmp を使って判定する
  • time.Local に nil をセットする

個別に対処方法を見ていきましょう

Time.Equal メソッドを使って判定する

こちらは time パッケージの Godoc に記載されている方法です。

result := h1.CreatedAt.Equal(h2.CreatedAt)
fmt.Printf("Time.Eqaul: %v", result)

出力結果は true になります。

Time.Eqaul: true

ただこの方法だと Time 型のフィールドを含む構造体同士を比較する場合はフィールドを一つずつ比較しないといけなくなります。

UTC に変換する

Time オブジェクトを生成するタイミングで UTC メソッドを使って UTC 変換に変換します。

func main() {
    time.Local = time.UTC
 
    // Hoge構造体を生成する
    h1 := Hoge{
        Name: "aaa", 
        // UTCに変換する
        CreatedAt: time.Unix(1593077427, 0).UTC(),
    }

    // Jsonsに変換
    j, _ := json.Marshal(h1)
 
    // Jsonから復元する
    h2 := Hoge{}
    json.Unmarshal(j, &h2)
 
    result := reflect.DeepEqual(h1, h2)
    fmt.Printf("reflect.DeepEqual: %v", result)
}

Hoge 構造体を生成するときに UTC メソッドを使って変換する必要がありますが結果はちゃんと true になります。

reflect.DeepEqual: true

Equal メソッドを使うよりも良さそうではありますが UTC メソッド呼び出しを忘れるとバグの温床になります。個人的にはあまりおすすめしません。

go-cmp を使って判定する

reflect.DeepEqual を使う代わりに、ユニットテストで構造体を比較する時の定番パッケージ go-cmp を使って比較します。

import (
    "fmt"
    "time"
    "encoding/json"
    "github.com/google/go-cmp/cmp"
    "reflect"
)

// Hoge構造体の定義は省略

func main() {
    time.Local = time.UTC
 
    // Hoge構造体を生成する
    h1 := Hoge{
        Name: "aaa", 
        CreatedAt: time.Unix(1593077427, 0),
    }

    // Jsonsに変換
    j, _ := json.Marshal(h1)
 
    // Jsonから復元する
    h2 := Hoge{}
    json.Unmarshal(j, &h2)
 
    result1 := reflect.DeepEqual(h1, h2)
    fmt.Printf("reflect.DeepEqual: %v\n", result1)
 
    result2 := cmp.Equal(h1, h2)
    fmt.Printf("cmp.Equal: %v", result2)
}

出力結果は以下のようになります。

reflect.DeepEqual: false
cmp.Equal: true

go-cmp は Time 型フィールドを比較する時、内部で Time.Equal メソッドを使って判定してくれるので reflect.DeepEqual が false 判定してしまう場面でも意図通り判定してくれます。

time.Local に nil をセットする

go-cmp も良いのですが Go の標準パッケージだけでなんとかするなら、main 関数で time.Local に nil をセットする方法があります。

package main

import (
    "fmt"
    "time"
    "encoding/json"
    "reflect"
)

// Hoge構造体の定義は省略

func main() {
    // nilをセットする
    time.Local = nil
 
    // Hoge構造体を生成する
    h1 := Hoge{
        Name: "aaa", 
        CreatedAt: time.Unix(1593077427, 0),
    }

    // Jsonsに変換
    j, _ := json.Marshal(h1)
 
    // Jsonから復元する
    h2 := Hoge{}
    json.Unmarshal(j, &h2)
 
    result := reflect.DeepEqual(h1, h2)
    fmt.Printf("reflect.DeepEqual: %v\n", result)
}

出力結果は以下のようになります。意図通り reflect.DeepEqual の結果が true になります。

reflect.DeepEqual: true

参考記事