4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【スパイの方にオススメ】 ドアの覗き穴を使って、モールス信号で家の鍵を開ける!?

Last updated at Posted at 2025-12-24

1. 大問題!

考えてみてください。
もしあなたがスパイで、敵に捕まったとして、物理鍵やカードキーを取られたら?
指紋を取られたら?変装されたら?
あなたの家の中の秘密、人によっては家族が敵の脅威を受けます🔪

スパイなら鍵は脳内に入れる必要があるのです🧠

じゃあ具体的にどうするのか?

  • 暗号でドアを開ける

が一番現実的で、かつ憧れますよね。

😇~現実~😇
現実という言葉が出てきました。現実はどうでしょう。
昔はSwitchBotでドアを開けられるようにしましたが、スマホを一々取り出さないといけない等、自分にとっては意外とハードルで、結局そんなに利便性を感じず、一度剥がしてしまいました。物理キーを普段使っています。

私は最寄り駅からの1kmを、ディズニーランドのパレードの曲を流して、踊りながら帰ります💃
邪魔なのでスマホもカバンに入れ、鍵もカバンの中にある。ということが多いわけです

その結果、家の前に着いたら止まって一々カバンから鍵を取り出す毎日という訳です😢

これが現実。

それでいいのか?
否!楽しく技術で解決したい!憧れを現実にしたい!スパイになる!😎

2. 行ったスパイ活動がこちら😎

"開けゴマ" からとって "GM" = "--. --" のモールス信号のコードで開けてます
覗き穴の裏に照度センサーがついてます💡

3. 作成したスパイグッズがこちら😎

1000004570.jpg

  • マイコン

  • 照度センサー

  • 電池 ... 18650 リチウムイオン電池 3.7V 2200mAh

配線はこんな感じ
1000004465.jpg

XIAO_ESP32C3 の裏面には、バッテリーを繋ぐところがあります。はんだづけすごく難しかった。
https://wiki.seeedstudio.com/ja/XIAO_ESP32C3_Getting_Started/

1000004569_2.jpg

  • あと、ドアの開閉にはSwitchBotのロックを使っています

4. 実装

コードはこちら (inoファイル)
// Step3_Final.ino (SwitchBot Lock対応版 - 最新トークン/シークレット)
// XIAO ESP32C3 + SparkFun VEML7700

#include <Wire.h>
#include "SparkFun_VEML7700_Arduino_Library.h"
#include <WiFi.h>
#include <HTTPClient.h>
#include <mbedtls/md.h>
#include <time.h>

VEML7700 mySensor;

// WiFi設定
const char* ssid = "ボクのSSID";
const char* password = "ボクのパスワード";

// SwitchBot API設定(最新)
const char* token = "ボクのトークン";
const char* secret = "ボクのクライアントシークレット";
const char* deviceId = "ボクのデバイスID";

// モールス信号設定
#define DOT_DURATION 250
#define DASH_DURATION 250
#define CHAR_GAP 600
#define WORD_GAP 2000

// 省電力設定
#define IDLE_TIMEOUT 60000
#define LUX_CHANGE_THRESHOLD 50
#define LUX_CHANGE_RATIO 0.4

float baseline = 0;
bool isCovered = false;
unsigned long lastChangeTime = 0;
unsigned long lastStatusPrint = 0;
unsigned long lastActivityTime = 0;
String morseBuffer = "";
String wordBuffer = "";

bool isAwake = true;
float lastLux = 0;
bool timeInitialized = false;

