M5Stack でシリアル通信

日曜電子工作の続き。

M5StackとMacとの間でシリアル通信を試してみる。
M5Stackの開発ではホストPCとUSBケーブルで接続してシリアル通信をプログラムの転送を行うので、その環境をそのまま利用する。

M5Stack側

M5Stack側のプログラムは前回写経した内蔵加速度センサのサンプルを少し改造して取得したデータをシリアルポートに出力するようにする。

#define M5STACK_MPU6886 

#include <M5Stack.h>
#include "utility/MPU6886.h"

#define MULTISAMPLE 20

void readAccelMulti(float *ax, float *ay, float *az, int multi) {
  float x, y, z;
  float _x, _y, _z;

  x = y = z = 0;
  for (int i = 0; i < multi; i++) {
    M5.IMU.getAccelData(&_x, &_y, &_z);
    x += _x;
    y += _y;
    z += _z;
  }
  *ax = x / multi;
  *ay = y / multi;
  *az = z / multi;
}

void drawGrid() {
  M5.Lcd.drawLine(41, 120, 279, 120, CYAN);
  M5.Lcd.drawLine(160, 1, 160, 239, CYAN);
  M5.Lcd.drawCircle(160, 120, 119, CYAN);
  M5.Lcd.drawCircle(160, 120, 60, CYAN);
}

int oldX = 0;
int oldY = 0;

void drawSpot(int ax, int ay) {
  int x, y;
  x = map(constrain(ax, -300, 300), -300, 300, 40, 280);
  y = map(constrain(ay, -300, 300), -300, 300, 240, 0);
  M5.Lcd.fillCircle(oldX, oldY, 7, BLACK);
  drawGrid();
  M5.Lcd.fillCircle(x, y, 7, WHITE);
  oldX = x;
  oldY = y;
}

float offsetX, offsetY, offsetZ;
#define MOVINGAVG 10
float movingavgx[MOVINGAVG], movingavgy[MOVINGAVG];
int _index = 0;

void setup() {
  M5.begin();         // INIT M5Stack
  M5.Power.begin();   // 
  M5.IMU.Init();     // Init MPU6886

  readAccelMulti(&offsetX, &offsetY, &offsetZ, MULTISAMPLE);
  for (int i = 0; i < MOVINGAVG; i++) {
    movingavgx[i] = offsetX;
    movingavgy[i] = offsetY;
  }
}

int serialStart = 0;

void loop() {
  float x, y, z;
  float ax, ay;

  M5.update();

  readAccelMulti(&x, &y, &z, MULTISAMPLE);
  movingavgx[_index] = x;
  movingavgy[_index] = y;
  _index = (_index + 1) % MOVINGAVG;

  ay = ax = 0;
  for (int i = 0; i < MOVINGAVG; i++) {
    ax += movingavgx[i];
    ay += movingavgy[i];
  }
  ax = ax / MOVINGAVG;
  ay = ay / MOVINGAVG;

  if (M5.BtnA.wasPressed()) {
    offsetX = ax;
    offsetY = ay;
  }  
  if (M5.BtnC.wasPressed()) {
    serialStart = (serialStart == 0) ? 1 : 0;
  }

  if (serialStart == 1) {
    Serial.printf("{x:%f, y:%f}¥n", ax- offsetX, ay - offsetY);
  }
  
  drawSpot((int)((ax - offsetX) * 1000), (int)((ay - offsetY) * 1000));
  delay(100);
}

シリアルポートのオープンは M5.update()の中で既に行われているのでデフォルトの通信速度(115,200bps)で問題なければ何もする必要はないみたい。
シリアルポートへの読み書きは、Serial オブジェクトのメソッドを利用して行う。ここらへんは普通に Arduino 互換な感じでいける。リファレンスにはprintf()は存在しないがなんか使えるっぽい。

Mac

親機であるMac用のプログラムはPythonで書いてみた。といってもたったの5行。

