入手しやすいデバイスを使って作業効率アップ

M5StackやM5Paperなどを使って効率アップデバイスを作ります。

M5StackのWiFi.softAPでの失敗

ライブラリをアップデートしたら、今まで動作していたソフトが動かなくなる事があります。
今回はWiFiのsoftAPでトラブルが発生しました。
ライブラリを元に戻せばいいのですが、それだと新たに実装された機能を使うことができませんし、どのみち修正しないといけません。

原因はIPアドレスの設定で、ip(192.168.10.0)はNGで、ip(192.168.10.1)はOKだったというだけの話でした。
アクセスポイントなので、0にした方がいいかと安易に考えていたのが問題でした。
今まではライブラリの不具合で偶然動いていただけなのかもしれません。

問題点を探して検索を繰り返しましたが、その過程でいくつかの新たな知識を得る事ができました。
ソフトの大幅な修正は不要だったので、結果的にはプラスだったと考える事にします。

CoreS3とBMM150

先日、M5StackCoreS3を購入しました。
地磁気センサーのBMM150が標準搭載されており、今まで作った装置の置き換えに使えると考えたからです。
しかし、CoreS3のサンプルスケッチにはBMM150に関する物が見当たりません。
ライブラリをいくつか試したのですが、どれもBMM150を認識しません。

CoreS3の回路図を見ると、I2Cの配線がBMI270を経由しているようなので、
回路構成がこれに近い「M5Unit IMU Pro Mini」のサンプルスケッチを使うことにしました。

ライブラリをインストールして、サンプルスケッチの「getSensorData.ino」を開きます。
このままでは動かないので、一部を修正します。
それと、Grayとはチップの実装方向が違うみたいなので、読み出したデータにー1をかけます。
BMP280に関する部分は削除します。
以下、抜粋です。

    //#define BIM270_SENSOR_ADDR 0x68
    #define BIM270_SENSOR_ADDR 0x69

    //bmi270.init(I2C_NUM_0, BIM270_SENSOR_ADDR);
    bmi270.init(I2C_NUM_1, BIM270_SENSOR_ADDR);

    int16_t mx, my, mz = 0;
    bmi270.readMagneticField(mx, my, mz);
    mx=-mx;
    my=-my;
    mz=-mz;

BMM150のデータは補正が必要です。10秒くらいぐるぐる回して補正データを取得します。
補正部分のソースを掲載します。データはプリファレンス領域に保存します。
補正用データはsetup()で初期化しておきます。
以下、抜粋です。

Preferences prefs;
bmm150_mag_data mag_offset, mag_max, mag_min;

void bmm150_offset_save() {
    prefs.begin("bmm150", false);
    prefs.putBytes("offset", (uint8_t *)&mag_offset, sizeof(bmm150_mag_data));
    prefs.end();
}

void bmm150_offset_load() {
    if (prefs.begin("bmm150", true)) {
        prefs.getBytes("offset", (uint8_t *)&mag_offset, sizeof(bmm150_mag_data));
        prefs.end();
        Serial.println("bmm150 load offset finish....");
    } else {
        Serial.println("bmm150 load offset failed....");
    }
}

void bmm150_calibrate(uint32_t calibrate_time) {
    uint32_t calibrate_timeout = 0;

    M5.Lcd.println("FLIP AND ROTATE");
    M5.Lcd.printf("for %d mseconds\n", calibrate_time);
    
    calibrate_timeout = millis() + calibrate_time;
    while (calibrate_timeout > millis()) {
        if (bmi270.magneticFieldAvailable()) {
            int16_t mx, my, mz = 0;
            bmi270.readMagneticField(mx, my, mz);
            mx=-mx;
            my=-my;
            mz=-mz;
            if (mx) {
                mag_min.x = (mx < mag_min.x) ? mx : mag_min.x;
                mag_max.x = (mx > mag_max.x) ? mx : mag_max.x;
            }
            if (my) {
                mag_max.y = (my > mag_max.y) ? my : mag_max.y;
                mag_min.y = (my < mag_min.y) ? my : mag_min.y;
            }
            if (mz) {
                mag_min.z = (mz < mag_min.z) ? mz : mag_min.z;
                mag_max.z = (mz > mag_max.z) ? mz : mag_max.z;
            }
        }
        delay(100);
    }

    mag_offset.x = (mag_max.x + mag_min.x) / 2;
    mag_offset.y = (mag_max.y + mag_min.y) / 2;
    mag_offset.z = (mag_max.z + mag_min.z) / 2;
    
    M5.Lcd.println(mag_offset.x);
    M5.Lcd.println(mag_offset.y);
    M5.Lcd.println(mag_offset.z);
    
    bmm150_offset_save();
}

