2024年5月31日金曜日

AirTag互換のスマートタグを買ってみた

 iPhoneで追加のアプリなしに使えるAirTag互換のスマートトラッカーが結構出ていたので試しにポチってみた。本家AirTagみたいにUWBに対応していない分安価だけど、あまり精度は良くないらしい。

Amazonでは中華のスマートタグが結構な種類売っていて、位置情報がわかるやつはiPhone専用で"Apple Find My"に対応しているやつと、iSearchingというアプリに対応していて、AndroidとiPhone両方使えるものがあるっぽい。位置情報がわからないタイプってのも売っていて、これは単純にペアリングしたiPhoneやAndroidと接続が切れると通知が来るタイプ。(つまり落としたりなくしたりした瞬間しかわからない)

iPhoneユーザーであれば"Apple Find My"対応デバイスのほうがiPhone率が高い日本だと優位かもしれない。

ということで試しにApple Find My対応のスマートタグ3種類をポチってみた。
左から、Finein TagAIYATO-L1(2.0.1)AIYATO-L1(2.0.0)っぽい。

AIYATO-L1は同じ名前だけど電池交換が簡単にできるタイプは少し厚みがあるし、マイナスドライバーで開けないと電池交換できないタイプはちょっと薄い感じ。起動音はどっちも同じ。
Finein Tagは起動音が少しかっこいいかも。質感もAIYATO-L1シリーズよりはかっこいいと思う。

三種類ともiPhoneの探すアプリから簡単に登録できた。

ボタンを押すと起動するので、探すアプリを開いて右下の+ボタンを押して「その他の持ち物を追加」をタップした状態でスマートトラッカーのボタンを押して起動すると簡単に登録できる。登録後は持ち物を探すタブで探すことができる。電源を切るときはボタンを長押しすると電源が切れるっぽい。(ブザー音が出るまで長押し)

ちなみにこのスマートトラッカーを他の人に上げたり、別なアカウントで登録する場合は「持ち物を探す」タブから削除したいデバイスをタップして下の方に行くと登録解除することができるっぽい。このメニューから音を鳴らして探すことも可能。

AIYATO-L1

違い的にはどれもビープ音もなるし、ボタンが付いていてオンオフやペアリングが行える。多分サイズや電池の交換のしやすさが違うだけだと思うんだけど、分解して違いを調べてみることに。

AYANEO-L1シリーズはどちらもRM1200というICが使われていた。音はどっちも同じなんだけど、電池が簡単に外せる四角いタイプは少し音が小さい。スピーカーも少し小さかった。Finein TagのほうはチップがST17H65Bという別なチップが使われていた。RM1200搭載モデルよりもICの数が多いので少し高級な作りかも。

AirTagとは違ってUWBに対応していないので位置精度は少し大まかな感じ。特に近くにiPhoneユーザーがいないようなところでは更新頻度も少なくなってしまいそう。
ちなみに感度的にはFinein Tagのほうが良さそうな気がする。AIYATO-L1(2.0.1)と同じところに一緒に放置してみたんだけど最初に反応したのがFinein Tagで、その後しばらくAIYATO-L1が反応していなかったのでもしかしたらチップの違いによる感度の違いはあるのかもしれない。そんでもって位置精度的には近くにいたiPhoneユーザーの位置が表示されるのか、Bluetoothの電波が届く範囲での誤差がありそう。Finein Tagが反応した距離でも10mぐらいの誤差があった。ちなみに更新間隔は6分ぐらいと結構いい感じに更新されている。

次回はブザー音をならないように改造してみようとおもう。
チャリンコの盗難対策とかでブザー外したのを取り付けたとしてもiPhoneユーザーとかだったら画面に通知されてしまうけどね。(ストーカーによる悪用防止のためかな)

2024年5月26日日曜日

Xbox Oneコントローラのスティックを交換してみた。

 Bluetooth対応のXbox Oneコントローラを買ってから7年ぐらい使ってるんだけど、スティックから黒いカスが出るようになってきてしまったのでスティックを交換してみた。スティックの交換というと可変抵抗がヘタってドリフトが起きて交換するパターンが多いと思うんだけどまさかのゴムのほうが最初にだめになってしまった。

