lynxeyedの電音鍵盤

MBDとFPGAと車載で使うデバイスの備忘録

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載せてて欲しかった。
ちょっとでかい。

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


f:id:Lynx-EyED:20200128002402p:plain
バーン。IOを欲張らなければ2層で引けるとはいえ、めんどくさい感じでした。結局BGA裏側はパスコンとフィルタで埋まるのであまり自由度はなく…。
8割ほど完成した図がこちら。SDRAMプログラマブルPLLのSi5351Aなどを搭載しています。

f:id:Lynx-EyED:20200128094549p:plain
電源とBGAからの配線引き出しと差動以外の配線はKiCadの自動配線に任せ、少し手直ししておしまいにしました。
3Dで様子を確認。Raspi Zeroと寸法はほぼ同じ。ピンアサインもラズパイにスタックできる配置になっています。RaspiからUrJTAGなどからコンフィグしたり、ピンアサイン通りではないので何らかのジグが必要ですが比較的容易にUSB Blaster IIを接続したりできるでしょう。

f:id:Lynx-EyED:20200215120210p:plain
FPGA(中央左寄りの大きい正方形の物体)の右横にある8pinのSOICはIoT RAMというものだそうです。(IPS6404L-SQ)
大容量の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とのジャンパは開放にしておく必要があります。

f:id:Lynx-EyED:20200215120145p:plain

国内の知り合いのメーカーさんに製造実装までお願いする予定なのでメーカーで規定されている認識マークをつけてパネライズをしました。



次回は到着したCyclone10LP基板とRaspberry Piを接続しバウンダリスキャンして遊びます。

Raspberry PiでBLE Notifyを確認する

LightBlueのLogだとつらい

前回PSoC63でBLEペリフェラルを作りました。
Terminalから文字列を入力するとBLE Notifyとして送信されるというものでした。
LightBlueで簡単に確認するのには便利ですが、センサ情報などを1日中収集するような用途には向きません。

参考サイト

www.ratoc-e2estore.com

ここのコードをほぼそのまま利用させていただきました。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接続

この時ハイバネートモードにしない方が楽だと思います。

f:id:Lynx-EyED:20190429132542j:plain:w250
ラズパイのコンソールを2つ開きます。

コンソール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

f:id:Lynx-EyED:20190320231803j:plain:h200f:id:Lynx-EyED:20190428000712p:plain:h200
2019年4月時点で価格は20ドル。

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)をクリックします
f:id:Lynx-EyED:20190428002420p:plain:h300
コンソールに書き込み中を示す

[ xx%] [############################    ] [ Programming ]

のような表示がしばらく現れたのち、
** Program operation completed successfully **という文字がIDE内コンソールに出ていれば成功です。
この時点ですでにPSoC63評価ボードはBLEペリフェラルとして機能しています。

BLE Notifyのテスト

BLEセントラル(iOSバイス)の準備

iOSバイスをお持ちの場合は、LightBlueアプリから見ることができます。

LightBlue® Explorer

LightBlue® Explorer

  • Punch Through
  • ユーティリティ
  • 無料

p6xbleという名前が見つかれば一応ここまで成功です。


f:id:Lynx-EyED:20190428005814j:plain:w150
f:id:Lynx-EyED:20190428155030j:plain:w150
f:id:Lynx-EyED:20190428160419j:plain:w150
先ほどのLightBlueで表示されたデバイス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.usbmodemxxxxxxxxxxは任意の数値になります。 
ここでTerminal.app上で任意の文字を入力し最後にリターンを押すと、
f:id:Lynx-EyED:20190428014505p:plain:w200

BLE writeのテスト

次にLightBlueアプリのPeripheralのページに戻りUUID:AAAA、0xBBBB(Properties: Write)をタップします.
右上のHEXをタップしUTF-8に変更した方がわかりやすいと思います。Write new valueをタップします。
任意の文字を入力し、最後に完了またはdoneをタップします。例としてあいうえおと入力しました。


f:id:Lynx-EyED:20190428155318j:plain:w200
f:id:Lynx-EyED:20190428160419j:plain:w200
f:id:Lynx-EyED:20190428161010p:plain:w200
f:id:Lynx-EyED:20190428161340j:plain:w200
このあいうえおをタップするたびに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のシリアルポートに入力されます。


f:id:Lynx-EyED:20190428232919p:plain:w400
シリアル入力があった場合にFreeRTOSのBLEタスクにNotificationを送信するキューを書き込みます。

// 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で送信する


f:id:Lynx-EyED:20190428234914j:plain:w400

例として、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使うのには必須)
サインアップはこちら
トークンはサインアップ後の画面の③で示されている部分です。赤ワクで囲んだ部分になります。
f:id:Lynx-EyED:20190410213929p:plain
コピーしておきます。
ラズパイのターミナルで実行しておく。