import serial
ser = serial.Serial("/dev/cu.SLAB_USBtoUART", 115200, timeout=0.1)
while True:
    line = ser.readline()
    print(line)

シリアル通信にはpyserialを使用するので、事前にpipを利用してインストールしておく。
通信ポートは開発環境をセットアップするときにインストールしたUSBのシリアルドライバによって生成されたデバイスを指定する。

$ pip3 install pyserial

実行してみる

M5StackとMacをUSBケーブルで接続、プログラムを転送してM5Stackの電源を入れてから、Mac側の受信プログラムを実行する。

$ python3 serial_test.py
b'{x:-0.019182, y:-0.096029}\xc2\xa5n'
b'{x:-0.003403, y:-0.126227}\xc2\xa5n'
b'{x:-0.031735, y:-0.192700}\xc2\xa5n'
b'{x:-0.045695, y:-0.215953}\xc2\xa5n'
b'{x:-0.048987, y:-0.247052}\xc2\xa5n'
b'{x:-0.049235, y:-0.274598}\xc2\xa5n'
b'{x:-0.048007, y:-0.308534}\xc2\xa5n'

とりあえずデータの受信はできているようだけど、前後に謎のデータが付与されている。 これらはなんじゃろかい?

HHKB Professional HYBRID Type-S

今年はあまり大きな買い物をしていないので、年末も近づいたことだし少し経費としてなにか計上しておきたいと思い、ちょっと贅沢ではあるがPFUHappy Hacking Keybord の新しいモデルを出したので購入することに。

常に同じキーボードを使う訳ではない(キーボードの持ち込みのできない現場や、ノートパソコンのキーボードを使うこともある)ので配列は日本語配列にした。色は墨。

今までは、ARCHISSのメカニカルキーボードを使用していたのだけど、静電容量無接点方式のHHKBとはキータッチの印象はかなり違う。指先の感触は断然HHKBの方が良いけど、メカニカルキーボードのあのカチャカチャ音がしないのはなんだか寂しい。あの音うるさいけれどテンションが上がるものがある。

あと、ついでにAnker のUSB Type-CのHUBも購入。年末の休みを利用して古いiMacを処分してMacbook Pro 15inch をメインに使いやすいように自室の配置を考えたい。

WSLを試してみる。

今、週末やっているサポートの仕事の対象がクライアント側だけだったのが、来年からサーバー側も面倒を見ることになった。

サーバーはVPS上のLinuxPHP + PostgreSQL の環境で動作している。検証・開発用の環境を新たに作る必要があるのでどうするか考え中。

案としては、

  1. 本番環境とどうようにVPSを借りてLinuxのサーバーを立ち上げる。
  2. クライアントの開発用マシンに Vagrant で仮想サーバーを立ち上げる。
  3. クライアントの開発用マシンのWSL上に PHPPostgreSQL をインストールする。

が浮かんだが、一番手軽な案3で試してみることにする。

とりあえずの下準備として、Microsoft Store で Ubuntu 18.04 LTSをインストールして、apt update & apt upgrade を済ませる。


追記 2019.12.14

WSL 環境に、apt で、PostgreSQL, PHP, Apache2 をインストール。普通に apt install hoge hoge するだけで特に問題なし。
WSL とは関係ないけど、PostgreSQL がローカルホストで接続する場合、Pear 認証が有効になっているため WSLのユーザーアカウント名と PostgreSQLに接続するロールの名称が一致していないとはじかれてしまう。回避方法はあるらしいけど、面倒なのでWSLのアカウントと 同じロールを作成することにした。

M5Stack 加速度センサーを使ってみる。

日曜電子工作の続き。今回は M5Stack Gray に内蔵されている加速度センサーを使ってみる。

入門書「みんなのM5Stack入門」に簡単なサンプルが掲載されているので写経してみるが、ちょっと問題がある。書籍では9軸のセンサーにMPU9250というチップが使用されていることが前提になっているが、最近の出荷分はMPU6886という別のチップが搭載されており、そのままでは動作しない。そこで、サンプルのMPU9250を使用しているコードをMPU6886用のコードに書き換える必要がある。