左スティックから黒いカスが出るようになってしまった。スティック自体はどっちも結構減ってる感じはある。
Aliexpressでスティックを買ってみたんだけど、2つで230円だとクオリティーはこんな感じ。
金型がヘタってきているのかそんなにきれいな感じではないな…
でもまぁ使うたびに黒いカスがつくよりはマシかなと思ってとりあえず交換してみることに。
コントローラを開けるためにグリップの部品を取り外す必要があるんだけど結構固くて大変だった。プラスティックの車用の内装剥がしを使ってみたんだけどかなり硬い。
ネジはトルクスのT8Hドライバーが必要。穴付きのT8ってドライバーセットでも意外と入っていないことが多いかも。
シールの下にも一つねじがあるので中途半端に剥がしながら外すと良いかも。
ネジを外すと表面のカバーが外れるのでスティックを交換することができる。
とりあえずスティックを交換してみたけど、やっぱりもともとついていたスティックと微妙に長さが違う気がする。このお値段でとりあえず使えるようになったのでいいかもしれないが、USB Type Cになった新しいXboxコントローラ買っても良さそうな気がする。

ついでにファームウェアも5.20.7.0にアップデートしておいた。
まぁとりあえずこのスティックでしばらく使ってみようと思う。

2024年5月4日土曜日

ESP32でSLCANプロトコルを使ってみた。

 PythonでCANアダプタを使っていてPCANの受信のレスポンスがちょっと遅い気がしたのでESP32でSLCANデバイスを作って試してみた。

前にESP-WROOM-32でCANを使ってみたときと同じハードで、最新のArduino CoreでCANライブラリを使わずに使えるようにしてみた。

#include "driver/twai.h"

#define SERIAL_BAUD 921600
const gpio_num_t CAN_TX_PIN = GPIO_NUM_32;
const gpio_num_t CAN_RX_PIN = GPIO_NUM_35;

boolean enableSLCAN = false;
boolean timestamp = false;
boolean cr = false;
int can_speed = 250;
static const char hexval[17] = "0123456789ABCDEF";

void slcan_ack() { Serial.write('\r'); }
void slcan_nack() { Serial.write('\a'); }

twai_timing_config_t get_timing(int speed_kbps) {
  switch(speed_kbps) {
    case 100:  return TWAI_TIMING_CONFIG_100KBITS();
    case 125:  return TWAI_TIMING_CONFIG_125KBITS();
    case 250:  return TWAI_TIMING_CONFIG_250KBITS();
    case 500:  return TWAI_TIMING_CONFIG_500KBITS();
    case 800:  return TWAI_TIMING_CONFIG_800KBITS();
    case 1000: return TWAI_TIMING_CONFIG_1MBITS();
    default:   return TWAI_TIMING_CONFIG_250KBITS();
  }
}

void send_canmsg(char *buf, bool rtr, bool ext) {
  if (!enableSLCAN) return;

  twai_message_t tx_frame = {};
  int msg_id = 0, msg_ide = 0, msg_len = 0;

  if (rtr) {
    if (ext) sscanf(&buf[1], "%04x%04x", &msg_ide, &msg_id), tx_frame.flags = TWAI_MSG_FLAG_EXTD | TWAI_MSG_FLAG_RTR;
    else sscanf(&buf[1], "%03x", &msg_id), tx_frame.flags = TWAI_MSG_FLAG_RTR;
  } else {
    if (ext) sscanf(&buf[1], "%04x%04x", &msg_ide, &msg_id), tx_frame.flags = TWAI_MSG_FLAG_EXTD;
    else sscanf(&buf[1], "%03x", &msg_id), tx_frame.flags = 0;
  }

  tx_frame.identifier = ext ? (msg_ide * 65536 + msg_id) : msg_id;

  if (ext) sscanf(&buf[9], "%01x", &msg_len);
  else sscanf(&buf[4], "%01x", &msg_len);

  tx_frame.data_length_code = msg_len;

  for (int i = 0; i < msg_len; i++) {
    int candata;
    if (ext) sscanf(&buf[10 + i*2], "%02x", &candata);
    else sscanf(&buf[5 + i*2], "%02x", &candata);
    tx_frame.data[i] = candata;
  }

  twai_transmit(&tx_frame, pdMS_TO_TICKS(10));
}

