2016年2月15日月曜日

TWE-Liteの自転車ビーコンにオートペアリングを実装

 自転車ビーコンにオートペアリングを実装してみた。

 自転車ビーコンは、ペアリングした送信機と受信機の間でのみ通信が成立する。今までのペアリングは、設定モードに入って手でIDを入力して行っていた。入力を注意深く行うのは面倒だし、動作確認するとはいえ、この方法ではいずれ間違いが発生するのは避けられない。

 オートペアリングでは、コールドスタート時に特定のボタンが押されていたらペアリングモードに入り、ペアリング用のパケットで双方が希望するAppIdとランダムに選んだチャンネルを送り合う。暗黙のルールの下にAppIdを決定し、従属的にチャンネルも決定する。ペアリングのパラメータが決まったら、新しいチャンネルで通信を確認し、一定数のパケットが交換できたらEEPROMに設定を保存する。
 オートペアリングで使うAppIdとチャンネルは固定だが、送信出力を最小にする事と、ペアリング用パケットに双方のシリアル番号を含めることによって、確実に1対1でペアリングする。同時に3台以上がペアリングモードに入ったとしても、ペアリングが成功するのは必ず1対1の組み合わせに限られる(または、ペアリングが失敗する)。

 今のところ、ペアリングのパラメータはAppIdとチャンネルだけ。他のパラメータを含めることも可能だが、自転車ビーコンでは、これ以上は必要無い。