作業は基本逐次的な置き換えで済むので難しくはない。というか、MPU6886の方がキャリブレーションが必要なかったりX,Y,Z 3軸の値を一度の関数呼び出しで取得できたりとむしろ簡易になっている。

書籍の水準器のサンプル level0.ino をMPU6886用に修正したものが次のソース。

#include <M5Stack.h>
#include "utility/MPU6886.h"

MPU6886 IMU;  // Create MPU6886 Object

void drawGrid() {
  M5.Lcd.drawLine(41, 120, 279, 120, CYAN);
  M5.Lcd.drawLine(160, 1, 160, 239, CYAN);
  M5.Lcd.drawCircle(160, 120, 119, CYAN);
  M5.Lcd.drawCircle(160, 120, 60, CYAN);
}

void drawSpot(int ax, int ay) {
  int x, y;
  x = map(constrain(ax, -300, 300), -300, 300, 40, 280);
  y = map(constrain(ay, -300, 300), -300, 300, 240, 0);
  M5.Lcd.fillScreen(BLACK);
  drawGrid();
  M5.Lcd.fillCircle(x, y, 7, WHITE);
}

void setup() {
  M5.begin();     // INIT M5Stack
  Wire.begin();   // INIT I2C
  IMU.Init();     // Init MPU6886
  
}

void loop() {
  float ax, ay, az;
  IMU.getAccelData(&ax, &ay, &az);
  drawSpot((int)(ax * 1000), (int)(ay * 1000));
  delay(100);
}

修正箇所は、

  1. 9軸センサーのライブラリ用ヘッダファイルを MPU6886.h に変更。
  2. それにともない、ライブラリのクラスもMPU6886に変更。
  3. 初期化は Init() メソッドを呼ぶだけに。キャリブレーションは不要。
  4. 加速度の取得は getAccelData() メソッドを呼ぶだけに。スケールを取得しての補正は不要。

という感じ。

Rust 入門 簡単な関数を書いてみる(5)

Rust 入門続き。

住所を正規化する関数、ひとまず完成。 住所の文字列から、都道府県、市区町村、番地、建物を分割し表記を正規化した構造体を返す。

文字列を文字単位に分割するには、chars() を呼び出せばよいというのはすぐに分かったが、char からユニコードのコードポイントに変換するのにちょっと迷った。
答えは知ってみれば単純で、単に u32にキャストしてやれば良いだけだった。逆にコードポイントから char に変換するには、

char::from_u32(0xff10).unwrap()

とすれば良い。

関数はトレイトを利用して構造体の静的メソッド風に呼び出されるようにしてみた。
Rustにはクラスはないが、トレイトを定義することで構造体にメソッドを生やすことができる。ここらへん golang に似てる。

extern crate regex;
use regex::Regex;
use std::collections::HashMap;

const PREFECTURES:[(&str, &str); 47] = [
        ("01","北海道"), ("02","青森県"), ("03","岩手県"), ("04","宮城県"), ("05","秋田県"),
        ("06","山形県"), ("07","福島県"), ("08","茨城県"), ("09","栃木県"), ("10","群馬県"),
        ("11","埼玉県"), ("12","千葉県"), ("13","東京都"), ("14","神奈川県"), ("15","新潟県"),
        ("16","富山県"), ("17","石川県"), ("18","福井県"), ("19","山梨県"), ("20","長野県"),
        ("21","岐阜県"), ("22","静岡県"), ("23","愛知県"), ("24","三重県"), ("25","滋賀県"),
        ("26","京都府"), ("27","大阪府"), ("28","兵庫県"), ("29","奈良県"), ("30","和歌山県"),
        ("31","鳥取県"), ("32","島根県"), ("33","岡山県"), ("34","広島県"), ("35","山口県"),
        ("36","徳島県"), ("37","香川県"), ("38","愛媛県"), ("39","高知県"), ("40","福岡県"),
        ("41","佐賀県"), ("42","長崎県"), ("43","熊本県"), ("44","大分県"), ("45","宮崎県"),
        ("46","鹿児島県"), ("47","沖縄県")    
    ];