void pars_slcancmd(char *buf) {
  switch(buf[0]) {
    case 'O': { // OPEN CAN
      if (!enableSLCAN) {
        twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(CAN_TX_PIN, CAN_RX_PIN, TWAI_MODE_NORMAL);
        twai_timing_config_t t_config = get_timing(can_speed);
        twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();
        if (twai_driver_install(&g_config, &t_config, &f_config) == ESP_OK &&
            twai_start() == ESP_OK) enableSLCAN = true;
      }
      slcan_ack();
    } break;

    case 'C': { // CLOSE CAN
      if (enableSLCAN) {
        twai_stop();
        twai_driver_uninstall();
        enableSLCAN = false;
      }
      slcan_ack();
    } break;

    case 't': send_canmsg(buf, false, false); slcan_ack(); break;
    case 'T': send_canmsg(buf, false, true);  slcan_ack(); break;
    case 'r': send_canmsg(buf, true, false);  slcan_ack(); break;
    case 'R': send_canmsg(buf, true, true);   slcan_ack(); break;

    case 'Z': timestamp = (buf[1] == '1'); slcan_ack(); break;

    case 'S': {
      int new_speed = 0;
      switch(buf[1]) {
        case '3': new_speed = 100; break;
        case '4': new_speed = 125; break;
        case '5': new_speed = 250; break;
        case '6': new_speed = 500; break;
        case '7': new_speed = 800; break;
        case '8': new_speed = 1000; break;
        default: slcan_nack(); return;
      }

      if (enableSLCAN) {
        twai_stop();
        twai_driver_uninstall();
        enableSLCAN = false;
      }

      can_speed = new_speed;

      twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(CAN_TX_PIN, CAN_RX_PIN, TWAI_MODE_NORMAL);
      twai_timing_config_t t_config = get_timing(can_speed);
      twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();

      if (twai_driver_install(&g_config, &t_config, &f_config) == ESP_OK &&
          twai_start() == ESP_OK) {
        enableSLCAN = true;
        slcan_ack();
      } else slcan_nack();
    } break;

    case 'V': Serial.print("V1234"); slcan_ack(); break;
    case 'N': Serial.print("N2208"); slcan_ack(); break;
    case 'F': Serial.print("F00");   slcan_ack(); break;
    default: slcan_nack(); break;
  }
}

void transfer_tty2can() {
  static char cmdbuf[32];
  static int cmdidx = 0;

  while(Serial.available()) {
    char val = Serial.read();
    cmdbuf[cmdidx++] = val;
    if (cmdidx == 32) { slcan_nack(); cmdidx=0; }
    else if (val == '\r') {
      cmdbuf[cmdidx]='\0';
      pars_slcancmd(cmdbuf);
      cmdidx = 0;
    }
  }
}

