戯言日記

Rの話だと思ったら唐突にサバゲーが混じってくる何か。

ArduinoでString→char変換を試したらドツボに嵌った

スマホが新しくなった。
というか長いこと使ってたXperia Z5にガタが来てた。指紋認証は故障、少し使っているだけでホッカイロ状態、バッテリー60%↑なのに落ちる、背面パネルの左端が剥がれてくる(中身が見える)、などなど……。よくここまで使ったと思う。


なおXperia過激派なので今回もXperiaである。iPhone? 知らない子ですねぇ。
型落ちで安いXperia XZ2にしたが、それでも3世代くらいアップデートされた。動作も快適で周回が捗る。

www.sonymobile.co.jp


それと仕様を確認したらBluetooth5.0対応らしい。Z5は4.1対応だったが、4.2以降は通信速度というか一度に送れるデータ量が桁違いに増えている。
具体的には、4.1までだと最大で20byteしか送れなかったのが、4.2以降だと最大で244byteまで送れる。


という訳で、テンション上がってマイコンから無線通信で文字列をぶん投げて遊んでいたら普通につまずいたポイントがあったのでメモ。
C++に詳しい人はこんなので引っ掛からないのかも知れないけど、書いておかないとたぶんまた引っ掛かる()。


使用機器は手元に転がってたAdafruitのFeather nRF52。BLEモジュールが最初から付いていて、しかもArduino IDE内にサンプルスケッチが入っているためお試し程度の実装ならかなり手軽にできる。
コード全体については分量がおかしくなるので書かないため、気になる人はサンプルスケッチを参照していただければ。

www.adafruit.com


そもそもの問題は、BLEUARTを利用したコマンドでマイコンからデバイスにStringを送ろうとしたらエラーが出たこと。

String xxx = String() + "\t" + String();
bleuart.write(xxx);


このエラー自体は前から知っていて、でもこれまでの遊びだとbyteデータを送るのがメインだったから無視してたのだが、uint16形式のデータを送りたい場面で「uint16→String」で送ろうとして引っ掛かったので無視できなくなった。
で、試しに適当な文字列を入れた時は動く。

bleuart.write("yasaimashi");


改めてエラーとかGitHubとかを見たところ、データが「uint8」じゃないと送れないらしい。BLEはbyteデータしか送れないんだから当たり前の話である。

C++系の文字のことをよく知らなかったので調べてみたら、charは一文字が1byte=uint8対応なので問題ないと。
で、上の例だと「文字列=String」という単純な話ではなく、自動でchar配列として扱ってくれるようになっているらしい。

R使ってると文字と文字列の違いとか普段意識しないから完全に盲点だった。
普通に文字を打つとchar扱いになるとか分からんて。


となると最初からcharで文字を扱えばいいのだが、Stringの方が処理の段階では便利。なので全部片付いてから最後に変換したい。
と思ったらString→charの一発変換は無理とか言われたので、一文字ずつ変換するようにして処理する。

www.musashinodenpa.com


適当な数字が思いつかなかったので、DeNAベイスターズの佐野と梶谷の2020/09/27時点の打率を引っ張ってきた。佐野の打率が全然落ちてこなくて震える。
自分の目的がuint16の変数のやり取りなので、この例でも打率を1000倍にして整数で処理するようにしている。

float sano = 0.352 * 1000 //3割5分2厘
float kaji = 0.319 * 1000 //3割1分9厘

String words = String(sano) + "\t" + String(kaji)
int len = words.length();
char result[len];
words.toCharArray(result, len);


こんな感じでやればいい感じに変換してくれる。




そう思っていた時期が私にもありました。

何回かやっていたら、なんかちょっとおかしいことに気が付いた。

Serial.print(result)
>> 351  30


……なんか最後の数字だけ消えてない???

自分で試していた時はタブ区切りで送った最後の数字が「0」だったので「ラストが0だとエラー起こすようになってんのか……?」と思ったけど、別にそんなことはなかった。
でも明らかにおかしいので色々と調べる羽目に。最初はtoCharArray()のエラーか何かだと思ってた。

結論を言うと、コードの途中でStringの長さから自動的に生成しているchar配列の長さが足りてなかった。

float sano = 0.352 * 1000 //3割5分2厘
float kaji = 0.319 * 1000 //3割1分9厘

String words = String(sano) + "\t" + String(kaji)
int len = words.length() + 1;
char result[len];
words.toCharArray(result, len);


Arduinoではchar配列を作成した時に、コンパイラが自動で配列の末尾にヌル終端を付けてくれる。
char配列はヌル終端がないとメモリを無制限に食ったりするそうで、この機能はそれを回避するための配慮らしい。

www.musashinodenpa.com


しかしこのヌル終端の自動付加、長さを指定して配列を作成した場合は「配列の末尾にヌル終端を加える」ではなく、「配列の末尾をヌル終端で置き換える」という動作をする模様。
つまりchar配列を作成する時は、配列の長さを「元のStringの長さ+1」しないといけない。いや分かるかよこんなの。

たぶんRを使ってる人なら分かってもらえると思うが、そもそもクラスどころか文字と文字列に違いがあることすら普段から意識せずに使える言語に触れているせいで、ヌル終端のことなんて知ってる訳がなかった。

もしStringの長さがある程度分かっていて、自動で生成しなくてもいいなら、最初から余裕をもってchar配列の長さを決定しておけば楽だと思われる。

float sano = 0.352 * 1000 //3割5分2厘
float kaji = 0.319 * 1000 //3割1分9厘

String words = String(sano) + "\t" + String(kaji)
int len = 10;
char result[10];
words.toCharArray(result, 10);


C++だとかなり注意喚起されているみたい(検索でかなりヒットした)だけど、Arduinoだとあまり資料が引っ掛からなかった。
C++慣れしている人なら言われるまでもない内容かもしれないが、そうじゃない人の役に少しでも立てば幸いです。


Enjoy!