const HANKAKU_ZENKAKU_KANA_CHARS:[(&str, &str); 89] = [
    ("ヴ", "ヴ"), 
    ("ガ", "ガ"), ("ギ", "ギ"), ("グ","グ"), ("ゲ", "ゲ"), ("ゴ","ゴ"), 
    ("ザ","ザ"), ("ジ","ジ"), ("ズ","ズ"), ("ゼ","ゼ"), ("ゾ","ゾ"), 
    ("ダ","ダ"), ("ヂ","ヂ"), ("ヅ","ヅ"), ("デ","デ"), ("ド","ド"), 
    ("バ","バ"), ("ビ","ビ"), ("ブ","ブ"), ("ベ","ベ"), ("ボ","ボ"), 
    ("パ","パ"), ("ピ","ピ"), ("プ","プ"), ("ペ","ペ"), ("ポ","ポ"), 
    ("ア","ア"), ("イ","イ"), ("ウ","ウ"), ("エ","エ"), ("オ","オ"),
    ("カ","カ"), ("キ","キ"), ("ク","ク"), ("ケ","ケ"), ("コ","コ"),
    ("サ","サ"), ("シ","シ"), ("ス","ス"), ("セ","セ"), ("ソ","ソ"),
    ("タ","タ"), ("チ","チ"), ("ツ","ツ"), ("テ","テ"), ("ト","ト"),
    ("ナ","ナ"), ("ニ","ニ"), ("ヌ","ヌ"), ("ネ","ネ"), ("ノ","ノ"),
    ("ハ","ハ"), ("ヒ","ヒ"), ("フ","フ"), ("ヘ","ヘ"), ("ホ","ホ"),
    ("マ","マ"), ("ミ","ミ"), ("ム","ム"), ("メ","メ"), ("モ","モ"),
    ("ヤ","ヤ"), ("ユ","ユ"), ("ヨ","ヨ"), 
    ("ラ","ラ"), ("リ","リ"), ("ル","ル"), ("レ","レ"), ("ロ","ロ"),
    ("ワ","ワ"), ("ヲ","ヲ"), ("ン","ン"),
    ("ァ","ァ"), ("ィ","ィ"), ("ゥ","ゥ"), ("ェ","ェ"), ("ォ","ォ"),
    ("ャ","ャ"), ("ュ","ュ"), ("ョ","ョ"), ("ッ","ッ"),
    ("、","、"), ("。","。"), ("ー","ー"), ("「","「"), ("」","」"),
    ("゙","”"), ("゚","'"), ("・","・")    
];