void transfer_can2tty() {
  if (!enableSLCAN) return;
  twai_message_t rx_frame;
  if (twai_receive(&rx_frame, pdMS_TO_TICKS(3)) == ESP_OK) {
    String command = "";
    if (rx_frame.flags & TWAI_MSG_FLAG_EXTD) {
      command += (rx_frame.flags & TWAI_MSG_FLAG_RTR) ? "R" : "T";
      command += hexval[(rx_frame.identifier >> 28) & 0xF];
      command += hexval[(rx_frame.identifier >> 24) & 0xF];
      command += hexval[(rx_frame.identifier >> 20) & 0xF];
      command += hexval[(rx_frame.identifier >> 16) & 0xF];
      command += hexval[(rx_frame.identifier >> 12) & 0xF];
      command += hexval[(rx_frame.identifier >> 8) & 0xF];
      command += hexval[(rx_frame.identifier >> 4) & 0xF];
      command += hexval[rx_frame.identifier & 0xF];
    } else {
      command += (rx_frame.flags & TWAI_MSG_FLAG_RTR) ? "r" : "t";
      command += hexval[(rx_frame.identifier >> 8) & 0xF];
      command += hexval[(rx_frame.identifier >> 4) & 0xF];
      command += hexval[rx_frame.identifier & 0xF];
    }
    command += hexval[rx_frame.data_length_code & 0xF];
    for (int i = 0; i < rx_frame.data_length_code; i++) {
      command += hexval[(rx_frame.data[i] >> 4) & 0xF];
      command += hexval[rx_frame.data[i] & 0xF];
    }
    if (timestamp) {
      uint16_t t = millis() % 60000;
      command += hexval[(t >> 12) & 0xF];
      command += hexval[(t >> 8) & 0xF];
      command += hexval[(t >> 4) & 0xF];
      command += hexval[t & 0xF];
    }
    command += '\r';
    Serial.print(command);
    if (cr) Serial.println();
  }
}

void setup() {
  Serial.begin(SERIAL_BAUD);
}

void loop() {
  transfer_can2tty();
  transfer_tty2can();
}

ESP32でSLCANを実装していた人がいたのでそのCAN部分をTWAIドライバに変更してみただけ。たしかにシリアル通信なのでBluetooth経由でもCANが使えるようにできるわけか…
Python-Canはソケット通信にも対応してるのでそのままWiFi化とかもできそう。




2024年5月3日金曜日

ESP32でクランプ式電力計のデータを吸い出してみた。

 前に設定を頼まれたWiFiクランプ式電力計なんだけどアプリだと使いづらいということでESP32を使ってデータをクラウドに蓄積できないかという検討をしてみることに。tuyaのクラウドでAPI使おうとすると有料だし…

tinytuyaがESP32で動くと良かったんだけど、暗号化系のライブラリの問題で厳しいようで、ESP8266からtinytuyaみたいにローカルでデータを取得できるようにしている人がいたのでESP32で試してみた。

使用させてもらったのはEspTuya。ほかにCryptoArduinoJsonが必要だけど全部IDEからインストールできた。

このスケッチ、ところどころ不完全なのでそのままではビルドできなかった。

195行目のところで、"while (WiFi.status() != WL_CONNECTED"ってなってるところは次の行に.............ってなってるレベルで不完全なのでこの部分は

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print('.');
  }

のように通常よくあるサンプルみたいに修正。

そんでもって201行目のloctimestamp  = epochTime ....;  ってところも中途半端なのでNTPから時間を取得するように修正。

const long gmtOffset_sec = 9 * 3600;
const int daylightOffset_sec = 0;
const char* ntpServer = "ntp.nict.jp";

configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
loctimestamp = time(NULL);

48行目の#include <ESP8266WiFi.h>はESP32用に#include <WiFi.h>に変更。そうするとFunctions.inoの272行目の

while ( _client.connected() && _client.availableForWrite() < len ) delay(10);

ってところで使われてるavailableForWrite()がESP32だと未実装なのでこの行をまるっと

while ( !_client.connected() ) delay(10);

のように変更。

あとはデバイスに合わせてEspTuya.inoのsDev disj1のところにLocal KeyをAscciからHEXに変換したものを入力するのとIPを修正。ほかのsDevの行はコメントアウト(loopの中のTest device2と3もコメントアウト)

とりあえずこれでWiFiのSSIDとパスワードをPJ-1103Aと同じにしてESP32に書き込んでみたらうまく動いた。getStatus()だとtinytuyaでDPSをプリントするのと同じ感じでプリントされるのであとはDPSとデータの対比表をつかってデータを抽出すればいけそう。

ESP32ならLCDつけて常時モニタリングしてもいいし、ついでにAmbientにアップロードもできたりして便利そう。PJ-1103AとLCD付きのESP32を組み合わせればデマンドとかを確認できるスマートメーターみたいなのを作れそう。

とりあえずデータをパースできるようにちょっと研究中。