最近やってるのは
- MIDI2LR のコントローラ
- 部品の納品待ち
- フルバランスのヘッドフォンアンプ
- DDC -> DAC でアナログにした時点でバランス。4ch 増幅するタイプ
- だいたいできてるけどモチベーションがない
- 結局アンバランスで使うやん?
あたり。なんかいまいち魅力に欠ける。
最近やってるのは
あたり。なんかいまいち魅力に欠ける。
500 Can't connect to lowreal.net:443 (certificate verify failed) とか先に書いていたが、掃除機を買っていた。
今まで 10.8V スライド対応の工具としてドライバドリルを持っていたので、掃除機はバッテリなしのモデルを買った。
マキタ(Makita) 充電式ドライバドリル 1.5Ah (バッテリー・充電器・ケース付) DF031DSHX cho45
ドライバドリル側はバッテリ2個と急速充電器付き。コードレスドライバドリルはほんと便利なので、持ってないなら電池2個セット付きのを今すぐ買いましょう。
マキタ コードレス掃除機CL107 紙パック式 標準25分稼働 軽量定番モデル 10.8Vバッテリ充電器別売 CL107FDZW cho45
掃除機はバッテリなしで紙パック式のものにした。紙パック式とはなってるが、洗って使えるダストカップも付属するので、基本的にはカプセル式の上位互換となっている。電池付属のも当然売ってます。
バッテリーを共有できるのは便利。共通バッテリーのシリーズラインナップは結構あるけど、家庭で最も使うのは掃除機とドリルドライバぐらいでしょう。
まずホースがないのと本体が小さいので、場所をとらずすっきり置いておけるこころに価値がある。露出した場所に突っ張り棒にフックをかけて、そこからこの掃除機をぶら下げておいてるけど、邪魔にならず気にならない。すぐに使えるのは便利。
うちの用途だと充電頻度も高くないので、あんまりバッテリーを気にせずコードレス化されたという感じがする。
1.5Ah のバッテリでの掃除機は連続可動時間は強 (スイッチを入れて最初のモード) で12分となっている。
結構スペック的には短く思えるが、実際使ってみると1日あたり1分もあれば十分に気になるところを吸いこめるので、1週間ほど無充電でいける。まぁルンバも併用しているので、広いところはおおむね綺麗というのはある。ルンバがカバーできないような、扉の裏とか、狭い洗面所とか、作業後のゴミの吸いこみとかの用途が多い。
バッテリーにはどうしても寿命があるので、交換式なのは嬉しい。掃除機なんて滅多に壊れるものではないので、内蔵型だとバッテリー寿命で製品寿命が決まるのが悲しい。
そもそも充電頻度が高くないけど、22分で充電が終わるのでだいぶ早い。そして標準モードでは25分稼動できるので、2つバッテリーで交互にずっと掃除できることになる。すごい。
ということで、最も覚えやすいのは以下の通り
だけ覚えておけば、●H が即座にどちらかわかるようになるので、おのずと●Lは逆で覚えられる。
図記号との対応は上記を覚えていれば簡単に導ける。●H ではスイッチと並列でランプがついているだけなので2接点ですむのに対し、●L は「常時点灯」と「同時点灯」と「異時点灯 (挙動はホタルスイッチと同じ)」で配線が変わるため3接点必要。
Low と High かと思って接点回路図を見つめて考えてみたけど、これではどっちがどっちだかわからず困った。参考書は暗記しろではなく、ちゃんと何の略称なのか書いておいてほしい…… 略称だけわかっていれば暗記する必要ないんだから。
「らしい」と書いたけど、根拠となる JIS C 0303 には何の略かは書いてないので、これが確かとはいえない。しかしこれで覚えやすいことは確かである。
ABS と PLA も試したけど PETG (Polyethylene terephthalate glycol-modified) が最良と思っている。
一方でPETG独特の問題もある
ということで、設定は
にするとうまくいきやすかった。フローは温度を決めてからテストプリントで見極める。接着性が高いのでサポート材の使用量に注意が必要。あんまり細かいところにサポートを入れると外すことができない (Simplify3D はサポート材が剥しやすいほうだけど、それでも)。
まぁとにかく PETG がおすすめ。
とにかく反り対策が難しい。
特に冬は絶対やめたほうがいい。膨張率が高いおかげで反りが激しいので、設計時にすら気を使う必要がある。当然スライサでプリント方向も気をつけないといけない。
ある程度大きなパーツを作るのはとても難しい。小さなパーツでもベッドとの接着が甘いと剥れてくるし、層を重ねると反ってくる。設計通りに寸法を出すのは、設計段階から気をつけるレベルでないとかなり難しい。つまり構造的に反りにくいようなリブをつけたりする必要がでてくる。
ヒートベッドは万能ではなくて、下層の反りを軽減してベッドから剥離することは防止することはできても、高さがある造形物が途中から反っていくのにはあまり効果がない。これを抑えるには室温を上げる必要があるが、実際はなかなか難しい。冬でも箱があれば30℃程度まで室温を上げられるが、この程度では反り対策にはあまり効果を感じない。
そもそもヒートベッドを100℃前後まで過熱するのは80℃前後まで過熱するよりも、余計にかなり時間がかかる。プリント開始されずもどかしい。ここで時間がかかるとそもそも試行錯誤しにくいということも罠のひとつ。
PLA はとても硬いのでやすりで微調整したり、ニッパーで強引にパーツの一部を切り取るとかがやりにくい。試行錯誤しようと思うとプリントしなおす前にプリント部品を修正してモデルに反映させてとやりたくなるけど、やりにくい。諦めてプリントしなおしたほうが精神的に楽に思えるぐらい。そうすると時間的にもフィラメント的にもコスト増である。
また耐候性がないので屋外で使えず、耐熱性が低いので高温になる場所 (高温といっても60℃ぐらい) では使えないので、用途が限られる。「プリントしやすい」以外は利点があまりないと感じる。けど肝心のプリントしやすさも、ノズルの詰まりやすさでスポイルされる部分がある (フィラメントに少量油を塗るオイラーが必要)。
工具用のバッテリーを流用する場合、以下のようなメリットがある。
工具用バッテリーは工事現場で使われるようなものなので、モバイルバッテリーのようなものより要求仕様が高い。サイズも容量にしては大きめで価格も高めだが汎用性が高い。
マキタの 10.8V リチウムイオンバッテリーシリーズには BL1015 (1.5Ah) BL1040B (4Ah) がある。Ah 表記だが、10.8V での Ah だとすると、それぞれ、16.2Wh、43.2Wh になる。このバッテリーは一般向け充電掃除機で使われているので工具以外でも馴染がある人は多いだろう。
マキタ コードレス掃除機CL107 紙パック式 標準25分稼働/充電22分 軽量定番モデル 10.8Vバッテリ充電器付 CL107FDSHW cho45
ニッケル水素 1900mAh 1.2V 10本直列だと 22.8Wh なので、1.5Ah のモデルはちょっと少なめに感じる。が、リチウムイオン電池は重量効率が良いのと、工具用バッテリーは上記の通り充電器が良くできていて充電速度が早いので、これを汎用的に使えると便利でしょう。
みてみるとバッテリー側には5端子ある。工具側には3端子。残り2つは充電器用っぽい。バッテリー側の表面をよく見てみるとプラスとマイナスの表示が書いてあり、普通に出力が常にされている。おそらく工具用にはプラス・マイナス・サーミスタの3端子だと思う。充電器用についてる2つの端子は何かわからない。(バッテリーの充電バランス用かもしれない)
マキタのリチウムイオンバッテリーは 10.8V のほか、 36V 18V 14.4V 7.2V のラインナップがある。14.4V 以上は価格が非常に高くなり、7.2V は小型工具しかない。基本的に家庭向けには 10.8V 一択に見える。10.8V には差し込み式のバッテリーもラインナップされてるが、スライド式のほうが良い(接点不良が起きにくいので)。
10.8V でもそこそこ電圧が高いので、もしさらに DC/DC コンバーターをつけてもスイッチング電流はそれほど大きくならずにすむ。
当然メーカー推奨の方法ではないが、バッテリー端子を直接引き出しで独自に使う。
バッテリーに合致する端子を作って、任意の汎用コネクタに変えることを考える。とりあえずは手元の無線機 (KX3) 用に DC φ 2.5mm センタープラスコネクタとして引き出す。
導体を扱うので 3Dプリンタだけではどうにもならない。接点として、今回はt=1mmの SUS430 の板を使うことにしてみた。SUS430 はステンレスの中でフェライト系というもので、比較的加工性が良いらしい。けど真鍮とかのほうがもっと加工性良さそうだし導体抵抗的にもいいかもしれない。
SUS430 板は CNC ミリングで加工した。あまり複雑な形にせず、最悪手で加工しても作れるような形とした。
端子を保持してスライドしてバッテリーと接続する部分は3Dプリンタで作成した。何度か出力しなおしてうまくマッチするようにした。
パーツは2つに分かれていて、ショート防止のため金属の露出部分を減らしてある。
Thingverse に STL をあげた。https://www.thingiverse.com/thing:2841303
リチウムイオンバッテリー直結だと思うので、素子素性そのままだと思うが念のため。
リチウムイオンバッテリーは1セルあたり3.6V。10.8V は3本直列ということになる。
公称3.6Vで、満充電4.2V、放電ぎりぎりでは2.75V 付近まで下がる。3本直列なら 8.25V〜12.6V。実用的には下限を9Vぐらいにしたほうが安心。
放電直前の電圧はそのうち追試
無線機に直結できるように配線してみたので、KX3 で実際に送信してみたところ、充電直後で 2A 程度は全く問題なくとれるようだった。
過電流保護がどこでかかるのか興味が湧いたので、電子負荷でさらに負荷をかけてみることとした。4.3Aまで出ることがわかった。
Original Prusa i3 MK2S Kit (3Dプリンタ) を買ってた | tech - 氾濫原 と書いたすぐ1ヶ月ほどあとに MK3 というメジャーアップグレードバージョンが販売されてしまってガックリきたが、部分的に MK3 からアップグレードを取り入れた MK2.5 Upgrade Kit というのが同時に用意され、直近購入ユーザ向けに割引の案内があったため、申し込んでおいていた。それが先日届いたのでアップグレード作業を行なった。
MK2S -> MK2.5 で以下がアップグレードされる。
MK2S でも割と十分なので、そんなにものすごく魅力的というほどではないが、フィラメントセンサーは便利で良さそう。
なお MK3 は MK2S から上記以外にも大幅に変わっていてもっと良さそうなんだけど、あいくにもう一台買うような余裕はない。
Prusa Knowledge Base に従ってやった。
キット内に PETG のフィラメントが入っていて、オマケかな?と思ったら、Gcode は用意してあるので、必要な 3D プリントパーツをまず今のでプリントしてください、という感じだった。そういえば確かに、既に動くプリンタがあるなら部品供給じゃなくて材料供給のほうが効率良いが、予想外だったのでちょっとびっくりした。
X軸はE軸と結合するパーツが全置き換え。E軸は完全に置き換え。地味に大変。
PINDA プローブやキャリブレーションがやりなおしなのも地味に大変だが、勘所は覚えてたので割とすぐ終わったっぽい。まだあまりプリントしてないのでこれからかな。
ファームが新しくなってウィザード形式で初回テストを行ったけど、ちょっとつまづくところがあった。
MIDI デバイスを Lightroom のコントローラとして使えるプラグインとして MIDI2LR というのがある。市販のMIDIコントローラを使ってLightroomの現像パラメータなどを可変できるというもの。
しかし意外と市販のMIDIコントローラでちょうどいいのがないという問題がある。インターフェイス上の制限から、最適なコントローラは単純なロータリーエンコーダとスイッチの組合せで、常にインクリメンタルな値を送るものとなる。
ちょうどいいのがないなら専用品を作れば良いので、まずブレッドボードレベルで実装してみる。
ロータリーエンコーダを処理するコードがあるのでちょっと長く見えるが main は短い。mbed の USBMIDI ライブラリのおかげ。
LPC11U35 で動かすことを前提として、ロータリーエンコーダのA相・B相を、それぞれ P0_11, P0_12 に繋ぐコードになっている。時計周りのときB相が先行するコードなので、逆のロータリーエンコーダの場合は逆にする。(ロータリーエンコーダの位相方向って決まってないっぽい?)
ロータリーエンコーダのチャタリング対策も入れてる。ググってみるとあんまりやってるコードを見ないけど必要ないのかな?
#include "mbed.h"
#include "config.h"
#include "USBMIDI.h"
class QEI {
static const int8_t INVALID = 2;
static uint8_t decode(const uint8_t prev, const uint8_t curr) {
/**
* 4bit decode table
* bit3 bit2 bit1 bit0
* [ prev A ][ prev B ][ curr A ][ curr B]
*
*/
switch ( (prev << 2) | curr) {
case 0b0000: return 0;
case 0b0001: return 1;
case 0b0010: return -1;
case 0b0011: return INVALID;
case 0b0100: return -1;
case 0b0101: return 0;
case 0b0110: return INVALID;
case 0b0111: return 1;
case 0b1000: return 1;
case 0b1001: return INVALID;
case 0b1010: return 0;
case 0b1011: return -1;
case 0b1100: return INVALID;
case 0b1101: return -1;
case 0b1110: return 1;
case 0b1111: return 0;
}
return INVALID;
}
volatile uint8_t prev;
Timeout timeout;
void interruptSample() {
uint8_t ok = sample();
if (!ok) error = 1;
}
void interruptDelay() {
// avoid chattering
timeout.attach(callback(this, &QEI::interruptSample), delay);
}
public:
volatile int position;
volatile uint8_t error;
InterruptIn phaseA;
InterruptIn phaseB;
float delay;
QEI(PinName _a, PinName _b, float _delay = 0.005) :
prev(0),
position(0),
error(0),
phaseA(_a),
phaseB(_b),
delay(_delay)
{
phaseA.mode(PullUp);
phaseB.mode(PullUp);
}
void enableInterrupt() {
phaseA.rise(callback(this, &QEI::interruptDelay));
phaseA.fall(callback(this, &QEI::interruptDelay));
phaseB.rise(callback(this, &QEI::interruptDelay));
phaseB.fall(callback(this, &QEI::interruptDelay));
}
/**
* sample digital input and return ok
*/
uint8_t sample() {
uint8_t curr = phaseA.read() << 1 | phaseB.read();
int8_t incr = decode(prev, curr);
prev = curr;
if (incr == INVALID) {
return 0;
}
position += incr;
return 1;
}
};
void show_message(MIDIMessage msg) {
switch (msg.type()) {
case MIDIMessage::NoteOnType:
printf("NoteOn key:%d, velocity: %d, channel: %d\r\n", msg.key(), msg.velocity(), msg.channel());
break;
case MIDIMessage::NoteOffType:
printf("NoteOff key:%d, velocity: %d, channel: %d\r\n", msg.key(), msg.velocity(), msg.channel());
break;
case MIDIMessage::ControlChangeType:
printf("ControlChange controller: %d, data: %d\r\n", msg.controller(), msg.value());
break;
case MIDIMessage::PitchWheelType:
printf("PitchWheel channel: %d, pitch: %d\r\n", msg.channel(), msg.pitch());
break;
default:
printf("Another message\r\n");
}
}
USBMIDI midi;
QEI encoder1(P0_11, P0_12);
int main() {
printf("init\r\n");
encoder1.enableInterrupt();
midi.attach(show_message);
while (1) {
if (encoder1.position) {
int8_t val = encoder1.position;
printf("send CC 1 %d 0\r\n", val);
midi.write(MIDIMessage::ControlChange(1, val, 0));
encoder1.position = 0;
}
}
} MIDI2LR の設定画面を開いた状態でロータリーエンコーダを動かすと、以下のように表示されるので、ここでは Exposure (露出) に割り当てている。
さらにこのボタンの部分を右クリックして、Adject CC dialog を出し、CC Message Type を Two's complement に設定する。
これですんなり動く。ロータリーエンコーダのA相・B相の仕様を確認しないと回転方向が逆になったりするのだけ注意が必要 (ソフトウェア側でなんとでもなるけど)
この調子でエンコーダーを増やしていけば実装上は良いことになる。しかしロータリーエンコーダ1つあたり2ピンのIOを使うので、実際は GPIO 拡張が必要になる気がする。
BLE Nano を使っていた自作キーボードだったが、Mac のアップデートとともにまた不安定になってしまい、使う気を失ってしまった (数時間ごとに再ペアリングが必要に)。直す気力もなくてしばらく放置していたが、そろそろ観念して USB 接続のキーボードに作りかえることとした。
作りかえるといっても、キーボードの部分は I2C GPIO のモジュールとして動くように作ってあるので、マイコンまわりを載せ変えて実装を書くだけになる。
ということで、タイトルの通り LPC11U35 を使ってキーボードを実装しなおした。
コード: https://github.com/cho45/keyboard-lpc11u35
mbed official に存在するライブラリの USBDevice の中に USBKeyboard というのがある。USBHID を継承していて、レポートデスクリプタとかを予め設定してくれる便利クラスとなっている。これは LPC11U35 でもちゃんと動くので基本的にハマるようなところはない。
標準で便利なメソッドがいくつか定義されているが、実際にキーボードを作る場合はこれらは使わない。USBKeyboard に定義されているメソッドはデモ用と考えていいと思う。
普通に使う場合は、レポートデスクリプタ定義はそのまま使いつつ (特に変更する必要がないので)、USBHID#send() を直接呼んで HID_REPORT を自分で構成して送ることになる。
#include "mbed.h"
#include "USBKeyboard.h"
#include "mcp23017.h"
#define REPORT_ID_KEYBOARD 1
#define REPORT_ID_VOLUME 3
USBKeyboard keyboard;
DigitalOut led(LED1);
DigitalIn key1(P0_4, PullUp);
int main() {
HID_REPORT report;
uint8_t modifier = 0;
uint8_t usage = 0;
report.data[0] = REPORT_ID_KEYBOARD;
report.data[1] = modifier;
report.data[2] = 0;
report.data[3] = usage;
report.data[4] = 0;
report.data[5] = 0;
report.data[6] = 0;
report.data[7] = 0;
report.data[8] = 0;
report.length = 9;
while(1) {
bool isKey1Pressed = key1.read() == 0;
if (isKey1Pressed) {
if (report.data[3] != 0x04) {
report.data[3] = 0x04 /* a */ ;
keyboard.send(&report);
}
} else {
if (report.data[3] != 0x00) {
report.data[3] = 0x00;
keyboard.send(&report);
}
}
wait_ms(10);
}
} 基本的にはこういう形です。USBKeyboard のメソッドではキーの「押しっぱなし」ができない実装なので、ちゃんとしたキーボードにするにはレポートを自分で管理する必要があります。実用的にはリポート内のキーの状態を管理するクラスが必要になるでしょう。
といっても、USBKeyboard をほとんど使わないようなら直接 USBHID を継承して MyUSBKeyboard を作ったほうがいい気がするので (レポート定義もいじれるようになるし)、実際はそうしています。
ハマることはないと書いたが、メインループで sleep() (Active Sleeep) を使ってデバイスを割り込み待ちにするコードを書いたところ、USBHID#send() が失敗するという状態になった。どうやらUSBの状態まで狂わすようだったのでビジーループに変えた。
なんでおかしくなるのか、実装を読んだりマニュアルを読んだりして調べてみてもよくわからない。USB のクロックは有効だし、USB まわりの電源も sleep で切れるようなものはないように思える。
内部プルアップ時の電流がスペックだと 50μA となっている。電源電圧 3.3V なら 66kΩ 相当のプルアップとなる。
I2C を 2kΩでプルアップ動かしていると、この内部プルアップのピンをかなり動かしてしまうようだった。とりあえずは大丈夫そうだったが、今後誤動作の原因となりそうだったので、こちらも同様に 2kΩ で外部プルアップとした。
キーボード左右+USBコントローラという構成になった。USBコントローラとキーボードの左右はLANケーブルで接続する。BLE 版だとコントローラー基板はキーボードの左に付属していて、左右のキーボード同士をLANケーブルで接続していたが、実際はこのように配線を変えても動くような構成にしていたので、回路自体はすんなり変更できた。
電池がなくなったり、コントローラ基板を別にしたことで、キーボード全体の座高を低く、かつフラットにすることができた。今まで地味にキー位置が高くて、キーボード前にクッションが必要だったんだけど、その必要がなくなった。
3Dプリンタを得たのでより剛性の高い筐体になった。
NKRO (N-key Rollover) というのは、おおざっぱに言うとキーボードのキーを同時押ししたとき、いくつ認識するか?ということです。
NKRO の理想的には以下の挙動になります。
これを NKRO として定義すると、全ての USB HID キーボードは NKRO ではありません。なぜなら、USB HID キーボードは修飾キーを除くと6キーまでしか押されていることを送信できないからです。
そうすると妥協した次点として以下のような仕様を NKRO とするほかありません。
完全な同時押しは6キーまでですが、入力をとりこぼすということはありません。USB HID で NKRO と呼ばれているものはおそらくこの挙動をすると思います。
まぁ実際のユースケースとして、そもそも7キー以上の同時押しは極めて稀です。ということでさらに妥協して以下のような実装も考えられます
これはこれでほとんど問題ないでしょう。単純に 6KRO になります。押した順番という状態を持つ必要が減るのでファームウェアの実装は簡単になります (バグを少なくできます)。一方で、キーを離さないクセを持つ人がものすごい高速タイピングをした場合にはキーをとりこぼすかもしれません。
PS-2 キーボードのプロトコルは単純で、シリアル通信で
というものです。つまり NKRO になるかはOS側の実装次第です。
ゲーマー向けのマザーボードには PS-2 端子がついていることが多いですが、これは
であると思われます。USB はホストからのポーリングで成りたっているため、キーを押した瞬間にコンピュータにデータが送られてくるわけではなく、コンピュータ側からのポーリングを待つ必要があります。ポーリング間隔はデバイス側から通知され、最小で1msまで設定可能ですが、OS 側に最終決定権があり、例えば Windows では最小で 8ms (Low Speed) , 1ms (Full Speed), 0.125ms (High Speed) です。つまりこの時点で最大で0.125msの遅延が発生します。
PS-2 の場合、キーが押された瞬間に Make 信号を送るため、理論的にはこちらのほうが早いことになります。
(画像は過去に入力したデータを全て Google Fit へ入力しなおした様子)
単純にグローバルな「体重」に対して値を追加するみたいになっているわけではない。
各アプリケーション(サードパーティ含む)は自分用の「データソース」を作る。これはセンサーに対応する。例えば「体重計 HOGE-001」みたいなデータソース。このとき「体重 (com.google.weight)」とかデータの種類と「浮動小数点」とかデータ型の定義をしておく。
データソースの定義に従って、データソースにデータポイントを追加してく。例えば「体重は 69.95kg」みたいな。
こうしていくと、複数のデータソースから「体重」データがいくつもできることになる。
実は Google Fit の画面から体重を入力すると user_input というデフォルトで存在するデータソースにそのデータは蓄積される。一方で、自分で独自の「体重」のデータソースを作って追記することもできる。これによって、データソースごとに自分のデータにだけ責任を持つという形にすることができる。
これらの「体重」のデータは最終的に derived:com.google.weight:com.google.android.gms:merge_weight というデータソースに集計されて、Fit で表示されている。
あとアクティビティ(ランニング)に対応するセッションとかがあるけど、今回は使ってないので調べてない。
なんらかの方法で体重情報を取得できるとして、それを Google Fit に保存したい場合を想定する。
全体の流れは以下の通り
Perl での例をしめす。CLI アプリケーションとしての実装。oob でキーを入力するため初回のみインタラクティブな インターフェイスになっている。
use v5.12;
use LWP::Authen::OAuth2;
use Path::Class;
use JSON;
use HTTP::Request::Common qw(GET HEAD POST DELETE PATCH);
use DateTime;
use constant {
CLIENT_ID => '',
CLIENT_SECRET => '',
};
my $token_file = file('.token_file');
my $token_string = eval { $token_file->slurp } || '';
my $google = LWP::Authen::OAuth2->new(
client_id => CLIENT_ID(),
client_secret => CLIENT_SECRET,
service_provider => "Google",
redirect_uri => "urn:ietf:wg:oauth:2.0:oob",
save_tokens => sub {
my ($token_string) = @_;
my $fh = $token_file->openw;
print $fh $token_string;
close $fh;
},
save_tokens_args => [],
token_string => $token_string,
);
unless ($google->token_string) {
# 新規 OAuth 認証
my $uri = $google->authorization_url(scope => join(' ',
'https://www.googleapis.com/auth/fitness.body.read',
'https://www.googleapis.com/auth/fitness.body.write',
));
printf "Access to authorization: %s\n", $uri;
printf "Input authorization code: ";
my $code = <>;
chomp $code;
$google->request_tokens(code => $code);
}
# データソース作成
# 既にある場合は 409 になる
my $res = $google->request(POST "https://www.googleapis.com/fitness/v1/users/me/dataSources",
Content_Type => "application/json;encoding=utf-8",
Content => encode_json({
"application" => {
"name" => "foobar baz",
"detailsUrl" => "http://example.com",
"version" => "1",
},
"dataType" => {
"name" => "com.google.weight",
"field" => [
{
"name" => "weight",
"format" => "floatPoint"
}
]
},
"dataStreamName" => "foobar",
"type" => "raw",
"device" => {
"manufacturer" => "my",
"model" => "foobar",
"type" => "scale",
"uid" => "1000001",
"version" => "1.0"
}
})
);
# 409 の場合エラーメッセージをパースしてデータソースIDを取得している
my $datasourceid = undef;
if ($res->code == 409) {
my $json = decode_json($res->decoded_content);
($datasourceid) = ($json->{error}->{message} =~ /Data Source: ([^ ]+) already exists/);
} elsif ($res->code == 200) {
my $json = decode_json($res->decoded_content);
$datasourceid = $json->{dataStreamId};
} else {
die "failed to request creating data source";
}
unless ($datasourceid) {
die "cannnot retrieve or create datasource";
}
# 送信するデータポイント
my $data_points = [
{ epoch => ..., weight => 69.4 },
{ epoch => ..., weight => 69.4 },
{ epoch => ..., weight => 69.4 },
];
my $minstarttime = min map { $_->{epoch} } @$data_points;
my $maxendtime = max map { $_->{epoch} } @$data_points;
# 追加するリクエストは PATCH
# https://developers.google.com/fit/rest/v1/reference/users/dataSources/datasets/patch
my $datasetid = sprintf("%s-%s", $minstarttime, $maxendtime);
my $res = $google->request(PATCH sprintf("https://www.googleapis.com/fitness/v1/users/me/dataSources/%s/datasets/%s", $datasourceid, $datasetid),
Content_Type => 'application/json;encoding=utf-8',
Content => encode_json({
"dataSourceId" => $datasourceid,
"minStartTimeNs" => $minstarttime * 1000 * 1000 * 1000,
"maxEndTimeNs" => $maxendtime * 1000 * 1000 * 1000,
"point" => [
map {
{
"dataTypeName" => "com.google.weight",
"originDataSourceId" => "",
"startTimeNanos" => $_->{epoch} * 1000 * 1000 * 1000,
"endTimeNanos" => $_->{epoch} * 1000 * 1000 * 1000,
"value" => [
{
"fpVal" => $_->{weight},
}
]
}
} @$data_points
]
})
);
say $res->as_string; 既存のデータポイントが残っていると削除できないため、以下の手順を踏む
ただ、データポイントを削除しても deletedDataPoint に入るだけで、完全に消えるわけではない。データソースも、削除は通っても、再度作成を行うと、deletedDataPoint が含まれた古いデータが復活する。ここらへんの挙動はよくわからない。
コード例は以下の通り
my $page_token = "";
my $minstarttime = "inf";
my $maxendtime = 0;
# データポイントを走査してデータ範囲を確定させる
while (1) {
infof("GET dataPointChanges with token %s", $page_token);
my $res = $google->request(GET sprintf("https://www.googleapis.com/fitness/v1/users/me/dataSources/%s/dataPointChanges?%s", $datasourceid, $page_token));
$res->code == 200 or die "failed to get dataPointChanges";
my $json = decode_json($res->decoded_content);
use Data::Dumper;
warn Dumper $json ;
@{ $json->{insertedDataPoint} } or last;
$minstarttime = min $minstarttime, map {
$_->{startTimeNanos}
} @{ $json->{insertedDataPoint} };
$maxendtime = max $maxendtime, map {
$_->{endTimeNanos}
} @{ $json->{insertedDataPoint} };
$page_token = "pageToken=" . $json->{nextPageToken};
}
# 全範囲のデータポイントを削除する
if ($maxendtime) {
my $datasetid = sprintf("%s-%s", $minstarttime, $maxendtime);
infof("Deleting existing data points for this data source %s", $datasetid);
my $res = $google->request(DELETE sprintf("https://www.googleapis.com/fitness/v1/users/me/dataSources/%s/datasets/%s", $datasourceid, $datasetid));
say $res->as_string;
} else {
infof("There are no data point");
}
# データソースを削除する
infof("Deleting this datasource");
my $res = $google->request(DELETE sprintf("https://www.googleapis.com/fitness/v1/users/me/dataSources/%s", $datasourceid));
say $res->as_string; 突然話は変わるがOMRON の Wi-FI 体重計を買ったのは失敗だなーと思っている。Bluetooth 体重計のほうがハックしやすいと思うからだ。Wi-Fi 経由で https でサービスと接続されているとサービス側の仕様変更やサービス終了の影響をうけてしまう。そして実際、オムロンはPC側のサービスを終了してしまった。
しかし BT 対応の体重計を買いなおすのも嫌なので、Android アプリが取得しているデータを普通にスクリプト (Perl) で取得できるようにして、Fit にインポートできるようにした。毎日動かせば常に Google Fit 側へデータが同期されるので、たとえ OMRON のサービスが終了しても、最悪データは失われない。リバースエンジニアリングしたので同期スクリプトの公開は控えるが、Google Fit のノウハウだけ記録しておく次第 (BT 体重計から Fit へ同期するアプリケーションなんかを書くときに役立つはずだ)。
Google はウェブの会社で、ユーザーデータの重要性はよくよく理解していると思われるので、サービス終了の際にエクスポートをちゃんと提供することが期待できる。一方でオムロンにそれは期待することはできない。PC版の閲覧サービス終了させてきたしね。
自作キーボードのコネクタとして 8P8C を使っていて、市販のLANケーブルを流用しているのだけれど、特定のLANケーブルで動作せず悩んだ。
結局は表題の通り、8本のうち4本しか結線されていないLANケーブルだったのが原因だった。
こういうケーブルは「カテゴリー5相当」と言う類のものらしい。
カテゴリー6の時代に今更こんなケーブルは売ってないと思うが、古いLANケーブルには注意しましょう。普通にイーサネット的にはリンクアップはするので罠いです。
コネクタ流用でこんな罠があるとは思わなかった。
どんな分野でもまず「温度感」をつかむのが大事だと思っていて、そもそも「温度感」って何かというと、どの部分の神経を使うか、熱量を使って考えて、どこの手を抜くかという話です。「コツ」を少し具体的に表現して「温度感」といっている、というとちょっと身も蓋もないのですが。
ここでいう 3D プリンタは普及している FDM (Fused Deposition Modeling・熱溶解積層法) の話です。FDM は基本的に X/Y 精度が悪く、Z精度はそれに比べると高いという特徴があります。
具体的には X/Y については±0.5mmぐらいで、Zは±0.1mmで考えます。特に X/Y の精度の悪さは設計難易度にかかってきます。同一条件で出力したパーツを組合せる分にはともかく、他の規格品、例えばナットや、あるいは三脚ネジのようなものを出したいときは、だいたいの場合は何度かプリントして現物あわせをする必要があるのです。
±0.5mm これはキャリブレーションしてフィラメント量を調節していてもなお生じる誤差と考えたほうが良くて、キャリブレーションしていなければもっと精度が出ません。
基本的に動く面すべてに 0.5mm のトレランスをもうけます。つまり引き出しみたいなものなら、1mm 小さい立体を入れるようにします。
パズルのピースのようにハメこむ場合は、0.25mmのトレランスをもうけます。四角に四角をハメる場合 0.5mm 小さい立体になります。しかし実際には素直にうまくいくことは少ないでしょう。絶対に失敗しない方法は 0.2mm 程度のトレランスで、出力してから削ることです。
圧入して固定するような場合、これはかなり難しいですが、形状で工夫すると楽になります。設計上は、内接多角形の柱と同一内径の穴をモデリングして無理矢理ハメこむのが楽です。
しばらくすると忘れてしまうので、現状のフローをまとめておく。
0.25mm 幅で削るので、これが限界。0.05mm でもトレランスをとれれば安定しやすい。
- 切削する銅箔層 (F.Cu or B.Cu) と Edge.Cut をガーバーに出力する
- ドリルファイルを出力する
以下のような前提で gcode 化する
先端0.1mmのエンドミルといっても、実際買ってみると綺麗に 0.1mm になっていることはすくない。というかすぐ先端が折れる。計算するときは 0.15〜0.20mm として扱う。
切削速度を落とせばφ0.8mmで穴開け〜外形カットまで可能で、ツール取り替えの手間が減らせるはずだけど、0.8mm のミルは高価であんまり折りたくないので、別途φ1.5mmのミルを使っている。これは今後 0.8mm にするかも。あんまり太いミルで外形カットすると切削抵抗が大きくなり、両面テープが剥れてずれる可能性もあるので注意。
# Use standard mm metric=true metricoutput=1 front=tachometer-B_Cu.gbr #back= outline=tachometer-Edge_Cuts.gbr drill=tachometer.drl front-output=front.gcode back-output=back.gcode outline-output=outline.gcode drill-output=drill.gcode milldrill-output=milldrill.gcode # https://github.com/chrysn-pull-requests/pcb2gcode/blob/graphical-documentation/man/options.svg zwork=-0.1 zsafe=1 mill-feed=220 mill-speed=10000 mill-vertfeed=150 mill-diameters=0.22 milling-overlap=25% isolation-width=0.5 zdrill=-1.7 zmilldrill=-1.7 zchange=20 drill-feed=100 drill-speed=10000 min-milldrill-hole-diameter=0 milldrill-diameter=0.8 # 外形カット時のミル直径 cutter-diameter=1 # PCB板厚+0.1mm zcut=-1.7 cut-feed=500 cut-speed=10000 cut-infeed=1 cut-side=front # mirror-axis=51.575mm optimise=true zero-start=true
pcb2gcode 2.0.0 で動くように変更
pcb2gcode して生成 gcode をデスクトップとリモートの Machinekit に転送する。デスクトップにコピーしているのは CAMotics で読みこませて確認するときに便利なので。
~/ghq/github.com/pcb2gcode/pcb2gcode/pcb2gcode && cp *.gcode ~/Desktop/ && scp *.gcode machinekit@192.168.0.240:gcode
drill や外形カットなどは面倒なので AutoLeveller はかけず、0.1mm 余計に掘る。
https://lowreal.net/2016/10/19/1
AutoLeveller の起動
java -cp ~/ghq/bitbucket.org/daedelus1982/autoleveller/out/artifacts/autoleveller_jar/autoleveller.jar com.cncsoftwaretools.autoleveller.Autoleveller
生成 gcode の転送
scp ~/Desktop/*.ngc machinekit@192.168.0.240:gcode
プローブログを手元に転送
scp machinekit@192.168.0.240:gcode/RawProbeLog.txt ~/Desktop/
基板パターン
ドリル
外形
ある程度規模が大きくなると machinekit に読みこませたときに非常に時間がかかってしまうので、その時間を使うぐらいなら AutoLeveller を使ったほうがよいと考える。時間がかかるのはプレビューのために gcode を一通り仮想的に実行しているためだと思う。
また、--al のオプションは Probe → 切削 がひとつの gcode で行われるため、リトライすることができない。途中でミルが折れたりすると交換してやりなおすということができなくなる。
基板切削をはじめた直後に削った銅がけばだつようだったらミル先端が既に折れている可能性がある。そのまますすめてもうまくいかないので新しいのに変えること
または確認しても折れていないようなら、ミルの突き出し量を見直すこと。ぎりぎりまでマシンに指しこんでいないと、同様にけばだつことがある。
秋月でLPC11U35 が乗っているボードを買った。うっかり3枚買っていた。EA LPC11U35 QuickStart Board
と互換のもの。
RAM や Flash が少なめだけど以下の点で魅力的なボード
コード的な備考を先に書いておく
Lチカのコードは何の変哲もない。
#include "mbed.h"
DigitalOut led(LED1);
// Serial serial(UART_TX, UART_RX);
int main() {
for (;;) {
led = 1;
wait(0.5);
led = 0;
wait(0.5);
}
} platformio でビルドしたのを書きこんでもさっぱり動かず、再度 CRP DISABLED というボリュームがマウントされてしまう。オンラインコンパイラでは動くので、手元の環境の問題であることはわかったが、なかなか原因がわからなかった。
結局 platformio は mbed OS 5 の環境でビルドしようとするが、LPC11U35 では RAM が足りず起動できないようだ。
実際、mbed の公式を見て mbed OS 5 系に対応するボードをリストにすると (そういうことができることにはじめて気付いたが)、LPC11U35 は出てこない。
platformio でフレームワークのバージョンを指定してビルドする方法がどう調べてもわからなかった。
そういうことで、いろいろ試したけど、あきらめて platformio のことは忘れましょう。
ARM 公式で提供されている mbed-cli をいれるのが今のところは最良のようです。
まず Python のツールなので python が必要なのと、pip も必要です。ここでは
という感じでいきます。
curl https://bootstrap.pypa.io/get-pip.py | sudo python sudo pip install mbed-cli IntelHex
mbed-cli には GCC_ARM のツールチェーンを入れてくれる機能はなく、前もって arm-none-eabi-gcc とかを入れておく必要がある。https://launchpad.net/gcc-arm-embedded とかを入れてパスを通しておく。
もしくは、platformio で既に入れてあるのがある場合は以下のようにしてパスを通せば使える。
export PATH=$HOME/.platformio/packages/toolchain-gccarmnoneeabi/bin:$PATH
今回使う LPC11U35 は mbed OS 5 未対応のため、mbed 2 (いろいろ別名がある。mbed library / mbed classic など) を使うようにプロジェクトを作る。--mbedlib を指定すると mbed 2 環境になる。
sketch はプロジェクト名。
mbed new sketch --mbedlib cd sketch vim main.cpp mbed deploy pip install --upgrade --ignore-installed --user -r .temp/tools/requirements.txt mbed compile -t GCC_ARM -m LPC11U35_401 # LPC11U35 を USB かきこみモードにして cp ./BUILD/LPC11U35_401/GCC_ARM/sketch2.bin /Volumes/CRP\ DISABLD/firmware.bin #リセットボタンで動く
しかし mbed-cli は凶悪で、プロジェクトディレクトリ以下に 2GB ぐらいファイルをコピーしてくる (mbed 2 の全ソースコード)。
自分は複数端末 (ノートとデスクトップ) で開発する関係で Dropbox のディレクトリで同期させておくことが多いのだが、これだと死ねる。つらい。
回路図
見比べると結構違う。
~/.platformio/packages/framework-mbed/targets/TARGET_NXP/TARGET_LPC11UXX/TARGET_LPC11U35_401 以下にある。
LM1972 デジタルボリューム | tech - 氾濫原 の続きで、Arduino ではなくmbed環境での実装。
LPC11U35 で試した。AD1 にBカーブボリュームをつけてこれによって 78dBから0dbまでアッテネーションレベルをかえる。
#include "mbed.h"
// mosi miso sck
//SPI spi(P0_9, P0_8, P0_10);
//DigitalOut cs(P0_11);
class LM1972 {
SPI spi;
DigitalOut cs;
const uint8_t daisy_chain;
public:
static const uint8_t MUTE = 0xff;
// uint16_t to 1dB step attenuation value
static inline uint8_t volumeToAttenuation(const uint16_t v) {
// volume 0% -> 78dB (126)
// volume 100% -> 0dB (0)
const uint8_t dB = ((uint32_t)(1023 - v) * 78 / 1023);
uint8_t att = 0;
if (dB < 48) {
att = 2 * dB;
} else {
att = 96 + dB - 48;
}
return att;
}
static inline uint8_t volumeToAttenuation(const float v) {
// volume 0% -> 78dB (126)
// volume 100% -> 0dB (0)
const uint8_t dB = 78 * v;
uint8_t att = 0;
if (dB < 48) {
att = 2 * dB;
} else {
att = 96 + dB - 48;
}
return att;
}
LM1972(
PinName _mosi,
PinName _miso,
PinName _sck,
PinName _cs,
uint8_t _daisy_chain = 1
) :
spi(_mosi, _miso, _sck),
cs(_cs),
daisy_chain(_daisy_chain)
{
spi.format(8, 0);
spi.frequency(1e6);
cs = 1;
}
inline void setAttenuation(const uint8_t channel, const uint8_t v) {
cs = 0;
// >150ns
wait_us(2);
spi.write(channel);
spi.write(v);
// >150ns
wait_us(2);
cs = 1;
}
uint8_t setVolume(const float v) {
const uint8_t att = volumeToAttenuation(v);
for (int i = 0; i < daisy_chain; i++) {
setAttenuation(0, att);
setAttenuation(1, att);
}
return att;
}
uint8_t setMute() {
for (int i = 0; i < daisy_chain; i++) {
setAttenuation(0, MUTE);
setAttenuation(1, MUTE);
}
return MUTE;
}
};
DigitalOut led(LED1);
Serial serial(UART_TX, UART_RX);
AnalogIn pot(P0_12); // AD1
LM1972 volume(P0_9, P0_8, P0_10, P0_11);
int main() {
serial.baud(9600);
serial.printf("init\r\n");
volume.setMute();
for (;;) {
led = 1;
wait(0.5);
led = 0;
wait(0.5);
float val = pot.read();
serial.printf("pot val = %d\r\n", (int)(val * 100));
uint8_t att = volume.setVolume(val);
serial.printf("set att = %x\r\n", att);
}
}