./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」を検索。
f:id:Lynx-EyED:20190410203156p:plain
incoming-webhookアプリ設定画面に移行します。
「設定を追加」をクリック

f:id:Lynx-EyED:20190410203626p:plain
次画面の「チャンネルへの投稿」で投稿したいチャネルを選択または新規作成します。
登録後、生成された「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に投稿できたら成功。
f:id:Lynx-EyED:20190410205756p:plain
うんこ絵文字が最初から入ってるなんて素敵。
何に使うのかしら。

サービスとして登録する

適当なフォルダで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してください。

余談:ラズパイのケース

Raspberry Pi 3b+になってかなり発熱が心配になってきました。
動作させているプロセスにも依存しますが、2月のまだ寒い屋内で(室内気温12,3度)、ヒートシンクを貼ったファンレス状態では

vcgencmd measure_temp
temp=70.1'C

 
となっておりました。夏にお亡くなりになるのが怖く、ファン付きのものをいくつか試すことに。


同じプロセスを走らせつつ、このケースを使用して使ったところ
f:id:Lynx-EyED:20190410220246j:plain

vcgencmd measure_temp
temp=49.7'C

と、かなり冷却効果がありました。でもヒートシンクをCPUに貼っている場合剥がさないとファンが干渉します。
で、この上部に貼ってあるRaspberry Piアクリルステッカーのせいで笛吹き現象が起こってうるさかったので剥がしました。剥がしたら1,2度下がったけど誤差なのかは不明

  • GeeekPi Raspberry Pi3b+ Case

もう一つ。前者のより安価で、すこし背が高い。ヒートシンク付き。ケースに基板を嵌合してからヒートシンクを貼ったほうがいいでしょう。

前者のケースのファンより回転数が高めに思われますが、すごく静か。
1番目のものより個人的には気に入っています。でも高さがあるのでそこは運用方法に合わせて購入でしょうかねぇ。
f:id:Lynx-EyED:20190410220901j:plain

vcgencmd measure_temp
temp=42.0'C


冷却性能も良さげでした。




 

Swift PlaygroundsでのMVP実装を考えた

MVP実装考えなきゃいけないほど大規模開発しないよね

FatViewControllerガーって言うほど、iPad単体で大規模なプログラムは書かないとは思うのですが…

  1. 組み込みデバイスと通信するプログラムが多いので、ViewControllerが組み込みデバイスとの通信APIとごちゃごちゃになるのは必至
  2. 別のViewアーキテクチャの機器に移植するのが大変になる
  3. API変更があった場合の変更が辛くなる

MVCから逃げたいのはこのくらいかな。MVPにしたのはその他のアーキテクチャにくらべて学習コストが低そうと言うだけです。

MVPについてちょっとお勉強した

参考にしたサイト

そのまま、Model - View - Presenterで成り立っており、比較的それぞれが疎結合になっているアーキテクチャ
f:id:Lynx-EyED:20181018214726p:plain
Presenterの役割が重要で、

  • Viewからイベントを受け取り、Modelに処理を移譲
  • Modelから結果を受け取り、Viewに通知

したがってModel、Viewとも、互いに独立している。
今回はModelはNotificationCenterを使い、Presenter通知しています。
f:id:Lynx-EyED:20181018215939j:plain
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

参考資料


iOS 11 Programming

iOS 11 Programming

  • 著者:堤 修一,吉田 悠一,池田 翔,坂田 晃一,加藤 尋樹,川邉 雄介,岸川克己,所 友太,永野 哲久,加藤 寛人,
  • 発行日:2017年11月16日
  • 対応フォーマット:製本版,PDF
  • PEAKSで購入する

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に固定してしまっています。複数の折れ線グラフも楽々。


f:id:Lynx-EyED:20171106153539p:plain

動作してるところ


Swift ScrollView plot

感想

iPad上のSwift Playgroundsで結構しっかりしたプログラム組めるのはすごいなと思いました。電車内やカフェで殴り書きするのにちょうどいい。
でも、ガチなソフト組むとなると、StoryBoardが欲しくなるしエディタとしては機能が限定されているので、MacXCodeが必要になりますね。

コード

本家
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()