// 漢数字をアラビア数字に変換する
fn kanji_numeral_to_arabic_numerals(s: &str) -> String {

    // 漢数字を半角数字に置換する
    fn z2h(s: &str) -> String {
        s.replace("一", "1")
        .replace("壱", "1")
        .replace("1", "1")
        .replace("二", "2")
        .replace("弐", "2")
        .replace("2", "2")
        .replace("三", "3")
        .replace("参", "3")
        .replace("3", "3")
        .replace("四", "4")
        .replace("4", "4")
        .replace("五", "5")
        .replace("5", "5")
        .replace("六", "6")
        .replace("6", "6")
        .replace("七", "7")
        .replace("7", "7")
        .replace("八", "8")
        .replace("8", "8")
        .replace("九","9")
        .replace("9", "9")
        .replace("〇", "0")
        .replace("0", "0")
    };

    // 変換処理
    fn convert(s: &str, re: &Regex) -> i64 {

        let mut digits: HashMap<&str, i64> = HashMap::new();
        digits.insert("十", 10);
        digits.insert("拾", 10);
        digits.insert("百", 100);
        digits.insert("千", 1000);
        digits.insert("万", 10000);
        digits.insert("億", 100000000);
        digits.insert("兆", 1000000000000);

        let reg_divide_digit_unit:Regex = Regex::new("[十拾百千]|\\d+").unwrap();
        let reg_arabic_numerals:Regex = Regex::new("^[0-9]+$").unwrap();

        let mut unit:i64 = 1;
        let mut result:i64 = 0;
        for piece in re.find_iter(s).map(|m| m.as_str()).collect::<Vec<&str>>().iter().rev() {
            if digits.contains_key(piece) {
                if unit > 1 {
                    result += unit;
                }
                unit = digits[piece];
            } else {
                if reg_arabic_numerals.is_match(piece) {
                    result += piece.parse::<i64>().unwrap() * unit;
                } else {
                    result += convert(piece, &reg_divide_digit_unit) * unit;
                };
                unit = 1;            
            }
        }
        if unit > 1 {
            result += unit;
        }

        result
    };
    
    convert(&z2h(s), &Regex::new("[万億兆]|[^万億兆]+").unwrap()).to_string()
}

// 住所から都道府県とそれ以降を分割する
fn get_prefecture(address: &str) -> (String, String) {
    let prefectures = PREFECTURES.iter().map(|x| x.1).collect::<Vec<&str>>().join("|");
    let pattern = format!("^({})(.+*)$", prefectures);
    let re = Regex::new(&pattern).unwrap();

    match re.captures(address) {
        Some(m) => ((&m[1]).to_string(), (&m[2]).to_string()),
        None => ("".to_string(), address.to_string())
    }
}

// 住所から市区町村とそれ以降を分割する
fn get_city(address: &str) -> (String, String) {
    let re1 = Regex::new(concat!(
        "^(余市郡(仁木町|赤井川村|余市町)|余市町|柴田郡村田町|(武蔵|東)村山市|",
        "[東西北]村山郡...?町|田村(市|郡..町)芳賀郡市貝町|(佐波郡)?玉村町|[羽大]村市|",
        "(十日|大)町市|(中新川郡)?上市町|(野々|[四廿]日)市市|西八代郡市川三郷町|",
        "神崎郡市川町|高市郡(高取町|明日香村)|(吉野郡)?下市町|(杵島郡)?大町町)(.+)"
    )).unwrap();
    let re2 = Regex::new("^(.+[市区町村])(.+)").unwrap();

    match re1.captures(address) {
        Some(m) => {
            match re2.captures(&m[12]) {
                Some(m2) => (format!("{}{}",(&m[1]),(&m2[1])), (&m2[2]).to_string()),
                None => ((&m[1]).to_string(), (&m[12]).to_string())
            }
        },
        None => {
            match re2.captures(address) {
                Some(m2) => ((&m2[1]).to_string(), (&m2[2]).to_string()),
                None => ("".to_string(), address.to_string())
            }
        }
    }
}

// 半角カナを全角カナに、全角英数を半角英数に変換する
fn pre_convert(s: &str) -> (String) {
    let mut tmp = s.replace(" ", " ");
    for (hankaku, zenkaku) in HANKAKU_ZENKAKU_KANA_CHARS.iter() {
        tmp = tmp.replace(hankaku, zenkaku);
    }

    tmp.chars().map(|ch| {
        let code_point = ch as u32;
        if code_point >= 0xff10 && code_point <= 0xff51 {
            std::char::from_u32(code_point - 0xfee0).unwrap()
        } else {
            ch
        }
    }).collect()
}

// 建物名の正規化
fn normalize_building_name(name: &str) -> String {
    let re = Regex::new("(.+?)(([\\d〇一二三四五六七八九十百千万]+)(階|F|F|号|号室))$").unwrap();
    match re.captures(name) {
        Some(m) => {
            let floor = kanji_numeral_to_arabic_numerals(&m[3]);
            let mut suffix = &m[4];
            if suffix == "F" {
                suffix = "階"
            }
            format!("{}{}{}", &m[1], floor, suffix)
        },
        None => {
            name.to_string()
        }
    }
}