void setup() {
  Serial.begin(115200);
  delay(2000);
  
  Serial.println("========================================");
  Serial.println("モールス信号ドアロックシステム");
  Serial.println("XIAO ESP32C3 + VEML7700");
  Serial.println("========================================");
  
  Wire.begin(6, 7);
  
  if (mySensor.begin()) {
    Serial.println("✅ VEML7700 初期化成功");
  } else {
    Serial.println("❌ センサーエラー");
    while(1);
  }
  
  // 起動時にWiFi接続
  connectWiFi();
  
  // NTPで時刻同期
  if (WiFi.status() == WL_CONNECTED) {
    Serial.println("⏰ NTPサーバーと時刻同期中...");
    configTime(9 * 3600, 0, "ntp.nict.jp", "time.google.com");
    
    struct tm timeinfo;
    int attempts = 0;
    while (!getLocalTime(&timeinfo) && attempts < 10) {
      delay(1000);
      Serial.print(".");
      attempts++;
    }
    
    if (attempts < 10) {
      Serial.println("\n✅ 時刻同期成功");
      Serial.printf("現在時刻: %04d/%02d/%02d %02d:%02d:%02d\n",
                    timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday,
                    timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
      timeInitialized = true;
    } else {
      Serial.println("\n⚠️ 時刻同期失敗(解錠機能は使えません)");
    }
  }
  
  Serial.println("========================================");
  
  // ベースライン取得
  delay(1000);
  float sum = 0;
  for (int i = 0; i < 20; i++) {
    sum += mySensor.getLux();
    delay(50);
  }
  baseline = sum / 20.0;
  lastLux = baseline;
  lastActivityTime = millis();
  
  Serial.printf("初期ベースライン: %.2f lux\n", baseline);
  Serial.println("デバイス: SwitchBot Lock (ロック 1)");
  Serial.println("パスワード: 'GM' を入力してください");
  Serial.println("O: --- (長×3)");
  Serial.println("P: .--. (短・長・長・短)");
  Serial.println("E: . (短×1)");
  Serial.println("N: -. (長・短)");
  Serial.println("========================================");
  Serial.println();
}

void loop() {
  float lux = mySensor.getLux();
  
  // 照度の変化をチェック(絶対値と変化率の両方)
  float luxChange = abs(lux - lastLux);
  float luxChangeRatio = 0;
  
  if (lastLux > 0.1) {
    luxChangeRatio = luxChange / lastLux;
  }
  
  // 絶対値50lux以上 OR 変化率40%以上で省電力解除
  if (luxChange > LUX_CHANGE_THRESHOLD || luxChangeRatio > LUX_CHANGE_RATIO) {
    if (!isAwake) {
      Serial.println("💡 照度変化検出!省電力モード解除");
      Serial.printf("   変化: %.2f lux (%.1f%%)\n", luxChange, luxChangeRatio * 100);
      isAwake = true;
      connectWiFi();
    }
    lastActivityTime = millis();
    lastLux = lux;
  }
  
  // 省電力モード判定
  if (isAwake && (millis() - lastActivityTime > IDLE_TIMEOUT)) {
    Serial.println("😴 1分間変化なし。省電力モードに移行");
    isAwake = false;
    morseBuffer = "";
    wordBuffer = "";
    disconnectWiFi();
  }
  
  if (!isAwake) {
    delay(500);
    return;
  }
  
  // ベースライン更新
  if (!isCovered) {
    baseline = baseline * 0.99 + lux * 0.01;
  }
  
  // 2秒に1回ステータス表示
  if (millis() - lastStatusPrint > 2000) {
    Serial.printf("[状態] 照度: %.2f lux | ベースライン: %.2f lux | WiFi: %s | 単語: %s\n", 
                  lux, baseline,
                  WiFi.status() == WL_CONNECTED ? "接続" : "切断",
                  wordBuffer.c_str());
    lastStatusPrint = millis();
  }
  
  // 覆われた判定
  bool nowCovered = (lux < baseline * 0.3);
  
  // 状態変化検出
  if (nowCovered != isCovered) {
    unsigned long duration = millis() - lastChangeTime;
    lastActivityTime = millis();
    
    if (!nowCovered && isCovered) {
      // 指を離した
      if (duration > 50 && duration < DASH_DURATION) {
        morseBuffer += ".";
        Serial.print(".");
      } else if (duration >= DASH_DURATION) {
        morseBuffer += "-";
        Serial.print("-");
      }
      
    } else if (nowCovered && !isCovered) {
      // 文字間隔チェック
      if (duration > CHAR_GAP && morseBuffer.length() > 0) {
        Serial.print(" → ");
        String letter = decodeMorse(morseBuffer);
        Serial.printf("'%s' ", letter.c_str());
        wordBuffer += letter;
        morseBuffer = "";
        
        Serial.printf("(現在: %s) ", wordBuffer.c_str());
        
        // "GM"判定
        if (wordBuffer == "GM") {
          Serial.println();
          Serial.println("========================================");
          Serial.println("🎉 'GM' 認識成功!");
          Serial.println("========================================");
          
          if (WiFi.status() != WL_CONNECTED) {
            connectWiFi();
          }
          
          unlockDoor();
          wordBuffer = "";
        }
      }
    }
    
    isCovered = nowCovered;
    lastChangeTime = millis();
  }
  
  // タイムアウト処理
  if (millis() - lastChangeTime > WORD_GAP && morseBuffer.length() > 0) {
    Serial.print(" → ");
    String letter = decodeMorse(morseBuffer);
    Serial.printf("'%s'\n", letter.c_str());
    wordBuffer += letter;
    morseBuffer = "";
    
    Serial.printf("(タイムアウト後の単語: %s)\n", wordBuffer.c_str());
    
    if (wordBuffer.length() > 0 && wordBuffer != "GM") {
      Serial.printf("入力: %s (不正解)\n", wordBuffer.c_str());
      wordBuffer = "";
    } else if (wordBuffer == "GM") {
      Serial.println();
      Serial.println("========================================");
      Serial.println("🎉 'GM' 認識成功!");
      Serial.println("========================================");
      
      if (WiFi.status() != WL_CONNECTED) {
        connectWiFi();
      }
      
      unlockDoor();
      wordBuffer = "";
    }
    
    lastChangeTime = millis();
  }
  
  delay(30);
}

void connectWiFi() {
  Serial.printf("📡 WiFi接続中: %s\n", ssid);
  WiFi.begin(ssid, password);
  
  int attempts = 0;
  while (WiFi.status() != WL_CONNECTED && attempts < 40) {
    delay(500);
    Serial.print(".");
    attempts++;
  }
  Serial.println();
  
  if (WiFi.status() == WL_CONNECTED) {
    Serial.println("✅ WiFi接続成功");
    Serial.printf("IPアドレス: %s\n", WiFi.localIP().toString().c_str());
  } else {
    Serial.println("❌ WiFi接続失敗");
  }
}

void disconnectWiFi() {
  if (WiFi.status() == WL_CONNECTED) {
    Serial.println("📡 WiFi切断中...");
    WiFi.disconnect(true);
    WiFi.mode(WIFI_OFF);
    Serial.println("✅ WiFi切断完了");
  }
}

String decodeMorse(String morse) {
  if (morse == ".-") return "A";
  else if (morse == "-...") return "B";
  else if (morse == "-.-.") return "C";
  else if (morse == "-..") return "D";
  else if (morse == ".") return "E";
  else if (morse == "..-.") return "F";
  else if (morse == "--.") return "G";
  else if (morse == "....") return "H";
  else if (morse == "..") return "I";
  else if (morse == ".---") return "J";
  else if (morse == "-.-") return "K";
  else if (morse == ".-..") return "L";
  else if (morse == "--") return "M";
  else if (morse == "-.") return "N";
  else if (morse == "---") return "O";
  else if (morse == ".--.") return "P";
  else if (morse == "--.-") return "Q";
  else if (morse == ".-.") return "R";
  else if (morse == "...") return "S";
  else if (morse == "-") return "T";
  else if (morse == "..-") return "U";
  else if (morse == "...-") return "V";
  else if (morse == ".--") return "W";
  else if (morse == "-..-") return "X";
  else if (morse == "-.--") return "Y";
  else if (morse == "--..") return "Z";
  else return "?";
}

void unlockDoor() {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("❌ WiFi未接続");
    return;
  }
  
  if (!timeInitialized) {
    Serial.println("❌ 時刻が同期されていません");
    return;
  }
  
  HTTPClient http;
  
  // UNIXタイムスタンプ(ミリ秒)を取得
  time_t now = time(nullptr);
  unsigned long long timestamp = (unsigned long long)now * 1000ULL;
  String t = String(timestamp);
  String nonce = "";
  
  // 署名生成
  String stringToSign = String(token) + t + nonce;
  String sign = generateSign(stringToSign);
  
  // v1.1 エンドポイント
  String url = "https://api.switch-bot.com/v1.1/devices/" + String(deviceId) + "/commands";
  
  Serial.println("🔓 SwitchBot Lock 解錠コマンド送信中...");
  Serial.printf("タイムスタンプ: %s\n", t.c_str());
  
  http.begin(url);
  http.addHeader("Authorization", token);
  http.addHeader("sign", sign);
  http.addHeader("t", t);
  http.addHeader("nonce", nonce);
  http.addHeader("Content-Type", "application/json");
  
  String payload = "{\"command\":\"unlock\",\"parameter\":\"default\",\"commandType\":\"command\"}";
  
  int httpResponseCode = http.POST(payload);
  
  Serial.printf("HTTPレスポンスコード: %d\n", httpResponseCode);
  
  if (httpResponseCode > 0) {
    String response = http.getString();
    Serial.printf("レスポンス内容: %s\n", response.c_str());
    
    // statusCodeが100なら成功
    if (response.indexOf("\"statusCode\":100") >= 0 || response.indexOf("\"statusCode\":1") >= 0) {
      Serial.println("✅ ドアを解錠しました!");
    } else if (httpResponseCode == 401) {
      Serial.println("❌ 認証エラー");
    } else if (response.indexOf("\"statusCode\":190") >= 0) {
      Serial.println("⚠️ デバイスがオフライン または サポートされていないコマンドです");
    } else {
      Serial.println("⚠️ 応答を受信しましたが、結果を確認してください");
    }
  } else {
    Serial.printf("❌ HTTP接続エラー: %d\n", httpResponseCode);
  }
  
  http.end();
  Serial.println("========================================");
  Serial.println();
}

