Stream Deckでswitchbotをいじりたかったので操作するためのサーバを実装してみた
Stream Deck購入
少し前にStream Deckというものを購入した。
社内SNSでこれを使って遊んでる人のつぶやき記事を見つけたので、クレカのポイントも余ってるので購入してみた。
買ったのはElgato Stream Deck Neoというもの。
Stream Deckとしてはかなり小型の部類で安価なので、ちょっとしたおもちゃとして遊ぶのはちょうどよい。
購入してからDiscordのミュート解除とか、音量調整とかで利用しており、かなり便利に利用している。
ページ機能があり、1ページ1ページでボタンの操作対象を変更することができる。
例えば1ページ目ではDiscord操作用ボタンとして機能し、2ページ目では音量、再生等操作の方に切り替わるといった具合だ。


Switchbot pluginにおける問題点
さて、このようにボタンで操作できると便利になるものはほかにもあって、それが我が家の場合はswitchbotでの照明操作、エアコン操作となる。
どうやら同じように考えていた同志がいたらしく、Stream Deck Plugin StoreにはSwitchbot用のプラグインがあった。
https://marketplace.elgato.com/product/switchbot-b0f7f3c2-9a44-448c-bdf1-a00ae99f68ff
しかし、悲しいことにSwitchBot APIの認証トークンの処理が変更され、本プラグインは動作しなくなっていた。
Switchbot APIの認証関係の変更箇所は以下を参照すること。
ざっくり説明すると今まではトークンと秘密鍵さえあればよかったが、今はそれらをもとに認証に必要な情報 (nonce) を生成してSwitchbot APIにリクエストを送って検証してもらう必要がある。
GitHub - OpenWonderLabs/SwitchBotAPI: SwitchBot Open API Documents
このような変更のせいで旧APIにしか対応してないプラグインでは動作しなくなっている。
対応方針
Stream DeckにはAPI Ninjaというpluginがあり、これでGET, POST, PUT, DELETEというようなリクエストを投げることができる。
このため何かしらのREST APIを持つサーバを作成して、そこに対してリクエストを投げると、Switchbot APIに操作を中継するというふうにすればよい。
https://marketplace.elgato.com/product/api-ninja-fd59edeb-e7e5-412f-91ef-304c3e03f035
認証トークン取得方法は上記リンク先に取得方法のコードがあるのでそれに基づいて取得する。
成果物
ということでできたのが以下のプログラム。
自宅にKubernetesがあるのでそれの上で動くようにhelm chart等に固めたり、トークン系はHashicorp Vault上から取得するようにとじゃっかん特殊なことをやっているが、バイナリをビルドして必要な環境変数を設定さえすればKubernetesがなくても簡単に動かすことができる。
ローカルでの動かし方はhttps://github.com/ABC10946/switchbot-middleware/tree/main/app/exampleを参照してほしい。
Kubernetes上のConfigMapで操作対象や操作するためのAPIを変更したかったためyamlファイルで設定をできるようにしている。
以下のように設定するとexample.com/light/turnonとするとライトが点灯するように設定できる。
独自操作としてtoggleというものを実装したが、これはこのAPIを叩くとOn, Off切り替えをしてくれるというもの。
ボタン一つでOn, Offさせたいというときに重宝するだろうということで実装してある。
switchbot-configuration: - name: "light-turnon" path: "/light/turnon" type: "turnOn" deviceIds: - "01-202304012328-87495896" - name: "light-turnoff" path: "/light/turnoff" type: "turnOff" deviceIds: - "01-202304012328-87495896" - name: "light-toggle" path: "/light/toggle" type: "toggle" deviceIds: - "01-202304012328-87495896" - name: allon path: "/all/turnon" type: "turnOn" deviceIds: - "01-202210180121-38128280" - "01-202304012328-87495896" - "70041D7EEE6A" - name: alloff path: "/all/turnoff" type: "turnOff" deviceIds: - "01-202210180121-38128280" - "01-202304012328-87495896" - "70041D7EEE6A" - name: aircon-toggle path: "/aircon/toggle" type: "toggle" deviceIds: - "01-202210180121-38128280"
操作している動画は以下
分かりづらいが部屋のライトが点灯したり、消灯したりしているのが見える。
gRPC学習レポート new-learning-1
1週間(2024/09/22 ~ 2024/09/29):gRPCについて学習したのでそのレポート的なものを記述する。
参考文献
スターティングgRPC | インプレス NextPublishing
gRPCとは?
RPC (Remote Procedure Call)を実現するための実装の一つ。googleが開発した。
RPCとは
リモートの機能を呼び出すための技術。似たようなものにRESTなどが挙げられる。
RESTとの違い
- HTTP/2による高速通信
- バイナリによる通信のため通信帯域が少なめ
- ストリーミング通信
- インターセプタによるgRPCのメイン処理前後への処理埋め込み (認証や入力検証に用いる)
利用手順
protocol buffers スキーマファイルの作成 (RESTでいうところのopenapiと似たようなものという認識)
各サーバ、クライアントの実装
protocol buffersスキーマファイルをまず作成、これをコンパイルすることで各サーバ、クライアント用のライブラリが作成される。
実装
ひとまずシンプルにクライアント、サーバを作ってクライアントからサーバ側にある関数を呼び出すことができるか確認をする。
そのためのシナリオとして以下のようなものを検討。
サーバ側で検索用関数を実装、クライアント側からその関数を呼び出してデータベースを検索し結果を取得する。データベースには名前、年齢、メールアドレスが記載されているということにする。
データベースと記載したが本記事では文字列である名前をキーにして詳細な情報が入っているmap[string]Personという型の変数をデータの参照元とする。
Personは以下のような型 (goのPerson型作成はprotocが自動的に行うためわざわざこの型を用意する必要はない。)
type Person struct { Name string Age int32 Email string }
実装したサンプルは以下のリポジトリを参照のこと。
記事投稿当時のcommitへのリンク
https://github.com/ABC10946/grpc-learning/tree/d3111a1297c23c2f401f0837bd5401579f94d565
スキーマ作成
まずはスキーマを作成する。
SearchでSearchRequestを飛ばすとSearchResponseが返ってくるサービスを作成。
SearchRequestには文字列型query、SearchResponseにはプロフィール情報が入るPerson型と発見したか否かを示すbool型変数が定義される。
ここで注意したいことは、このprotobufファイルで記述したものは単なるインターフェイスに過ぎないというもので、このインターフェイスをやり取りするためのライブラリがprotocにより自動生成される。
実際に行いたい処理は人間が実装していく必要がある。(例えばここではSearch関数を実装する必要がある。)
syntax = "proto3";
option go_package = "gen/api";
package simple.maker;
service SimpleSearchService {
rpc Search(SearchRequest) returns (SearchResponse) {};
}
message SearchRequest {
string query = 1;
}
message SearchResponse {
bool is_found = 1;
Person person = 2;
}
message Person {
string name = 1;
int32 age = 2;
string email = 3;
}
コンパイル
protoc --proto_path=. --go_out=. proto/simple.proto --go-grpc_out=.
サーバの実装
重要な部分のみを抜粋したコードが以下。
api.RegisterSimpleSearchServiceServer(s, &server{})でserverという構造体を指定しており、このserver構造体にSearch関数を実装している。
Register.*ServiceServerというものがprotocにより自動生成された関数であり、これの第二引数にはSearch関数がある構造体のinterfaceが指定されている。
このSearch関数こそが今回RPCされる対象の関数である。
このSearch関数自体もinterfaceのみ自動生成されており、これを満たすように内部を実装していく。
今回はSearchRequestを受け取ってそこからqueryを取得、そのquery文字列をキーにmap[string]Personから該当の人間のプロフィールを検索するというふうに実装する。
もちろんインターフェイス以外は、内部実装は自由にいじれるので他PostgreSQLなどとやりとりするORMを利用しても良い。
func (s *server) Search(ctx context.Context, req *api.SearchRequest) (*api.SearchResponse, error) { query := req.GetQuery() log.Printf("Query: %s", query) if data[query].Name != "" { person := data[query] return &api.SearchResponse{Person: &person, IsFound: true}, nil } return &api.SearchResponse{IsFound: false}, nil } func main() { port := 50051 lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) if err != nil { log.Fatalf("failed to listen: %v", err) } s := grpc.NewServer() api.RegisterSimpleSearchServiceServer(s, &server{}) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }
クライアントの実装
クライアント側ではgrpcのclientを初期化すればSearch関数を呼び出すことができる。
まるで別のライブラリを呼び出しているかのようにリモートサーバ上にある関数を呼び出すことができる。
func main() { flag.Parse() conn, err := grpc.NewClient("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatalf("failed to connect: %v", err) } defer conn.Close() c := api.NewSimpleSearchServiceClient(conn) r, err := c.Search(context.Background(), &api.SearchRequest{Query: *name}) if err != nil { log.Fatalf("failed to search: %v", err) } if r.GetIsFound() { log.Printf("Person found: %v", r.GetPerson()) return } else { log.Printf("Person not found") return } }
実行結果
実行してみるとjohnというユーザを検索してそのレスポンスが返ってくることが確認できる。
$ https://github.com/ABC10946/grpc-learning $ cd grpc-learning/simple $ cd server $ go run . -------別ターミナル $ cd grpc-learning/simple $ cd client $ go run . 2024/09/29 13:22:14 Person found: name:"john" age:20 email:"john@local" $ go run . -name jane 2024/09/29 13:22:18 Person found: name:"jane" age:20 email:"jane@local"
calicoの設定はサボるな
TL;DR
NetworkManagerとの競合を防ぐためNetworkManagerで一部インターフェイスはマネジメントしないように設定する。
calico daemonsetsのIP_AUTODETECTION_METHODをfirst-foundからcidrsに切り替えることでちゃんと通信先のNICを指定する。
タイトルの通り。
kubernetesクラスタを構築する際にcalicoの設定をサボった結果あとから痛い目に遭遇したのでそれの共有でもしていきます。
我が家のkubernetesはCNIにcalicoを使っており、特別な設定をせずにQuick Startそのまんまでデプロイしていた。
しばらくはそれでPodにちゃんとIPアドレスが割り当てられていたし、Ingressを設定しても問題なく疎通があった。
さて、しばらくはそんな感じでいくつかのPodを上げては壊しを繰り返しいろいろ実験していました。
そうするとやはりというかなんというか、リソース不足に陥るわけです。
特にメモリが不足しており、OOM Killが走り他のノードに割り当て直されそこでもOOM Killが走り・・・ということが起きていました。
というわけで普段踏み台としてしか使っていないそれなりにスペックのあるミニPCと普段はゲームくらいでしか酷使されないメインマシンをKubernetesクラスタに突っ込んでしまうことにしました。
普段踏み台とかメインとして使っているホストをKubernetesのノードとして動かす試みというのはあまりないわけですので、まあいろいろ面倒は出てきます。
その最たる例はcalicoでした。
calicoはCNI Container Network Interface Pluginと呼ばれるものでコンテナ間のネットワークを司るKubernetesのネットワークプラグインです。
このcalicoが例の新しく追加しようとしたノード上ではうまく動きませんでした。
その原因は以下2つです。
NetworkManagerとの競合
Interfaceの疎通不可能NICを自動選択したことによる疎通不可能状態
NetworkManagerとの競合
これはかなり有名なお話のようでcalicoの公式ドキュメントのTrouble Shootingのところに記載あるのでこちらを参照してもらえればなと思います。
https://docs.tigera.io/calico/latest/operations/troubleshoot/troubleshooting
NetworkManagerがcalicoのルーティングテーブルと競合した結果、calicoのルーティングの方を優先させようとしたと思われますがデフォルトゲートウェイのmetricがものすごい大きい数が割り当てられており、あらゆる通信がcalicoの方に流れるという問題が生じました。
その結果ホスト自体の通信が落ちてしまいSSHもつながらないとひどい状態に陥りました。
というわけで、そうならないようにNetworkManager側にはそれを是正するような設定を記載しました。
[keyfile] unmanaged-devices=interface-name:cali*;interface-name:tunl*;interface-name:vxlan.calico;interface-name:vxlan-v6.calico;interface-name:wireguard.cali;interface-name:wg-v6.cali
ひとまずNetworkManager関連の競合問題はこれで解決しました。
Interfaceの疎通不可能NICを自動選択したことによる疎通不可能状態
こちらは仕事の忙しさも相まってかなり長い期間悩まされてきましたが、昨日ようやく解決に至ったのでご報告いたします。
この問題はまず最初稀にIngressの疎通ができないな〜という問題から始まりました。
そのときは稀に通信ができない程度だったのでDNSが原因だろうかとcorednsの設定を見たりなんやらしていた記憶があります。
Pod自体の問題かとか、Serviceリソースの設定がなにかおかしいのかとかかなり的外れなことを調べていましたが、
なんだかんだいろいろ試していくうちに新しく追加したミニPCとメインマシンへのPodのみ通信ができなくなるということが分かりました。
その頃は、まあいろいろあって一部システムを止めていたこともあったので一旦ノードをcordonしてどう解決していこうか検討していこうということでお茶を濁しました。
その結果、いろいろ仕事が忙しかったり、なんか6月病みたいなものになって休日も何もできない日々がずーっと続いていたので放置していました。
k8s自体も放置していましたし、まだモニタリングシステムもなんの整備もしていないのでアラートが鳴るということもなかったわけです。
そうこう過ごしてるうちに、またおうちk8sをいじりたいという気持ちが昂ぶり、さあいじるか〜 -> あれリソース足らねえ -> あ、cordonしてたわ〜と長い間放置していた前の記憶を掘り起こし、重い腰上げて根本原因調査するか〜とはじめました。
お仕事でもKubernetesクラスタのメンテナンスや障害対応をしていたため、だいぶ見るべきところが見えてきて数ヶ月前になんとな〜く公式ドキュメント通りにデプロイしたcalicoが怪しいな〜とたどりつきました。
見てみると、なんと一部ホストでcalico-node-***というPodがちゃんと動いていないと
$ k get pods -n calico-system NAME READY STATUS RESTARTS AGE calico-kube-controllers-558b7d9cf4-rslz8 1/1 Running 313 (22h ago) 227d calico-node-28xr4 1/1 Running 23 (7h2m ago) 227d calico-node-45xkr 1/1 Running 0 10d calico-node-5qrz5 1/1 Running 16 (10d ago) 226d calico-node-6xjgq 1/1 Running 0 10d calico-node-fkf9d 1/1 Running 16 (10d ago) 227d calico-node-k62hq 1/1 Running 22 (8d ago) 76d calico-node-nnjkr 1/1 Running 25 (44d ago) 227d calico-node-smxkd 0/1 Running 0 38m calico-node-snq2d 1/1 Running 25 227d calico-typha-996fb9cc7-fgtms 1/1 Running 21 (44d ago) 227d calico-typha-996fb9cc7-m8ftm 1/1 Running 27 (10d ago) 227d calico-typha-996fb9cc7-tdg8k 1/1 Running 20 (44d ago) 227d csi-node-driver-4c62z 2/2 Running 42 (8d ago) 81d csi-node-driver-6blck 2/2 Running 26 (44d ago) 227d csi-node-driver-7rqxd 2/2 Running 0 10d csi-node-driver-cnzcp 2/2 Running 32 (10d ago) 226d csi-node-driver-d5mss 2/2 Running 27 (44d ago) 227d csi-node-driver-fldqr 2/2 Running 0 38d csi-node-driver-s2jpt 2/2 Running 32 (10d ago) 227d csi-node-driver-t6f4z 2/2 Running 0 10d csi-node-driver-xd8vc 2/2 Running 26 (44d ago) 227d
で、そのPodをdescribeしてみるとなーんかBIRDのconnectionがうまくいってないことが分かります。
Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 41m default-scheduler Successfully assigned calico-system/calico-node-smxkd to einsteinium Normal Pulled 41m kubelet Container image "docker.io/calico/pod2daemon-flexvol:v3.26.1" already present on machine Normal Created 41m kubelet Created container flexvol-driver Normal Started 41m kubelet Started container flexvol-driver Normal Pulled 41m kubelet Container image "docker.io/calico/cni:v3.26.1" already present on machine Normal Created 41m kubelet Created container install-cni Normal Started 41m kubelet Started container install-cni Normal Pulled 41m kubelet Container image "docker.io/calico/node:v3.26.1" already present on machine Normal Created 41m kubelet Created container calico-node Normal Started 41m kubelet Started container calico-node Warning Unhealthy 41m kubelet Readiness probe failed: calico/node is not ready: BIRD is not ready: Error querying BIRD: unable to connect to BIRDv4 socket: dial unix /var/run/calico/bird.ctl: connect: connection refused W0626 11:17:59.370689 24 feature_gate.go:241] Setting GA feature gate ServiceInternalTrafficPolicy=true. It will be removed in a future release. Warning Unhealthy 41m kubelet Readiness probe failed: calico/node is not ready: BIRD is not ready: Error querying BIRD: unable to connect to BIRDv4 socket: dial unix /var/run/calico/bird.ctl: connect: connection refused W0626 11:18:00.347606 62 feature_gate.go:241] Setting GA feature gate ServiceInternalTrafficPolicy=true. It will be removed in a future release. Warning Unhealthy 41m kubelet Readiness probe failed: 2024-06-26 11:18:07.119 [INFO][365] confd/health.go 180: Number of node(s) with BGP peering established = 0
さて、ここでまずはこのBIRDが使うであろうポート179が空いてるかなぁと見ていきますが案の定繋がりはするわけです。
https://docs.tigera.io/calico/latest/getting-started/kubernetes/requirements
$ curl *****:179 つながりはするけどホストのサービス側(calico)から落とされる $ telnet *****:179 上と同様
となると179ポート自体は閉じていない。
もちろんufw自体も停止しているので阻むものは何もありませんでした。
そうこうして調べていくうちに以下のstackoverflowにたどり着き、どうやらIP_AUTODETECTION_METHODを設定する必要があるということが分かりました。
jenkins - Kubernetes - Calico-Nodes 0/1 Ready - Stack Overflow
で、calicoインストール時にデプロイしたオペレータのコードを見ていくとnodeAddressAutoDetectionV4には他にもいくつか設定できる項目があり、その中にCIDRを設定できるということが分かりました。
https://raw.githubusercontent.com/projectcalico/calico/v3.28.0/manifests/tigera-operator.yaml
お、これは勝利の予感。
というわけでcalicoのDaemonSetのnodeAddressAutoDetectionV4をホスト間通信用のNICが使っているCIDRにしてデプロイし直してようやく解決しました。
gnome-terminalのレスポンスが非常に遅くなる問題への対処方法
gnome-terminalのレスポンスが遅くなる問題にここ2ヶ月くらい悩まされていましたが、それがようやく解決できたので共有程度に記事を書きました。
issueやコミュニティフォーラムなどを調査すると、NVIDIAグラフィックボードが搭載されたPC上でUbuntu 22.04を動かしかつWindow ManagerにMutterを使っている場合に生じるということが分かりました。
https://askubuntu.com/questions/1509058/input-delay-on-terminal-ubuntu-22-04-4
原因
mutterのSyncオブジェクトの実装が不足していたようです。
journalctlでブートID 0のログを参照してみるとMetaSyncRingのアラートが出ていることが分かります。
$ journalctl -b0 | grep MetaSyncRing May 26 03:31:34 oxygen gnome-shell[293854]: Window manager warning: MetaSyncRing: Sync object is not ready -- were events handled properly?
ソースコードの変更部分を見るとmeta_sync_ring_insert_waitという関数内でring->current_sync->stateがMETA_SYNC_STATE_READYでない場合はwarningを出し、meta_sync_ring_rebootをかけています。
おそらくこのmeta_sync_ring_rebootで最インスタンス化などをしており、そのせいでレスポンスが遅くなっていたと思われます。
今回の変更ではMETA_SYNC_STATE_WAITINGの場合、gpu_fenceというものを0にしてMETA_SYNC_STATE_READYをstateに入れ込んでいますが、ここらへんは追いきれていないです。
(gpu_fenceとかはなんだろうカーネルとかx11の実装とかを見ておけばよいのでしょうか?)
gpu_fenceについてはあまりよく分かりませんでしたが、とにかくこれが原因で、すでに修正コミットがMutterのメインブランチにマージされていますのでリリースを気長に待ちましょう。
といってもUbuntuの公式リポジトリに入るのはだいぶ先ですので、この不具合の修正コミットをあてたmutterを開発者の方がPPAで公開(https://bugs.launchpad.net/ubuntu/+source/mutter/+bug/2059847/comments/25)しているのでこちらをインストールして凌ぐことにしましょう。
対処方法
sudo add-apt-repository ppa:vanvugt/mutter sudo apt update sudo apt upgrade sudo apt-get install gir1.2-mutter-10=42.9-0ubuntu7vv1 mutter-common=42.9-0ubuntu7vv1 libmutter-10-0=42.9-0ubuntu7vv1
一部コンポーネントは古いバージョンのものとなっているため、apt updateで更新されてしまいます。
このため以下のコマンドで更新を抑制しておきます。
sudo apt-mark hold gir1.2-mutter-10 libmutter-10-0 mutter-common
DeepCool AK620-DIGITALとAORUS ELITE AX-WのLED周りをUbuntu上でいい感じにする方法
メインマシンを新しくしたのでUbuntuをインストールした。
その際にできればLED周りをWindowsと同様に設定したいと思いゴニョゴニョ調べたら出てきたのでやってみた。
GitHub - raghulkrishna/deepcool-ak620-digital-linux
DeepCool AK620-DIGITAL
参考リンクのdeepcool-ak620-digital-linuxを使う。
中身は単純にPythonスクリプトとsystemd用のserviceファイル、あとインストール用のスクリプトが入っている。
Pythonスクリプトの中身を見るとCPUの温度、使用率を取得してハードウェアにwriteしているだけのように見える。
インストール用スクリプトで書かれているインストール先がなんか気持ち悪いので変更。
以下フォークリポジトリにpushした。
動作する。
マザボ(AORUS ELITE AX-W)のLED
これはOpenRGBを使う。
普通に入れようとするといらないudevルールも入るのでlsusbで該当のデバイスのベンダーID、プロダクトIDを取得して、そのudevルールを抽出。
これを/etc/udev/rules.d/60-openrgb.rulesに入れ込む。
AppImageを起動すると普通に使えるのでよしなに設定する。
devcontainerでssh-addやgit configが引っ張ってこれない問題 on WSL2 windows11
自分は普段wsl2上で作業しているのですが、会社の先輩からdevcontainerはいいぞと教えていただいたので試してみました。
しかし、git pushはおろかgit commitすらもできない状況で困っていました。
結論は、wsl2上のssh-agentやgit configを引っ張ってくるにはdevcontainerの設定をしないといけないということでした。
- Dev>Containers:Execute In WSLを有効化
- Dev>Containers:Execute In WSLDistroでディストリビューションを指定します。

これでOKです。
実行中のプロセスを後からscreenに移動させる方法
実行中のプロセスを後からscreenに移動させる方法について紹介します.
reptyrパッケージが必要になりますのであらかじめインストールしておいてください.
$ sudo apt install reptyr
次に,何かしら時間がかかりそうなプロセスを走らせておきましょう.
$ <時間がかかるコマンド>
これで準備完了です.
では,screenに移動させてみましょう.
1 . Ctrl+Zでプロセスを一時停止
[1]+ Stopped <時間がかかるコマンド>
2 . jobsコマンドで該当プロセスのPIDを見つけてメモします.
$ jobs -l [1]+ 7597 <時間がかかるコマンド>
3 . ptrace_scopeのモードの変更
Ubuntuでは他のプログラムにプロセスを奪わることを避けるためにptrace_scopeによりプロセスが操作できないようになっています.
https://www.kernel.org/doc/Documentation/security/Yama.txt
今回,プロセスを別のプロセスに移動させる操作をしたいので,
そのモードを変更する必要があります.
そのためには以下のコマンドを実行します.
$ echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope 0
4 . screenを実行
5 . screen上でreptyrを用いてプロセスの親を切り替え
$ reptyr 7597
ちなみに,screenに移行したいプロセスが子プロセスを走らせていたりするときは,Tオプションを追加します.
このTオプションはターゲットの端末セッション全体を移動させるものです.
$ reptyr -T 7597
6 . fgコマンドで停止しているプロセスをフォアグラウンドで実行
$ fg ... プロセスの出力 ...
7 . ptraceによるロックを有効化
$ echo 1 | sudo tee /proc/sys/kernel/yama/ptrace_scope 1
参考
https://datawookie.netlify.app/blog/2017/12/moving-a-running-process-to-screen/