ペアリングモードへの入り口は、cbAppColdStart()から呼ばれるvInitHardware()の中。cold startの時にGPIOの状態からモードを設定する箇所に、DI1によるペアリングモード判定を入れている。
if (bPortRead(PORT_INPUT1)) {
sAppData.bPairingMode = TRUE;
}
DI1は押しボタンスイッチなので、M1,M2,M3のようなプルアップ解除は不要。cbAppColdStart()では、オートペアリング用に状態マシンとAppContextを設定。
// 状態遷移マシンの登録
if (sAppData.bPairingMode) {
ToCoNet_Event_Register_State_Machine(vProcessEvCorePairing);
sAppData.prPrsEv = (void*) vProcessEvCorePairing;
sToCoNet_AppContext.bRxOnIdle = TRUE;
sToCoNet_AppContext.u8TxPower = 0; // 最小出力
sToCoNet_AppContext.u32AppId = APP_ID;
sAppData.u8AppIdentifier = u8CCITT8(
(uint8*) &sToCoNet_AppContext.u32AppId, 4);
sToCoNet_AppContext.u8Channel = CHANNEL; // pairing用に固定
sToCoNet_AppContext.u32ChMask = CHMASK;
}
状態マシンのスタートアップでペアリング用のパラメータを初期化。
static void vProcessEvCorePairing(tsEvent *pEv, teEvent eEvent, uint32 u32evarg) {
switch (pEv->eState) {
case E_STATE_IDLE:
if (eEvent == E_EVENT_START_UP) {
sAppData.u16CtRndCt = 0;
sAppData.u32ReqAppId = ToCoNet_u32GetSerial();  // 要求APP ID
sAppData.u8ReqCh = ((ToCoNet_u16GetRand() & 0xF) + 11); // 要求channel
sAppData.u32CandidateAppId = 0;
sAppData.u32AnotherAppId = 0;
sAppData.u8CandidateCh = 0;
sAppData.u16MatchCount = 0;
sAppData.u16PeerMatched = 0;
}
相手をスキャンして、見つかったら合意形成フェーズへ移行。
case E_STATE_APP_PAIR_SCAN:
// ペアリング相手が現れたら提案確認フェーズ
if (0 < sAppData.u16MatchCount) {
vfPrintf(&sSerStream, "!INF PEER EXIST.@%dms"LB, u32TickCount_ms);
ToCoNet_Event_SetState(pEv, E_STATE_APP_PAIR_PROPOSE);
}
合意形成フェーズで1秒待ち、この間に相手からの合意パケットが十分な量受信できていたら、確認フェーズへ移行。
case E_STATE_APP_PAIR_PROPOSE:
if (1000 <= PRSEV_u32TickFrNewState(pEv)) {
// 1秒待ってから判断
sToCoNet_AppContext.bRxOnIdle = FALSE;
ToCoNet_vRfConfig(); // 受信を一旦停止
if (AUTO_PAIR_COUNT_MIN <= sAppData.u16MatchCount
&& AUTO_PAIR_COUNT_MIN <= sAppData.u16PeerMatched) {
ToCoNet_Event_SetState(pEv, E_STATE_APP_PAIR_CONFIRM);
} else {
ToCoNet_Event_SetState(pEv, E_STATE_APP_PAIR_FAILED);
}
}
break;
確認フェーズでは、最初にRF設定を変更。
case E_STATE_APP_PAIR_CONFIRM:
if (eEvent == E_EVENT_NEW_STATE) {
// カウンタを一旦クリア
sAppData.u16MatchCount = 0;
sAppData.u16PeerMatched = 0;
// u32AppIdはcbAppColdStart以外で変更不可なのでu8AppIdentifierだけを変更
sAppData.u8AppIdentifier = u8CCITT8(
(uint8*) &sAppData.u32CandidateAppId, 4);
sToCoNet_AppContext.u8Channel = sAppData.u8CandidateCh;
sToCoNet_AppContext.u32ChMask = (1UL << sAppData.u8CandidateCh);
// 次のパケットのタイミングを仕込む
sAppData.u16CtRndCt = (ToCoNet_u16GetRand() & 0x3);
sToCoNet_AppContext.bRxOnIdle = TRUE;
ToCoNet_vRfConfig(); // 新たなRF設定に切り替える
}
確認フェーズでも、十分な合意パケットが受信できたら、完了フェーズへ移行。
if (1000 <= PRSEV_u32TickFrNewState(pEv)) {
// 1秒待ってから判断
if (AUTO_PAIR_COUNT_MIN <= sAppData.u16MatchCount
&& AUTO_PAIR_COUNT_MIN <= sAppData.u16PeerMatched) {
ToCoNet_Event_SetState(pEv, E_STATE_APP_PAIR_COMPLETE);
} else {
ToCoNet_Event_SetState(pEv, E_STATE_APP_PAIR_FAILED);
}
}
break;
完了フェーズでは、新しいAppIdとチャンネルに更新してEEPROMへ保存。
case E_STATE_APP_PAIR_COMPLETE:
// AppId,Chを書き換えて保存
{
tsFlash sFlash = sAppData.sFlash;
sFlash.sData.u32appid = sAppData.u32CandidateAppId;
sFlash.sData.u8ch = sAppData.u8CandidateCh;
sFlash.sData.u32chmask = (1UL << sAppData.u8CandidateCh);
sFlash.sData.u32appkey = APP_ID;
sFlash.sData.u32ver = VERSION_U32;
bool_t bRet = bFlash_Write(&sFlash, FLASH_SECTOR_NUMBER - 1, 0);
V_PRINT("!INF FlashWrite %s"LB, bRet ? "Success" : "Failed");
vWait(100000);
}
ペアリングモードでやり取りするパケットは、受信コールバックcbToCoNet_vRxEvent()の中でデコード関数に振り分け。
case TOCONET_PACKET_CMD_APP_USER_PAIRING: // auto pairing
if (PRSEV_eGetStateH(sAppData.u8Hnd_vProcessEvCore) == E_STATE_RUNNING) {
vReceivePairingData(psRx);
}
break;
デコード関数の中では、AppIdの大小で優先度を決定。確定したAppIdが送られてきたら、受信数をカウントする。受信数は状態遷移の判断材料として使用する。
// 要求AppId
uint32 u32ReqAppId = G_BE_DWORD();
// 要求ch
uint8 u8ReqCh = G_OCTET();
(void)u8ReqCh;
// 候補AppId
uint32 u32AcceptAppId = G_BE_DWORD();
// 対向AppId
uint32 u32AnotherAppId = G_BE_DWORD();
// 候補ch
uint8 u8AcceptCh = G_OCTET();
(void)u8AcceptCh;
sAppData.u16PeerMatched = G_BE_WORD(); // 相手方pairingマッチカウンタ
if (sAppData.u32CandidateAppId == 0) {
if (u32ReqAppId < sAppData.u32ReqAppId) {
sAppData.u32CandidateAppId = u32ReqAppId;
sAppData.u32AnotherAppId = sAppData.u32ReqAppId;
sAppData.u8CandidateCh = u8ReqCh;
} else if (sAppData.u32ReqAppId < u32ReqAppId) {
sAppData.u32CandidateAppId = sAppData.u32ReqAppId;
sAppData.u32AnotherAppId = u32ReqAppId;
sAppData.u8CandidateCh = sAppData.u8ReqCh;
} else {
// bad case
}
} else if (sAppData.u32CandidateAppId == u32AcceptAppId
&& sAppData.u32AnotherAppId == u32AnotherAppId) {
sAppData.u16MatchCount++;
} else {
// ignore u32AcceptAppId == 0 or bad case(maybe multiple peer).
}
その他、詳細はgithubのソースコードを参照。

2016年2月7日日曜日

自転車発見器(自転車ビーコン)を出品

ヤフオクにて、「自転車発見器(自転車ビーコン)」として出品しました。
開始価格ではやや赤字なのですが、1世代前の試作機なのでサービス価格。

改めて実測してみると、100m届くケースは滅多に無いとわかったので、スペックはMFT2015でチラシに書いたものより控えめに書いています。

さて、売れるかなぁ?