String generateSign(String data) {
  byte hmacResult[32];
  
  mbedtls_md_context_t ctx;
  mbedtls_md_type_t md_type = MBEDTLS_MD_SHA256;
  
  mbedtls_md_init(&ctx);
  mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md_type), 1);
  mbedtls_md_hmac_starts(&ctx, (const unsigned char*)secret, strlen(secret));
  mbedtls_md_hmac_update(&ctx, (const unsigned char*)data.c_str(), data.length());
  mbedtls_md_hmac_finish(&ctx, hmacResult);
  mbedtls_md_free(&ctx);
  
  return base64Encode(hmacResult, 32);
}

String base64Encode(const unsigned char* input, size_t length) {
  const char* base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
  String output = "";
  
  for (size_t i = 0; i < length; i += 3) {
    unsigned char b1 = input[i];
    unsigned char b2 = (i + 1 < length) ? input[i + 1] : 0;
    unsigned char b3 = (i + 2 < length) ? input[i + 2] : 0;
    
    output += base64_chars[b1 >> 2];
    output += base64_chars[((b1 & 0x03) << 4) | (b2 >> 4)];
    output += (i + 1 < length) ? base64_chars[((b2 & 0x0F) << 2) | (b3 >> 6)] : '=';
    output += (i + 2 < length) ? base64_chars[b3 & 0x3F] : '=';
  }
  
  return output;
}

テストの動画  ("OPEN" = "--- .--. . -." を入力)

磁石でデバイスをドアの覗き穴にくっつけて、覗き穴の外の照度を読み取ります
1000004483_affinity.png

実装ポイント

  • モールス信号😎
  • 1秒に33回、照度センサの値読み取り💡
  • 朝から夜まで、環境光の変動に自動適応⭐
  • 60秒間、急な変化=操作がなければ、省電力モードに入る💤

5. まとめ

  • これで毎日、踊りながら楽しく帰って、楽しくドアを開けられる💃
  • 友達が遊びくる時とか、スパイのアジトみたいになって楽しそう😎

P.S. 売りポイント

  • 20日くらい電池持つ! (はず)
  • 充電は簡単! USB-Cをマイコンに繋ぐだけ!

$\huge{以上!楽しく解決できた👍}$

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?