// 住所から番地と建物名に分割する
fn get_address(address: &str) -> (String, String) {
    let all_num = "[\\d一二三四五六七八九十百千万]+";
    let pattern = format!("(.*?)({}({}|丁目|丁|番地|番|号|-|‐|ー|−|の|東|西|南|北)*({}|丁目|丁|番地|番|号))(.*)",
                    all_num, all_num, all_num);
    let re = Regex::new(&pattern).unwrap();
    match re.captures(address) {
        Some(m) => {
            let re2 = Regex::new(all_num).unwrap();
            if re2.is_match(&m[2]) {
                let address_number = re2.captures_iter(&m[2]).map(|x| (&x[0]).to_string()).collect::<Vec<String>>().join("-");
                (format!("{}{}", &m[1], address_number), normalize_building_name(&m[5]))
            } else {
                (format!("{}{}", &m[1], &m[2]), normalize_building_name(&m[5]))
            }
        },
        None => {
            (address.to_string(), "".to_string())
        }
    }
  }

// 住所構造体 
#[derive(Debug)]
pub struct Address {
    prefecture: String,
    city: String,
    address: String,
    building_name: String
}

impl Address {
    // 文字列から住所を正規化して構造体として返す
    pub fn from(addr: &str) -> Address {
        let (prefecture, tmp1) = get_prefecture(&pre_convert(addr));
        let (city, tmp2) = get_city(&tmp1);
        let (address, building_name) = get_address(&tmp2);

        Address {
            prefecture,
            city,
            address,
            building_name
        }
    }
}

詳解Swift 第5版

「詳解Swift」の第5版が出たので購入。 今回から判型が小型になって驚いた。

初版から出るたびに買い直しているが、第5版の目玉はカスタム属性とDSLに関する解説だろうか? SwiftUI に関係する章だが普通にSwiftUIで画面をポチポチ作る分には直接出てくる部分ではなさそうなので、そこら変は必要が出てきたらちゃんと読もうという感じでななめ読みで済ましてしまった。

基本的な文法の解説などに関しては、第4版から大きく変わったという印象はなし。

詳解 Swift 第5版

M5Stack ではじめてのLチカ

f:id:tricogimmick:20191117095929j:plain

先日購入したM5Stakは書籍に連動したスターターキットなので、LEDやセンサ、ブレッドボードなどがおまけについてくる。日曜工作ではじめてのLチカに挑戦してみることに。

といっても、やることはブレッドボードにLEDと抵抗をぶっ刺し、M5スタックのデジタル出力とグランドにジャンパワイヤで繋ぐだけ。あとは数行のスケッチを写経してM5Stackに転送するだけ。いとも簡単にLEDが点滅した。電気工作なんて小学生以来だが当時はブレッドボードなんて便利なものはなく全部半田ごてだった。それを思うと隔世の感がある。

ついでに、温度センサも試してみる。こちらは抵抗も不要なので直接センサをジャンパワイヤでM5Stackに繋げることができる。こちらも書籍のサンプルコードを写経して転送するとあっさり動く。ただ、アナログデータの読み込み時にM5Stackの内蔵スピーカーからプッという音が出る。なんかノイズを拾ってるっぽい。

検索してみると、「M5Stack analogRead() ノイズ問題(20180407解決) - t-yosh’s blog」という記事を見つけた。記事によると、スピーカーが接続されているGPIOの25番のピンに対して0を書き込んでやると良いとのこと。写経したスケッチの setup() 関数内に

dacWrite(25, 0);

を追記したところノイズは無事消えた。先人の知恵に感謝。

とりあえず、GPIOを使ったデジタルやアナログの入出力は簡単にできることは判ったんで、これを使ってなにか遊びたい。何か良いネタはないものか。

t-yosh.hatenablog.com