Cyclone10 LP基板をつくる
プロトタイピング環境の刷新
最近基板を作るのも、組み込みや車載の仕事もいろいろ残念なトラブル続きで最小限の一部の知り合いに絞っていたのですが、
そろそろ身の回りの「ラピッドプロトタイプするときのデバイス」のアップデートをしなきゃと思い立ち、開発の合間に「使い捨て」できるボードを設計していました。
USB/WiFi/Subギガ/BLEなどは新しいデバイスがどんどんリリースされているわけですが、オールインワンデバイスを使うとマイコンペリフェラルのお勉強からやり直しだったりとしんどかったので、まずその辺りの設計スタンスの確認。
「機能、概念試作」がおおいのでそこで手間取らないようなものを考えます。
- WiFi/BLE/USBはラズパイで完結させてしまい、どうしても必要になったら組み込みWiFi/BLE/USBデバイスを選定する
- なのでラズパイでできるところは頑張らない(移植できる余地は残しておく)
- intel FPGAのデバッグ環境が充実しているのでマイコンはNiosII/f
- あまりHDLを頑張って書かなくても良いようにする(プロトタイピングの時間短縮のため)
- 簡単な画像処理ができるようSDR SDRAMとSPI SRAMが欲しい。カメラIFなどは別にラズパイにやらせて問題ない気もする。
- RF関連の試作がよくあるのでプログラマブルなPLLを載せておく
- 差動を8ch以上出しておく(もちろん差動じゃない用途にも使えるようにしておく)
- それ以外に汎用GPIOを4chくらい出しておく
- 小さめがいい
「使い捨て」にするためには(壊れない限り処分しませんが)、なるべく基板の製造コストも下げないといけません。
先行デザインの調査
MAX10はCQ出版誌でも取り上げられたせいかホビーでよく使われるようになってきたように見受けられますが、業務プロダクトとしてはCyclone IV派生のCyclone10が多く使われている印象です。プロト用基板もいろいろ豊富です。
- インテル公式
https://www.intel.co.jp/content/www/jp/ja/programmable/products/boards_and_kits/dev-kits/altera/cyclone-10-lp-evaluation-kit.html
プログラマブルPLL搭載。
HyperRAMを搭載しているのですが、商用には別途IPの購入が必要。
SDR SDRAM載せてて欲しかった。
ちょっとでかい。
- Arduino MKR Vidor4000
https://store.arduino.cc/usa/mkr-vidor-4000
小さい。ピンアサインがArduino MKR互換。比較的安価
SDRAMもついている。WiFiいらないな。。
マイコン-FPGA間のコミュニケーションAPIが色々揃ってて便利(実態はSPI2Avalon-MMなのかな?)。ただ、FPGAを直接コンフィグすると不便そうなのでやめ。
- Trentz electronic CYC1000
https://shop.trenz-electronic.de/en/Products/Trenz-Electronic/CYC1000-Intel-Cyclone-10/
ちいさくて良い。とても安価。ピンアサインはArduino MKR互換。SDRAMついてる。Quartus Primeから直接認識できるArrow USB Programmerなるものが搭載されている。便利そうだけど、USB Blaster使いたい時に切り離すのがめんどくさそう。
どれも魅力的な部分はあったのですが、
をすべて満たすものがありませんでした。Trentzは迷いましたが、トレンツ日本(代理店?)がレスポンス遅いという関係者からのタレコミもあり。トレンツの注文が取れんツ。(さむ
設計
プロトタイプに適したボードがないので作ります。
EQFP144デバイスを使おうと以前に調査したのですがSDRAM接続が難しかったので、U256のBGAに。
参考資料:
Cyclone10LPの各々のピンの扱い方など
https://www.intel.co.jp/content/dam/altera-www/global/ja_JP/pdfs/literature/dp/cyclone-10/pcg-01021-j.pdf
パッケージごとのピンアサイン
https://www.intel.co.jp/content/www/jp/ja/programmable/support/literature/lit-dp.html
8割ほど完成した図がこちら。SDRAM、プログラマブルPLLのSi5351Aなどを搭載しています。
3Dで様子を確認。Raspi Zeroと寸法はほぼ同じ。ピンアサインもラズパイにスタックできる配置になっています。RaspiからUrJTAGなどからコンフィグしたり、ピンアサイン通りではないので何らかのジグが必要ですが比較的容易にUSB Blaster IIを接続したりできるでしょう。
大容量のQSPI RAMで便利そう。
下図は基板裏側。ラズパイと接続時には意識する必要はないですが、
VccIO=Vin
のジャンパを短絡させることによって96boardsやSpresenceボードなど3.3Vではないロジックと通信ができます。Vinから入力したロジック電圧で駆動します。(Vin=1.5 V / 1.8 V / 2.5 V / 3.0 V / 3.3 V)この時VccIO=3V3
とのジャンパは開放にしておく必要があります。国内の知り合いのメーカーさんに製造実装までお願いする予定なのでメーカーで規定されている認識マークをつけてパネライズをしました。
次回は到着したCyclone10LP基板とRaspberry Piを接続しバウンダリスキャンして遊びます。
Raspberry PiでBLE Notifyを確認する
LightBlueのLogだとつらい
前回PSoC63でBLEペリフェラルを作りました。
Terminalから文字列を入力するとBLE Notifyとして送信されるというものでした。
LightBlueで簡単に確認するのには便利ですが、センサ情報などを1日中収集するような用途には向きません。
参考サイト
ここのコードをほぼそのまま利用させていただきました。MTUの文字数を変更したくらいでしょうか
ラズパイのセットアップ(bluepy)
ラズパイ上で行う作業です
Python3は導入済みといたします。
sudo apt install cu sudo pip3 install bluepy nano ble_notify_central.py
コードを書きます。
#!/usr/bin/python3 # -*- coding: utf-8 -*- Cy63ble Button Event Notification import sys import time from bluepy.btle import * class ControlCy63ble: def __init__(self, mac): self._data = {} try: self.p = Peripheral(mac, ADDR_TYPE_PUBLIC) self.p.setDelegate(NotificationDelegate()) self.p.setMTU(256) print('Cy63ble connected !') except BTLEException: self.p = 0 print('Connection to Cy63ble failed !', mac) raise def _enableNotification(self): try: # Enable notification print('Notifications enabled') except BTLEException as err: print(err) self.p.disconnect() def _disableNotification(self): try: # Disble notification print('Notifications disabled') except BTLEException as err: print(err) self.p.disconnect() def monitorCy63ble(self): try: # Enable notification self._enableNotification() # Wait for notifications print('Waiting for button pushed 180 second') while self.p.waitForNotifications(180.0): # handleNotification() was called continue print('Notification timeout') self._disableNotification() except: return None def disconnect(self): self.p.disconnect() class NotificationDelegate(DefaultDelegate): def __init__(self): DefaultDelegate.__init__(self) def handleNotification(self, cHandle, data): try: if cHandle == 0x10: print('data :',data.decode()) else: print('handle=', cHandle,":", data) except BTLEException as err: print(err) self.p.disconnect() # main program if __name__== '__main__': print("Cy63blenotify start") argvs = sys.argv argc = len(argvs) if (argc < 2): print("Require Bluetooth address [XX:XX:XX:XX:XX:XX]") quit() Cy63ble_mac_addr = argvs[1] myCy63ble = ControlCy63ble(Cy63ble_mac_addr) print("Cy63ble found :",Cy63ble_mac_addr) myCy63ble.monitorCy63ble()
保存したら
chmod +x ble_notify_central.py
PSoC63ボードをRaspberryPiにUSB接続
この時ハイバネートモードにしない方が楽だと思います。
コンソール1つ目
cu -l /dev/ttyACM0 -s 115200 Connected. ....
/dev/ttyACM0
は /dev/ttyAMA0
、 /dev/ttyUSB0
、/dev/ttyACM1
などの場合もあります。
コンソール2つ目
./ble_notify_Central.py 00:A0:50:00:00:00 Cy63blenotify start Cy63ble connected ! Cy63ble found : 00:A0:50:00:00:00 Notifications enabled Waiting for button pushed 180 second ...
ここの 00:A0:50:00:00:00
は前回プログラムしたPSoC63基板のBLEデバイスのMACアドレスです。
コンソール1つ目に戻ってなにか文字を打ち込み、最後にリターンキーを押します。(例えばtesttesttest
)
コンソール2つ目をみると、こうなっています。
data : testtesttest
できました!おしまい。
PSoC6とModusToolbox IDE v1.1でBLEを試す
前置き
なんだかんだでPSoC63もわりと使えるかもしれないって思ってもらえれば幸い。
補足
ソースコードを逐一追って細かく解説はしません。IDEのインストールとか使い方はのりたんさんがQiitaで手取り足取り解説してくださっています。v1.0の解説だけど、v1.1でもそんなに変わりません。しらんけど。(おい
この記事でできること
- PSoC63ボードでBLE peripheralを作り notify / writeができるようになる。
- 外部からのシリアル入力をBLE notifyとしてセントラルに送ることができる
では参りましょう。
PSoC63基板の準備
プロジェクト一式
git cloneします
git clone https://github.com/panda5mt/psoc63_ble_notify_rtos.git
ModusToolbox IDEv1.1を起動しcloneしたプロジェクトをロードします。
評価ボード
CY8CPROTO-063-BLE
www.cypress.com
基板の書き込み用ファームウェアが古い場合があるので、最新のものに書き換えます。SW3(上の右側写真、親指で押している部分)を押しながらUSBに接続。
(以下はMacの場合です)
Terminal.appを起動し、fw-loaderでアップデートを行います。
/Applications/ModusToolbox_1.1/tools/fw-loader-2.1/bin/fw-loader --update-kp3
ModusToolboxにもどり、左下の[プロジェクト名] Program (KitProg3)
をクリックします
コンソールに書き込み中を示す
[ xx%] [############################ ] [ Programming ]
のような表示がしばらく現れたのち、
** Program operation completed successfully **
という文字がIDE内コンソールに出ていれば成功です。
この時点ですでにPSoC63評価ボードはBLEペリフェラルとして機能しています。
BLE Notifyのテスト
BLEセントラル(iOSデバイス)の準備
iOSデバイスをお持ちの場合は、LightBlueアプリから見ることができます。
p6xble
という名前が見つかれば一応ここまで成功です。
p6xble
をタップしてBLE接続しましょう。Notifyの確認をします。UUID:BBBB、0xCCCC(Properties: Notify)
をタップします。右上のHEX
をタップしUTF-8
に変更した方がわかりやすいと思います。Listen for notifications
をタップしてNotification受信待ちにしておきます
BLEペリフェラル(PSoC63)の準備
Terminal.appに戻ります。
sudo cu --parity=none --nostop --line /dev/tty.usbmodemxxxxx --speed 115200 Password:(パスワードを入力する) Connected.
tty.usbmodemxxxxx
のxxxxxは任意の数値になります。
ここでTerminal.app上で任意の文字を入力し最後にリターンを押すと、
BLE writeのテスト
次にLightBlueアプリのPeripheralのページに戻りUUID:AAAA、0xBBBB(Properties: Write)
をタップします.
右上のHEX
をタップしUTF-8
に変更した方がわかりやすいと思います。Write new value
をタップします。
任意の文字を入力し、最後に完了
またはdone
をタップします。例としてあいうえお
と入力しました。
あいうえお
をタップするたびにTerminalにWriteされた内容が反映されます。
Info : BLE - GATT write request Info : BLE - GATT read request write value = あいうえお Info : BLE - GATT write request Info : BLE - GATT read request write value = あいうえお ....
とりあえず動作確認はできました。
仕組み
Terminal.appからの入力(stdin)は評価ボード上のUSBシリアルを経由し、PSoC63のシリアルポートに入力されます。
// main.c,248行目付近 ble_commandAndData_t bleCommand = {.command = SEND_NOTIFICATION, .data=str }; xQueueSend(bleCommandDataQ, &bleCommand, 0u);
また、BLEタスクでWriteがあった場合は直ちにUSB-UART経由で出力(stdout)しています
// ble_task.c,355行目付近 case CY_BLE_EVT_GATTS_WRITE_REQ: { writeReqParameter = *(cy_stc_ble_gatts_write_cmd_req_param_t*)eventParam; DebugPrintf("write value = %s \r\n", writeReqParameter.handleValPair.value.val); }
応用
USB-UARTではなく別デバイスからの入力データをNotifyで送信する
例として、PSoC63のP9.0からのシリアル入力があった場合にNotifyを送信することができるようにしてあります。
ボーレートは38400baudです。先ほどとボーレートが異なりますので注意。
NMEAシリアル出力機能を持っているGPSモジュールなどの入力に使えるとおもいます。
// stdio_user.h, 157行目付近 #define IO_STDIN_UART KIT_UART_HW // この行のコメントを外す //#define IO_STDIN_UART UART_STDIO // この行をコメントにする
コンパイル後、書き込みをします。
Hibernateモードを有効化する
BLEペリフェラルはフィールドの要求に応じて、動作しない時電池の消費を極限まで抑える必要があります。
PSoC63もいくつかの消費電力を抑える機能を持っており、その一つがハイバネートモードです。
ほとんどのリソースを停止します。今回のコードではBLEセントラルからの接続が1分以上ない場合、基板上のスイッチSW2が押されるまで、Hibernateに遷移します。
この機能を有効化するには、以下のようにします。
// ble_task.c, 59行目付近 #define HIBERNATE_ENABLE 1 // ここを1にする
BLEセントラルからの接続を待つ待ち時間を変更したい場合は、そのすぐ上の行を変更します
// ble_task.c, 58行目付近 define TIMEOUT_INTERVAL pdMS_TO_TICKS(2 * 60000u) // 2分待つように変更
とこんな感じです。ちょっと長くなったのでここまでにします。
お疲れ様でした。
次回
BLE NotifyをLightBlueで確認するのはデータ量が増えるとすこし面倒です。
次回はRaspberry PiをBLEセントラルにして、PSoC63からのNotifyのデータをテキストや標準出力に表示するPythonコードを紹介します。
Raspberry Piでngrokのtcpアドレスをslackへ通知する
NAT越えしたい
自分の家に設置したラズパイにsshで入るならしかるべき方法で公開し、公開鍵設定やVPNを張るなどをして入れるとおもいますが、IoTセンサノードとして屋外で使う場合はモバイルルータなどで接続することが多いかもしれません。
モバイルルータが固定IPを持っていてNATトラバーサルできる契約のもありますが、そうでない時も多い。
できるものもオプション扱いでお金が別途かかる。もちろん常用するならIIJモバイルbiz+とかがいいと思いますが、そうでない時の方法です。
エージェントの選定
いろいろ探しました。いいなと思ったのが以下
Pulsewayはちょっとterminalの出来が悪すぎ…。というかそういう用途ではないですね。
Fluentdで端末のパラメータを監視したりするような作業をお手軽にできる感じがいいですね。
ngrokをNode.jsで使う準備
今回はngrokを使いました。超便利。ですが、起動のたびにアドレスとポートが変更されてしまいます。
アドレスを確認する手段の一つとしてSlackに自身のngrokアドレスを投稿させるコードを記述し、Systemdサービスとして登録します。
Node.jsを使います。ラズパイにサクサク入れていきます。
nohupはhupシグナルがngrokプロセスに送られないように使っています。
他にいい方法があれば教えてください。
sudo apt update
sudo apt install nohup
wget http://node-arm.herokuapp.com/node_latest_armhf.deb
sudo dpkg -i node_latest_armhf.deb
2020年2月4日追記:上記の方法ではできなくなりました。下記の方法に修正いたします。sudo npm cache clean
のところでエラーが出ても気にせず続けてしまいましょう。
sudo apt update sudo apt install -y nodejs npm sudo npm cache clean sudo npm install npm n -g sudo n latest mkdir -p ~/node_app/sendslack_ngrok cd !$ nano package.json
ここでpackage.jsonを作っておきます。下記内容で構いません。
{ "name": "sendslack_ngrok", "description": "sendslack_ngrok", "version": "1.2.3", "private": true, "scripts": { "start": "node app.js" } }
保存します。
npm install ngrok node-slack os --save
Ngrokの設定
www.npmjs.com
ngrokは無料で十分使えますが、アカウントを取得しないとトークンがもらえません。(tcp使うのには必須)
サインアップはこちら。
トークンはサインアップ後の画面の③で示されている部分です。赤ワクで囲んだ部分になります。
コピーしておきます。
ラズパイのターミナルで実行しておく。
./node_modules/ngrok/bin/ngrok authtoken this-is-your-ngrok-auth-token
ホームディレクトリに.ngrok2/ngrok.ymlが生成されます。
あとで記述するthis-is-your-ngrok-auth-tokenの部分です。
Slackのアプリ設定
投稿したい専用チャネルを作るか自分へのDMとしたほうがいいでしょう(この記事の最後まで実施すると、起動するたびにSlackへ投稿します。#generalとかに投稿すると結構うざいです)。Slackアプリからは歯車のマーク -> アプリを追加する。Webからは自分のワークスペースにログイン後 https://{your-company-workspace}.slack.com/apps
で「incoming webhook」を検索。
incoming-webhookアプリ設定画面に移行します。
「設定を追加」をクリック
次画面の「チャンネルへの投稿」で投稿したいチャネルを選択または新規作成します。
登録後、生成された「Webhook URL」をコピーしておく。あとで記述するthis-is-your-slack-incoming-webhook-addressの部分です。
webhookコードの記述
さて、準備が整いました。コードを書いていきます
今回はapp.jsというファイルに記述します。
cd ~/node_app/sendslack_ngrok nano app.js
const Slack = require('node-slack'); const ngrok = require('ngrok'); var slack = new Slack(' this-is-your-slack-incoming-webhook-address '); ngrok_connect().then(url => { console.log('URL : ' + url); var message = 'tcp url: ' + url; slack.send({ text: message, username: 'raspibot', icon_emoji: ":poop:" }); }); // ngrokを非同期で起動 async function ngrok_connect() { await ngrok.authtoken(' this-is-your-ngrok-auth-token '); let url = await ngrok.connect({proto:'tcp',port:22}); // このコマンドを実行するのにトークンが必須 return url; }
いったんサービスとして登録する前に動作確認をします
cd ~/node_app/sendslack_ngrok npm start
こんな感じでSlackに投稿できたら成功。
うんこ絵文字が最初から入ってるなんて素敵。
何に使うのかしら。
サービスとして登録する
適当なフォルダでngroksend.serviceというファイルを作ります。
なお下記例ではホームディレクトリが/home/pi/になってます。
cd ~/ nano ngroksend.service
下記を記述します。
[Unit] Description=ngrokSend After=syslog.target [Service] Type=simple WorkingDirectory=/home/pi/node_app/sendslack_ngrok/ ExecStart=/usr/bin/nohup npm start TimeoutStopSec=5 StandardOutput=null [Install] WantedBy = multi-user.target
保存後、systemdに登録します。
sudo mv ngroksend.service /etc/systemd/system/ sudo systemctl start ngroksend.service sudo systemctl enable ngroksend.service
動作が確認できたらsudo rebootしてください。
参考
ngrok - npm
Raspberry Piをngrokで公開する - Part1: Expressをpm2から起動する - Qiita
Raspberry Piでプログラムを自動起動する5種類の方法を比較・解説(記事移動済み)
余談:ラズパイのケース
Raspberry Pi 3b+になってかなり発熱が心配になってきました。
動作させているプロセスにも依存しますが、2月のまだ寒い屋内で(室内気温12,3度)、ヒートシンクを貼ったファンレス状態では
vcgencmd measure_temp temp=70.1'C
となっておりました。夏にお亡くなりになるのが怖く、ファン付きのものをいくつか試すことに。
- Eleduino Raspberry Pi Case
同じプロセスを走らせつつ、このケースを使用して使ったところ
vcgencmd measure_temp temp=49.7'C
と、かなり冷却効果がありました。でもヒートシンクをCPUに貼っている場合剥がさないとファンが干渉します。
で、この上部に貼ってあるRaspberry Piアクリルステッカーのせいで笛吹き現象が起こってうるさかったので剥がしました。剥がしたら1,2度下がったけど誤差なのかは不明
。
- GeeekPi Raspberry Pi3b+ Case
もう一つ。前者のより安価で、すこし背が高い。ヒートシンク付き。ケースに基板を嵌合してからヒートシンクを貼ったほうがいいでしょう。
前者のケースのファンより回転数が高めに思われますが、すごく静か。
1番目のものより個人的には気に入っています。でも高さがあるのでそこは運用方法に合わせて購入でしょうかねぇ。
vcgencmd measure_temp temp=42.0'C
冷却性能も良さげでした。
Swift PlaygroundsでのMVP実装を考えた
MVP実装考えなきゃいけないほど大規模開発しないよね
FatViewControllerガーって言うほど、iPad単体で大規模なプログラムは書かないとは思うのですが…
- 組み込みデバイスと通信するプログラムが多いので、ViewControllerが組み込みデバイスとの通信APIとごちゃごちゃになるのは必至
- 別のViewアーキテクチャの機器に移植するのが大変になる
- API変更があった場合の変更が辛くなる
MVCから逃げたいのはこのくらいかな。MVPにしたのはその他のアーキテクチャにくらべて学習コストが低そうと言うだけです。
MVPについてちょっとお勉強した
参考にしたサイト
そのまま、Model - View - Presenterで成り立っており、比較的それぞれが疎結合になっているアーキテクチャ
Presenterの役割が重要で、
- Viewからイベントを受け取り、Modelに処理を移譲
- Modelから結果を受け取り、Viewに通知
したがってModel、Viewとも、互いに独立している。
今回はModelはNotificationCenterを使い、Presenter通知しています。
iPad SwiftPlaygroundsですので、複数ファイルに記述するのが少々ハードルが高いので1ファイルに汚く書いてしまいました。
import PlaygroundSupport import UIKit import Foundation // MVPの練習 // protocol宣言 -------------------- protocol MyViewProtocol: class { func reloadData() } protocol MyModelProtocol: MyModelProtocolNotify { func fetchControl() } protocol MyModelProtocolNotify: class { func addObserver(_ observer: Any, selector: Selector) func removeObserver(_ observer: Any) } protocol MyViewPresenterProtocol: class { } // protocol宣言おわり--------------- class MyLiveView: UIView { } // ------------------------------- // View class MyViewController: UIViewController { private var myTextView = UITextView(frame: CGRect(x: 20, y: 20, width: 220, height: 60)) var presenter: MyViewPresenter! override func viewDidLoad() { super.viewDidLoad() view.addSubview(myTextView) presenter = MyViewPresenter(view: self) updateControl() } override func loadView() { super.loadView() view = MyLiveView() } @objc func updateControl(){ // Presenterに取得処理を移譲 presenter.updateModelControl() } } extension MyViewController: MyViewProtocol { // Presenterから呼ばれる処理 func reloadData() { myTextView.text = "通知を受信" } } // ------------------------ // Presenter class MyViewPresenter: MyViewPresenterProtocol { var view: MyViewProtocol var model: MyModelProtocol init(view: MyViewProtocol) { self.view = view self.model = MyModel() // NotificationCenter model.addObserver(self, selector:#selector(self.updated)) } // Modelに処理を移譲 func updateModelControl(){ model.fetchControl() } // Modelから更新の通知があったらViewに更新を依頼 @objc func updated() { //print("updated!!!") view.reloadData() } } // -------------------- // Model class MyModel: MyModelProtocol { func fetchControl(){ // NotificationCenterでPresenterに通知 self.notify() } } extension MyModel: MyModelProtocolNotify { var notificationName: Notification.Name { return Notification.Name(rawValue: "MyModel") } func removeObserver(_ observer: Any) { NotificationCenter.default.removeObserver(observer) } func notify() { NotificationCenter.default.post(name: notificationName, object:nil) } func addObserver(_ obserber: Any, selector: Selector) { NotificationCenter.default.addObserver(obserber, selector: selector, name: notificationName, object: nil) } } PlaygroundPage.current.liveView = MyViewController()
Swift Playgrounds向けアクセサリ「PlaygroundBluetooth API」を使ってBluetooth LEデバイスと通信をする
WWDC2017で発表されたPlaygroundsBluetooth API便利そう
今年最後のブログになります。
今何してあそんでるかの報告がわりに。
WWDC2017
What’s New in Swift Playgrounds - Apple WWDC 2017
iPad向けのSwift Playgroundsアクセサリとして提供されているPlaygroundBluetoothを使って見ました。CoreBluetoothベースですが、Appleが言うにはより一貫性がある(これはSwift PlaygroundsでのBluetooth通信のユーザエクスペリエンスが一貫していると言う意味だと思います)APIで、以下のコンポーネントを提供しています
PlaygroundBluetooth API
- PlaygroundBluetoothCentralManager(および、そのDelegate)
- Bluetooth機器との接続、通信 (CBCentralManagerに近い)
- PlaygroundBluetoothConnectionView(および、そのDelegate)
- Bluetooth機器の検出、接続、接続解除などのためのUI
特にPlaygroundBluetoothConnectionViewはUIを提供して検出、接続、解除までを一貫してしてくれるので、プログラマによってBluetooth検出、接続、解除時のUIが全く異なり、アプリを使うユーザが途方に暮れる…事は少なくなりそうです。
と言っても2017年12月現在、Appleのページで纏まった資料やサンプルコードが見当たりませんでした。探し方が下手なだけかも知れませんが。
2018年9月更新:AppleのページにPlaygroundBluetooth Frameworkの詳細情報が掲載されています。最新情報はこちらを確認してください。
サンプルコードないかな…
唯一見つけたのが以下のサイト。
https://swiftexample.info/snippet/liveviewswift_bricklife_swift
iPhoneの電池残量が確認できるサンプルコード
なんか途中から全力で生のCoreBluetooth触りにいってるけど大丈夫かこのサンプルコード…。
カスタムプロファイルのペリフェラルとお話しする
これをベースに、カスタムプロファイルを持ったBluetoothペリフェラルに接続し、データをやり取りするターミナルっぽいやつを作りました。
iPadは終始セントラルとして動きます。
- read
- writeWithResponse
- writeWithoutResponse
- notify
キャラクタリスティックに対応します。
ペリフェラル側とその設定が必要です。
今回はセントラル(iPad)からきたデータをWrite(writeWithoutResponse)し、Readに対してはそのデータを返すようにします。(ループバックっぽい動作にする)
いきなりBLEを持ってる組み込みマイコンにさせると、バグったときに切り分けが出来なくなって詰みそうですので、今回はiPhoneにLightBlueアプリをいれて仮想的にペリフェラルとして動かしています。
VirtualPeripheralの提供するサービスは「Blank」を選択し
- Service UUID : 0x1111
- Characteristic UUID : 0x2222
PropertyはReadとWriteWithoutResponseにチェック
そうすると、iPadは以下のように動きます。
Communicate with BLE accessory using PlaygroundBluetooth API
Swift Playgrounds(Swift4.0)で横スクロールするグラフを描く
単純な横スクロールのグラフだけでいい
iOSプログラミングのお仕事がちょいちょい来てるのですが、ブランクが長すぎてiPad Proでリハビリをする毎日です。Cordova/PhoneGapも最近聞かないなぁ。
最近はReactがアツいとかなんとか聞くのですが。
iOS11になってから今まで順調に動いていたPythonista3でグラフをプロットしようとすると何かの契機にアプリごとクラッシュしてしまうようになりました。
自分がmatplotlibの扱いがよくわかってないのもあるのですが…そのうち解決するでしょうということで。
Swiftでグラフを描くのはライブラリを使うのが良いようです。たとえばGitHub - core-plot/core-plot: Core Plot source code and example applications
今回は簡単な折れ線グラフが書きたいだけなのでこのライブラリの学習コストが見合わないなと思いいろいろ探していたところ、ありました。
Swiftでシンプルなグラフ描写する ... | FiNC Developers Blog
ここで紹介されているものをお借りしました。ありがとうございます。
プロット数が多くなると、UIViewサイズに合わせてx軸のマージンがどんどん小さくなるので、UIScrollViewを使って横スクロールしてしまうようにしました。またSwift 2.0の記述だったので4.0に対応するように一部直しています。
なお、プロット間のx軸の距離は80に固定してしまっています。複数の折れ線グラフも楽々。
動作してるところ
感想
iPad上のSwift Playgroundsで結構しっかりしたプログラム組めるのはすごいなと思いました。電車内やカフェで殴り書きするのにちょうどいい。
でも、ガチなソフト組むとなると、StoryBoardが欲しくなるしエディタとしては機能が限定されているので、MacとXCodeが必要になりますね。
コード
本家
Swiftでシンプルなグラフ描写する ... | FiNC Developers Blog
のコードと比較したほうがいいかも。急いででっち上げてしまったので…
// // ViewController.swift // SimpleGraph // // Created by Yoshimi Kondo on 2015/11/19. // Copyright © 2015年 yoshimikeisui. All rights reserved. // // fixed to use UIScrollView by lynxeyed 2017/11/05 import PlaygroundSupport import UIKit public class ViewController: UIViewController{ let wd = 500 let ht = 630 override public func viewDidLoad() { super.viewDidLoad() drawLineGraph() //drawBarGraph() } func drawLineGraph() { let stroke1 = LineStroke(graphPoints: [1, 3, 6, 4, 9, 12, 4, 11, 15,5,1,5,10,5]) let stroke2 = LineStroke(graphPoints: [16,3, nil, 6, 4, 8]) let stroke3 = LineStroke(graphPoints: [24,5, 5, 6, 4, 5]) stroke1.color = #colorLiteral(red: 0.239215686917305, green: 0.674509823322296, blue: 0.968627452850342, alpha: 1.0) stroke2.color = #colorLiteral(red: 0.854901969432831, green: 0.250980406999588, blue: 0.47843137383461, alpha: 1.0) stroke3.color = #colorLiteral(red: 0.584313750267029, green: 0.823529422283173, blue: 0.419607847929001, alpha: 1.0) let lineGraphView = UIScrollView() lineGraphView.frame.size = CGSize(width: wd, height: ht) let graphFrame = LineStrokeGraphFrame(strokes: [stroke1, stroke2, stroke3]) lineGraphView.isPagingEnabled = false lineGraphView.backgroundColor = UIColor.darkGray view.addSubview(lineGraphView) lineGraphView.addSubview(graphFrame) let fw = graphFrame.xAxisPointsCount * Int(graphFrame.xAxisMargin) lineGraphView.contentSize = CGSize(width:fw, height:ht) //スクロールビュー内のコンテンツサイズ設定 } func drawBarGraph() { let bars = BarStroke(graphPoints: [nil, 1, 3, 1, 4, 9, 12, 4]) bars.color = UIColor.green let barFrame = LineStrokeGraphFrame(strokes: [bars]) let barGraphView = UIView(frame: CGRect(x: 0, y: 240, width: view.frame.width, height: 200)) barGraphView.backgroundColor = UIColor.gray barGraphView.addSubview(barFrame) view.addSubview(barGraphView) } override public func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } } protocol GraphObject { var view: UIView { get } } extension GraphObject { var view: UIView { return self as! UIView } func drawLine(from: CGPoint, to: CGPoint) { let linePath = UIBezierPath() linePath.move(to: from) linePath.addLine(to: to) linePath.lineWidth = 0.5 let color = UIColor.white color.setStroke() linePath.stroke() linePath.close() } } protocol GraphFrame: GraphObject { var strokes: [GraphStroke] { get } } extension GraphFrame { // 保持しているstrokesの中で最大値 var yAxisMax: CGFloat { return strokes.map{ $0.graphPoints }.flatMap{ $0 }.flatMap{ $0 }.max()! } // 保持しているstrokesの中でいちばん長い配列の長さ var xAxisPointsCount: Int { return strokes.map{ $0.graphPoints.count }.max()! } // X軸の点と点の幅 var xAxisMargin: CGFloat { //return view.frame.size /CGFloat(xAxisPointsCount) return 80.0 // } } class LineStrokeGraphFrame: UIView, GraphFrame { var strokes = [GraphStroke]() convenience init(strokes: [GraphStroke]) { self.init() self.strokes = strokes } override func didMoveToSuperview() { if self.superview == nil { return } //self.frame.size = self.superview!.frame.size let uisview = ViewController() //var frameWidth = gs. self.frame = CGRect(x: 0, y: 0, width: 3000, height: uisview.ht ) self.view.backgroundColor = UIColor.clear strokeLines() } func strokeLines() { for stroke in strokes { self.addSubview(stroke as! UIView) } } override func draw(_ rect: CGRect) { drawTopLine() drawBottomLine() drawVerticalLines() } func drawTopLine() { self.drawLine( from: CGPoint(x: 0, y: frame.height), to: CGPoint(x: frame.width, y: frame.height) ) } func drawBottomLine() { self.drawLine( from: CGPoint(x: 0, y: 0), to: CGPoint(x: frame.width, y: 0) ) } func drawVerticalLines() { for i in 1..<xAxisPointsCount { let x = xAxisMargin*CGFloat(i) self.drawLine( from: CGPoint(x: x, y: 0), to: CGPoint(x: x, y: frame.height) ) } } } protocol GraphStroke: GraphObject { var graphPoints: [CGFloat?] { get } } extension GraphStroke { var graphFrame: GraphFrame? { return ((self as! UIView).superview as? GraphFrame) } var graphHeight: CGFloat { return view.frame.height } var xAxisMargin: CGFloat { return graphFrame!.xAxisMargin } var yAxisMax: CGFloat { return graphFrame!.yAxisMax } // indexからX座標を取る func getXPoint(index: Int) -> CGFloat { return CGFloat(index) * xAxisMargin } // 値からY座標を取る func getYPoint(yOrigin: CGFloat) -> CGFloat { let y: CGFloat = yOrigin/yAxisMax * graphHeight return graphHeight - y } } class LineStroke: UIView, GraphStroke { var graphPoints = [CGFloat?]() var color = UIColor.white convenience init(graphPoints: [CGFloat?]) { self.init() self.graphPoints = graphPoints } override func didMoveToSuperview() { if self.graphFrame == nil { return } self.frame.size = self.graphFrame!.view.frame.size self.view.backgroundColor = UIColor.clear } override func draw(_ rect: CGRect) { let graphPath = UIBezierPath() graphPath.move( to: CGPoint(x: getXPoint(index: 0), y: getYPoint(yOrigin: graphPoints[0] ?? 0)) ) for graphPoint in graphPoints.enumerated() { if graphPoint.element == nil { continue } let nextPoint = CGPoint(x: getXPoint(index: graphPoint.offset), y: getYPoint(yOrigin: graphPoint.element!)) graphPath.addLine(to: nextPoint) } graphPath.lineWidth = 5.0 color.setStroke() graphPath.stroke() graphPath.close() } } class BarStroke: UIView, GraphStroke { var graphPoints = [CGFloat?]() var color = UIColor.white convenience init(graphPoints: [CGFloat?]) { self.init() self.graphPoints = graphPoints } override func didMoveToSuperview() { if self.graphFrame == nil { return } self.frame.size = self.graphFrame!.view.frame.size self.view.backgroundColor = UIColor.clear } override func draw(_ rect: CGRect) { for graphPoint in graphPoints.enumerated() { let graphPath = UIBezierPath() let xPoint = getXPoint(index: graphPoint.offset) graphPath.move( to: CGPoint(x: xPoint, y: getYPoint(yOrigin: 0)) ) if graphPoint.element == nil { continue } let nextPoint = CGPoint(x: xPoint, y: getYPoint(yOrigin: graphPoint.element!)) graphPath.addLine(to: nextPoint) graphPath.lineWidth = 30 color.setStroke() graphPath.stroke() graphPath.close() } } } PlaygroundPage.current.liveView = ViewController()