RTKのおかげ様で今まで不可能だったスキーのターン軌跡が測定できるようになったので、スキーの運動解析が進んでます。一般のスキーは横滑りしながら滑走してます。横滑りでエネルギをロスさせながら制動をかけてます。
制動をかけなければ重力加速度のまま加速されて50-80kmhまであっという間に上がってしまいます。
※2020年12月19日追記
本記事が2020年1月10日ですが、スキーの横滑りと精密軌跡の測定は、RTK MovingBase法で計測成功しました。9軸加速度センサでは、スキーの振動と姿勢変動を拾ってノイズに埋もれてしまって失敗しました。ここ5年間スキー滑走運動の測定にトライしてきましたがRTK MovingBase法で解決できました。以下の記事をご覧ください。
2021年は、左右スキーにアンテナ計4個とりつけて、F9P4CH測定します。
●追加したいセンシング項目
スキー研究論文を調査すると人間の体の動きを中心に研究されております。要するにどういう体の動きをすればスキーを安全に早くきれいに滑れるかの研究です。私の場合は、人間の体は、自分の動作なので、自分がどう滑っているかの滑走データをインプットすることで、自分で制御を変えながら試行錯誤して滑走データをいろいろインプットすることで上達できるとおもいます。ですので、体ではなく、足からスキーに入る力からスキーがどういう挙動をするのかをデータ化できれば、あとは、人間の素晴らしいフィードバック制御で上達できるはずということでセンシング項目を選びます。
●横滑りセンシング
切れるターンとずれるターンがあります。通常切れるターン(カービング)が美しくてよいとされてます。私も切れるターンをやりたいのですが
安定してはできてません。切れるターンとずれるターンの定量化ができれば自分の滑りの定量評価ができるので是非ともセンシングしたいところです。
原理としてRTKで得られた接線とのスキー板の挟角を横滑り角とします。
ロードバイクで使っていたLSM9DS1を流用する方法か、
スキー活動力計で開発したスマホロガーで、IMU付きスマホをスキーに取り付けるかどちらかです。防水加工など手作りが面倒なのでまずは防水スマホでできるか検討してみます。このスマホロガーの開発記事群はこちらです。
KOCERAのTORQUE G01の中古機で内蔵の気圧センサと9軸IMUを活用してProcessing for AndroidでKETAIライブラリーを使って簡単にスマホプログラムが自由自在につくれました。
●気圧センサ
RTKの3D方向の精度はcm級になりません。そこで、気圧センサをつかって正確な高度変化を得ます。気圧センサならcm級の高度変化が簡単にえられます。
CASIOのサイトに気圧精密計算式がありました。
https://keisan.casio.jp/exec/system/1257609530
これをEXCELで計算すると
気圧は天候によってころころ変化しますが、スキー滑走の場合短時間で、相対的な気圧変化から高度差を計算するので、正確な高度差がでます。
これも、上記スマホロガーで代用できますが、以前スキー活動量計で計測したのは、BMP085でした。
気圧センサは、スキー活動量計で実績のあるBMP085を使います。
廃版になってますが、digikey にdatasheetはあります。
https://media.digikey.com/pdf/Data%20Sheets/Bosch/BMP085.pdf
●システム組み込み検討
今までは、マイコン手作りを極力避けてきたのですが、IMUと気圧センサをとりつけるとなると面倒なことになります。そこで、スマロガーをスキーに取り付けてしまうことをまず最初に検討します。
KYOCERA TORQUE G01は、防水、振動に強いタフな
スマホですので、スキー板に取り付けても大丈夫ではないかと思いますが、
実験してみないとわかりません、中古で3千円で購入したので、壊れても惜しくはありません。
●スマホのプログラムはProcessing for Androidが便利です
スマホのプログラムは、JAVAプログラムがメインですが、アマチュアが自作するには学習に手間がかかりすぎます。そこで、JAVAベースですが、簡単なUIでArduinoとよく似たIDEをもつProcessingを使ってIOTまわりのプログラミングをしてます。Processingは、WIN,MAC,LINUX,Android対応しているので、PCで作ったプログラムが他のOS環境でもそのまま走りますので、超便利です。Androidの場合はハードが特殊なので、専用のライブラリをつかうとスマホのセンサを自由に使えます。
こちらの記事群にProcessingの解説があります。
KETAIを使ったスマホプログラミング
●プログラムいじる
2年前に作ったProcessing for Androidのソースを見ながら、変更箇所を埋め込みます。2年ぶりにいじるので、Processingの最新バージョンをインストールしました。androidモードを使う場合は、できるだけ最新のprocessingバージョンをいれたほうがいいです。バグがFIXされて使い勝手がよくなってます。ということで2019年2月の最新版processing3.5.3
初めての方は、入門から応用まで親切なサポートをしてくれる善意のブログがあるのでそこで学習してみてください。1日で使えるようになります。
質問も丁寧に回答してくださります。MSLabo様のPROCESSINGのページ
①processingをインストール
https://processing.org/download/
③2年前のプログラムにパッチあて
1:加速度表示をやめて、磁気方位の表示に切り替え
2:気圧値から標高の計算式をいれてElevartion表示
以下ソースです。これは、Processingの
FILE>サンプル>Contributed Library>Ketai>MultipleSensorsを
まる写しして、GPSと気圧センサを追加しただけですので、小1時間で
完成できます。
操作0:コンパイル終了してスマホに書き込まれるとファイル名と同じアプリアイコンができてるので、タップすれば起動します。
操作1:左上のkeypressedをタップするとキーボードがでてきます。
sキーをタッチするとログがはじまります。キーボードを隠すには再度
kepress欄をタッチしてください。
ログは、日時をファイル名にしたCSVでスマホの内部ストレージのDCIMに保存されてますが、スマホの設定で変わるかもしれませので、適当に探してみてください。ログサンプリングは40-50msecです。
ログデータ:時刻msec単位,3軸磁気方位,緯度経度,気圧,標高,3軸加速度
操作2:ログをやめるには、keypressedをタップして、キーボードを表示させてからxキーをおすとログが終了してプログラムも終了します。
/** * <p>Ketai Sensor Library for Android: http://Ketai.org</p> * * <p>KetaiSensor Features: * <ul> * <li>handles incoming Sensor Events</li> * <li>Includes Accelerometer, Magnetometer, Gyroscope, GPS, Light, Proximity</li> * <li>Use KetaiNFC for Near Field Communication</li> * </ul> * <p>Updated: 2017-08-29 Daniel Sauter/j.duran</p> */import ketai.sensors.*; import android.location.Location.*; import ketai.ui.*;KetaiList selectionlist; KetaiVibrate vibe; double longitude, latitude, altitude, accuracy; KetaiLocation location; Location uic; KetaiSensor sensor; PrintWriter output; PVector magneticField, accelerometer; float light, proximity; float pres; float elev; //float tp; float sensorX, sensorY, sensorZ; // float px,py,px_1,py_1; String stat=””; int pKey=0; int n=0; int tim=0; int tim_1=0; int period=0; void setup() { fullScreen();// Create a new file in the sketch directory String fileName = createFileName(); output = createWriter(fileName); sensor = new KetaiSensor(this); sensor.start(); sensor.list(); accelerometer = new PVector(); magneticField = new PVector(); orientation(LANDSCAPE); location = new KetaiLocation(this); uic = new Location(“uic”); // Example location: the University of Illinois at Chicago uic.setLatitude(41.874698); uic.setLongitude(-87.658777);textAlign(CENTER, CENTER); textSize(displayDensity * 20); }void draw() {background(78, 93, 75); drawUI(); elev=((pow((1013.25/pres),0.190223)-1)*(273.5+2))/0.0065 ;// hPa -> Elevation m float Ayx=degrees(atan(accelerometer.y/accelerometer.x)); float Ayz=degrees(atan(accelerometer.y/accelerometer.z)); float Axz=degrees(atan(accelerometer.x/accelerometer.z)); text( //”Accelerometer :” + “\n” //+ “x: ” + nfp(accelerometer.x, 1, 2)+”=>atan y/x:”+nfp(Ayx,3,1) + “deg\n” //+ “y: ” + nfp(accelerometer.y, 1, 2)+”=>atan y/z:”+nfp(Ayz,3,1) + “deg\n” //+ “z: ” + nfp(accelerometer.z, 1, 2)+”=>atan x/z:”+nfp(Axz,3,1) + “deg\n” “\nMagneticField :” + “\n” + “x: ” + nfp(magneticField.x, 1, 2) + “\n” + “y: ” + nfp(magneticField.y, 1, 2) + “\n” + “z: ” + nfp(magneticField.z, 1, 2) + “\n” + “Location(GPS) :” + “\n” + “Latitude= ” + nf((float)latitude, 3, 6) + “\n” + “Longitude= ” + nf((float)longitude, 3, 6) + “\n” + “Pressure : ” + nf(pres,4,4) + “\n” + “Elevation: ” +nf(elev,4,4)+”\n” + “Log data N=: ” + nf(n,4,0) +”/”+nf(period,6)+ “\n” , 20, 0, width, height); tim_1=tim; tim=millis(); period=tim-tim_1; if (pKey==’s’){ stat=”Logging”; output.println(nf(hour(),2,0)+”,” + nf(minute(),2,0)+”,” + nf(second(),2,0)+”,”+nf(period,6)+”,”+nf(magneticField.x, 1, 2)+”,”+nf(magneticField.y, 1, 2)+”,”+nf(magneticField.z, 1, 2)+”,”+nf((float)latitude,3,6)+”,”+nf((float)longitude,3,6)+”,”+nf(pres,4,2)+”,”+nf(elev,4,4)+”,”+nf(accelerometer.x, 1, 2)+”,”+nf(accelerometer.y, 1, 2)+”,”+nf(accelerometer.z, 1, 2)); // Write the coordinate to the file n++; } if (pKey==’x’){//Exit the program output.flush(); // Writes the remaining data to the file output.close(); // Finishes the file this.getActivity().finish(); text(“Finished!”,0,0); } } void onBackPressed() { }void onPressureEvent(float p) {//: p ambient pressure in hPa or mbar pres=p; }void onLocationEvent(Location _location) { //print out the location object println(“onLocation event: ” + _location.toString()); longitude = _location.getLongitude(); latitude = _location.getLatitude(); altitude = _location.getAltitude(); accuracy = _location.getAccuracy(); }void onAccelerometerEvent(float x, float y, float z, long time, int accuracy) { accelerometer.set(x, y, z); } String createFileName() { String fileName=”//sdcard/DCIM/”+ nf(year(), 2) + nf(month(), 2) + nf(day(), 2) +”-“+ nf(hour(), 2) + nf(minute(), 2) + nf(second(), 2); fileName += “.csv”; return fileName; }void onMagneticFieldEvent(float x, float y, float z, long time, int accuracy) { magneticField.set(x, y, z); }void onLightEvent(float v) { light = v; }void onProximityEvent(float v) { proximity = v; } void drawUI() { pushStyle(); textAlign(LEFT); fill(0); stroke(255); rect(0, 0, width/3, 100); rect(width/3, 0, width/3, 100);rect((width/3)*2, 0, width/3, 100);fill(255); text(“KeyPressed=”+char(pKey), 5, 60); text(stat, width/3 + 5, 60); text(“x:Exit”, width/3*2 + 5, 60); popStyle(); } void keyPressed() { if (keyCode == BACK){ keyCode = 0; key=char(pKey); } if (key==’s’){ pKey=’s’; } if (key==’x’){ pKey=’x’; } } void mousePressed() { if (mouseY < 100) { if (mouseX < width/3) {KetaiKeyboard.toggle(this);} //else if (mouseX > width/3 && mouseX < width-(width/3)) //{KetaiAlertDialog.popup(this, “Pop Up!”, “this is a popup message box”);} //else //{vibe.vibrate(1000);} } }//void mousePressed(){ //タッチされた座標を覚える //px_1=px; //py_1=py; //px=mouseX; //py=mouseY; //output.flush(); // Writes the remaining data to the file //output.close(); // Finishes the file //} //public void mousePressed() { // if (sensor.isStarted()) // sensor.stop(); // else // sensor.start(); // println(“KetaiSensor isStarted: ” + sensor.isStarted()); //} /* available sensors/methods* void onSensorEvent(SensorEvent e) – raw android sensor event * void onAccelerometerEvent(float x, float y, float z, long a, int b): x,y,z force in m/s^2, a=timestamp(nanos), b=accuracy * void onAccelerometerEvent(float x, float y, float z): x,y,z force in m/s2 * void onOrientationEvent(float x, float y, flaot z, long a, int b): x,y,z rotation in degrees, a=timestamp(nanos), b=accuracy * void onOrientationEvent(float x, float y, float z) : x,y,z rotation in degrees * void onMagneticFieldEvent(float x, float y, float z, long a, int b) : x,y,z geomag field in uT, a=timestamp(nanos), b=accuracy * void onMagneticFieldEvent(float x, float y, float z): x,y,z geomagnetic field in uT * void onGyroscopeEvent(float x, float y, float z, long a, int b):x,y,z rotation in rads/sec, a=timestamp(nanos), b=accuracy * void onGyroscopeEvent(float x, float y, float z): x,y,z rotation in rads/sec * void onGravityEvent(float x, float y, float z, long a, int b): x,y,z force of gravity in m/s^2, a=timestamp(nanos), b=accuracy * void onGravityEvent(float x, float y, float z): x,y,z rotation in m/s^s * void onProximityEvent(float d, long a, int b): d distance from sensor (typically 0,1), a=timestamp(nanos), b=accuracy * void onProximityEvent(float d): d distance from sensor (typically 0,1) * void onLightEvent(float d, long a, int b): d illumination from sensor in lx * void onLightEvent(float d): d illumination from sensor in lx * void onPressureEvent(float p, long a, int b): p ambient pressure in hPa or mbar, a=timestamp(nanos), b=accuracy * void onPressureEvent(float p): p ambient pressure in hPa or mbar * void onTemperatureEvent(float t, long a, int b): t temperature in degrees in degrees Celsius, a=timestamp(nanos), a=timestamp(nanos), b=accuracy * void onTemperatureEvent(float t): t temperature in degrees in degrees Celsius * void onLinearAccelerationEvent(float x, float y, float z, long a, int b): x,y,z acceleration force in m/s^2, minus gravity, a=timestamp(nanos), b=accuracy * void onLinearAccelerationEvent(float x, float y, float z): x,y,z acceleration force in m/s^2, minus gravity * void onRotationVectorEvent(float x, float y, float z, long a, int b): x,y,z rotation vector values, a=timestamp(nanos), b=accuracy * void onRotationVectorEvent(float x, float y, float z):x,y,z rotation vector values * void onAmibentTemperatureEvent(float t): same as temp above (newer API) * void onRelativeHumidityEvent(float h): h ambient humidity in percentage * void onSignificantMotionEvent(): trigger for when significant motion has occurred * void onStepDetectorEvent(): called on every step detected * void onStepCounterEvent(float s): s is the step count since device reboot, is called on new step * void onGeomagneticRotationVectorEvent(float x, float y, float z): * void onGameRotationEvent(float x, float y, float z): * void onHeartRateEvent(float r): returns current heart rate in bpm */ |
●以後
スマホをスキーにうまく固定しないと元も子もないので固定方法を検討します。現地で試行錯誤やってみます。