void setup() {
    auto cfg = M5.config();
    M5.begin(cfg);
    M5.Ex_I2C.begin();
    M5.Display.setTextSize(2);
    unifiedButton.begin(&M5.Display);

    //bmi270.init(I2C_NUM_0, BIM270_SENSOR_ADDR);
    bmi270.init(I2C_NUM_1, BIM270_SENSOR_ADDR);
    
    mag_max.x = -2000;
    mag_max.y = -2000;
    mag_max.z = -2000;
    mag_min.x =  2000;
    mag_min.y =  2000;
    mag_min.z =  2000;

CoreS3はボタンABCがありませんので、「 unifiedButton」ライブラリを使用しました。
これで移植が簡単になります。

人手不足と業務の細分化

以前テレビでスキルや資格が必要な業務とそれ以外を分ける手法を紹介していました。
解析した結果、専門職でなければ行ってはならない仕事は半分以下だったようです。
こういった作業の細分化は、製造業ではよく使われる手法です。
細分するレベルが違いますが、工程分解とか呼ばれていたりします。
ある業種がすぐに思いつく手法も、他の業種では思いもよらないという事があるという事でしょう。
私も肝に銘じなければなりません。

採用した人が数年以内で退職してしまう事もよくあります。
原因の一つは、配属先の意識が昔のままだからかもしれません。
新人にいつまでも雑巾がけのような仕事をさせていると辞めてしまいますし、
パートさんもぞんざいに扱えば悪い噂がたって集まりません。
「俺ががんばるから問題ない」とが言っている人もいますが、自分が年をとるという事に気づいていないようです。
近所でよく聞く話でも、
「自分が定年になるなんて思ってもみなかった」とか
「知らない間に年をとっていた」とか本気で驚いています。
私も自覚していますが、そもそも自分が思っているほど動けてはいないものです。

iOSのメモアプリで7セグのOCR

昔の装置の説明書をテキスト化しようとしてメモアプリを起動した時の事です。
おそまきながら、リアルタイムのOCR機能が実装されてる事に気づきました。
そこで、デジタルメーターの読み取りを試してみることにしました。
結果ですが、撮像状態によっては「8」が「日」になったりしましたが、だいたい読み取れるようです。
7セグは無理だろうと思っていたので意外でした。
数字のみOCRする方法はみつけられませんでしたが、設定アプリで言語を英語優先にすると日本語は無視されるようになりました。

この機能に興味を持ったのは、点検でメーターを確認する事があるからです。
数字が回るタイプのアナログメーターはいいのですが、多機能のデジタルメーターが少しやっかいです。
電力メーターなどがそうですが、表示部分は1か所で表示切替ボタンなどは無く、10秒おきくらいで表示データーが変わる物があります。
いちど読み逃すと次の表示まで待つことになります。60秒くらいなものなのですが...長い。
それに10秒あれば余裕で点検表に書き写せそうですが、そうでもありません。
桁数が7桁とかになると、読み間違いが発生したりしするので、1周待って確認したりします。
人がやるとこんな具合ですので、メーター読み取りサービスが多くの会社から販売されています。

CSVファイルから3D散布図を作成

3軸散布図の作成方法はすぐに見つかりました。
作図はPythonのpyplotを使用し、CSVファイルはpandasを使って読み出します。
出来た散布図がこれです・・・。スタート位置とゴール位置が大幅にずれています。
坂を上って下って元の位置に戻ったのですが、上り坂と下り坂で歩幅が違うからかもしれません。

3D散布図

しかし、これは便利なツールです。
今までPythonはあまり興味が無かったのですが、これは使わない手はないでしょう。
ActiveXコントロールのような手軽さです。

コードは以下の通りです。

from matplotlib import pyplot as plt
import pandas as pd

col_names = ['x', 'y', 'z', 'm']
data = pd.read_csv('data.csv', names=col_names)
listX = data["x"]
listY = data["y"]
listZ = data["z"] 
listM = data["m"]
fig = plt.figure(figsize=(8, 6))
ax = fig.add_subplot(projection='3d')
cm = plt.cm.get_cmap('RdYlBu')
mappable = ax.scatter(listX, listY, listZ, c=listM, cmap=cm, linestyle='-')
fig.colorbar(mappable, ax=ax)
ax.plot(listX,listY,listZ, c="black", linestyle='-')
plt.show()

以下はCSVデータを作るための、エクセルのワークシートの計算式です。
方角は90度単位にしました。

H=ROUND(方角/90,0)*90
X1=X0+ROUND(歩幅*SIN(RADIANS(H)),0)
Y1=Y0+ROUND(歩幅*COS(RADIANS(H)),0)

データの一部は以下の通りです。X,Y,Z列のほかにマーク位置の列を追加しています。
X軸方向を北で計算しています。Z列は気圧です。
高度が上がると気圧が下がってしまうので-1をかけていますが、pyplotで指定できるのかもしれません。
XYZ位置は線でつないで、マーク位置を散布図のように表示しています。

-50,50,-2425,1
-100,50,-2427,
-150,50,-2426,
-200,50,-2425,
-250,50,-2428,
-300,50,-2427,2

精度が良くなれば、なにか屋外用ゲームに使えるかもしれません。

M5GOで作った移動計に階段を認識させるには

先日作成したM5GO移動計は、高度を測定できないのでプロットすると各階のデータがかぶってしまいます。
階段を検出できればいいのですが、加速度センサーでは難しそうです。
やり方を検索してみましたが、どのページも検出は結構シビアであると結んでありました。
階段を上り下りすると、歩行時より加速度は大きく振れるのですが、上りと下りの区別がつきません。
少し試行錯誤してみましたが、加速度センサーでの検出はあきらめる事にしました。
あきらめは早い方です。

そこで次の手ですが、M5GOのキットには気圧センサーが付いていますので、これを試します。
出力されるデータはPa(パスカル)なので、よほど低い階段でないかぎり、階の違いは判別できます。
以下、追加したコードです。

float pressure;
bool BMP280_present = false;
Adafruit_BMP280 bme;

  M5.Lcd.println("BMP280 INIT ...");
  if(!bme.begin(0x76))
  {  
    M5.Lcd.println("BMP280 ERROR ...");
    BMP280_present = false;
  }
  else
  {
    BMP280_present = true;
  }

  if(BMP280_present==true)
  {
    pressure=bme.readPressure()-100000;//101325;
  }
  else
  {
    pressure=0;
  }

測定データはパスカルそのままですが、データの上の方の桁はあまり意味が無いので、100000ほど引き算しています。
標準の気圧は101325Paだそうです。
(ミリバールからヘクトパスカルに変わった時はかなり違和感があったものです)
天気図の気圧と比較してみましたが、大きな違いは無いようでした。

さて、データがとれたのでプロットさせようとしたのですが、エクセルでは3軸散布図の機能を見つけられませんでした。
工夫すればそれらしい表示にできるようですが、私には難易度が高く、早々にあきらめて次の手を探す事にしました。

昔買ったM5GOで移動計を作る

昔買ったまま放置していたM5GOで何か出来ないかと考えていたところ、初期のM5GOには地磁気センサーが標準搭載されている事に気づきました。
(M5GOには昔から搭載されていないと思い違いをしていました)

加速度センサーで歩数を、地磁気センサーで方位を測定して、1歩ごとの方位を記録すれば簡易移動ロガーができそうです。
GPSが無かった頃の初期のカーナビみたいなやり方です。
加速度センサーは標準のライブラリを使い、地磁気センサーのBMM150はライブラリマネージャでインストールします。私は、M5_BMM150を使用しました。
作り方を調べてみましたが、歩数計の部分の作り方は大体同じです。
方位磁石もサンプルスケッチを参考にしました。

どちらのデータもちらつくので、直前までのデータを何個か保存しておいて、平均をとります。保存する個数はソフトを作りながら決定しました。
1歩ごとに方位を記録していきます。
M5GOの仕事はここまでで、データはCSVファイルにしてパソコンで加工します。

BMM150のサンプルスケッチがベースになっています。追加した部分のみ掲載します。
データ保存とかはそれぞれだと思いますので掲載していません。
加速度センサーは使う前に平らな場所で補正する必要があります。
地磁気センサーは毎回補正しなくてもいいようです。

補正値を計測する部分と、それを適用する部分です。
私はいつも、サンプルコードのオリジナル部分と区別するため、{}の付け方とタブ数を変えています。

float acc[3];
float gyro[3];
float aOfs[3];
float gOfs[3];

void readGyro()
{
 M5.MPU6886.getGyroData(&gyro[0], &gyro[1], &gyro[2]);
 M5.MPU6886.getAccelData(&acc[0], &acc[1], &acc[2]);
}

void mpu6886_calibrate()
{
 float gSum[3];
 float aSum[3];
 for(int i = 0; i < 500; i++)
 {
  readGyro();
  gSum[0] += gyro[0];
  gSum[1] += gyro[1];
  gSum[2] += gyro[2];
  aSum[0] += acc[0];
  aSum[1] += acc[1];
  aSum[2] += acc[2];
  delay(2);
 }
 gOfs[0] = gSum[0]/500;
 gOfs[1] = gSum[1]/500;
 gOfs[2] = gSum[2]/500;
 aOfs[0] = aSum[0]/500;
 aOfs[1] = aSum[1]/500;
 aOfs[2] = aSum[2]/500 - 1.0;
}

void apply_mpu6886_calibrate()
{
 gyro[0] -= gOfs[0];
 gyro[1] -= gOfs[1];
 gyro[2] -= gOfs[2];
 acc[0] -= aOfs[0];
 acc[1] -= aOfs[1];
 acc[2] -= aOfs[2];
}

読み出したデータはちらつくので平滑化します。
バッファして平均をとります。

#define ACC_CNT_MAX 8
#define ACC_LOW 1040
float accBuf[ACC_CNT_MAX];

以下、セットアップ部分になります。
まずは加速度センサーMPU6886の初期化です。

void setup(void){
 M5.begin();
 M5.Power.begin();  //Init Power module.
 Wire.begin();
 M5.Lcd.setTextSize(2);
 adc_power_acquire();

 M5.Mpu6886.Init();
 M5.Mpu6886.setAccelFsr(M5.Mpu6886.AFS_2G);

地磁気センサーBMM150の初期化です。
地磁気センサーも補正が必要なのですが、サンプルスケッチでは補正値はNVS領域に保存されています。

 M5.Lcd.println("BMM150 INIT ...");
 if(bmm150_initialization() != BMM150_OK)
 {
  M5.Lcd.println("BMM150 ERROR ...");
  for(;;)
  {
   delay(100);
  }
 }
 M5.Lcd.println("BMM150 OFFSET LOAD ...");
 bmm150_offset_load();

最初に加速度センサーの補正を行います。

 M5.Lcd.println("MPU6886 CALIBRATION ...");
 mpu6886_calibrate();

地磁気センサーの補正を行います。毎回やらないくてもいいので、スキップできるようにしています。

 M5.Lcd.println("");
 M5.Lcd.println("BMM150 CALIBRATION btnC");
 M5.Lcd.println("      CANCEL btnB");

 for(;;)
 {
  M5.update();
  if(M5.BtnC.wasPressed())
  {
   M5.Lcd.println("FLIP AND ROTATE");
   M5.Lcd.println("for 10 seconds");
   bmm150_calibrate(10000);
   break;
  }
  if(M5.BtnB.wasPressed())
  {
   break;
  }
 }

 M5.Lcd.fillScreen(BLACK);
 M5.Lcd.setTextSize(4);

以下ループ部分です。地磁気センサーと加速度センサーを読み出します。
地磁気は±180度に変換し、10度単位にします。
加速度はXYZ軸全部を2乗して足して平方根をとります。

int max0=-10000;
int min0=10000;
float a0;
float ax, ay, az;
int mark=0;
int accCnt=0;
int flg0=0;
int pedometer=0;
int acc0=0;
int stepNum=0;

struct pedoStruct
{
 int cnt;
 int tm;
 int head;
 int pre;
 int mrk;
} pedoData[3600];

unsigned long tm0=0;

void loop(void){
 tm0=millis()/1000;
 M5.update();  //Read the press state of the key.
 bmm150_read_mag_data(&dev);
 float head_dir=atan2(dev.data.x-mag_offset.x,dev.data.y-mag_offset.y)*180.0/M_PI;
 int head_dir2=((int)head_dir/10)*10;
 readGyro();
 apply_mpu6886_calibrate();

 ax=acc[0];
 ay=acc[1];
 az=acc[2];
 a0=sqrt(ax*ax+ay*ay+az*az);

平滑化するためにバッファします。
計算結果が閾値以上なら歩数カウントアップします。
最大値と最小値も保存しておきます。

 accBuf[accCnt]=a0;
 accCnt++;
 if(accCnt>=ACC_CNT_MAX)
 {
  accCnt=0;
 }

 float accSum=0;
 for(int j=0; j<ACC_CNT_MAX; j++)
 {
  accSum+=accBuf[j];
 }
 acc0=(accSum/ACC_CNT_MAX)*1000;

 if(acc0>max0)
 {
  max0=acc0;
 }
 if(acc0<min0)
 {
  min0=acc0;
 }

 if((flg0==0)&&(acc0>ACC_LOW))
 {
  pedometer++;

歩数カウントがアップしたら測定データを保存し、一時データを初期化します。
フラグをONして、加速度が1G以下になるまでスルーさせます。

  max0=-10000;
  min0=10000;
  for(int j=0; j<ACC_CNT_MAX; j++)
  {
   accBuf[j]=1;
  }
  pedoData[pedometer].cnt=pedometer;
  pedoData[pedometer].tm=millis()/1000;
  pedoData[pedometer].head=head_dir2;
  accCnt=0;
  flg0=1;
 }

 if((flg0==1)&&(acc0<1000))
 {
  flg0=0;
 }