カワリモノ息子の技術メモ的な~

カワリモノ息子の技術メモ的な〜

学校が苦手な息子くんの作品などいろいろを在宅パートタイムエンジニアの母が綴る

MENU

小6息子くんRobloxでプレイヤー新規登録者を発射する

タイトル「プレイヤー新規登録者を発射する」て、なんのこっちゃですね。
こんなの作っていました。

f:id:toriko0413:20200222215901g:plain

↑ 初めてgif動画埋め込んでみました。いいですね^^

なんか顔の書いた緑色の紙みたいなものがたくさん発射されていますね。
これ、Robloxにリアルタイムで新規登録された人の情報(顔と名前)らしいです!
すごい勢いでロブロックスのプレイヤー登録者(アカウント登録者)が増えている様子がわかります。世界中から登録されてるんでしょうね!すごい!
そしてゲーム開発側からそんな情報も取得できるんですね。

元にしたスクリプト

公開されているこちらのスクリプトをカスタマイズしたとのこと。
www.roblox.com

息子くんによると、このスクリプトリアルタイムで新規登録者が増えるスピードに処理が追い付いていないから、追いつけるように修正したとのこと。

どのようにしたかというと、非同期でどんどん関数をコールして多重処理してるんですって。
スレッドをたくさん起動して同時に処理しているということです。

息子くん作スクリプトを紹介

そのスクリプトを公開します。
「適当に作ってるからね!」とソースコードが綺麗でないことを主張してましたw

--Get ChatService
local ChatService = require(game:GetService("ServerScriptService"):WaitForChild("ChatServiceRunner"):WaitForChild("ChatService"))

--Wait for the channel 'All' to exist
if not ChatService:GetChannel("All") then
	while true do
		local ChannelName = ChatService.ChannelAdded:Wait()
		if ChannelName == "All" then
			break
		end
	end
end

local function getTime()
    local date = os.date("!*t")
    return ("%02d:%02d %s"):format(((date.hour % 24) - 1) % 12 + 1, date.min, date.hour > 11 and "PM " or "AM ".."(UTC)")
end

--Make your speaker and have it join the 'All' channel we waited for
local Welecome = ChatService:AddSpeaker("Welecome!")
Welecome:JoinChannel("All")

--Change data for your speaker such as name color, chat color, font, text size, and tags
Welecome:SetExtraData("NameColor", Color3.fromRGB(255, 255, 0))
Welecome:SetExtraData("ChatColor", Color3.fromRGB(255, 255, 204))

MaxPos = 0

function WelecomeMessage(PlayerName)
	Welecome:SetExtraData("NameColor", BrickColor.Random().Color)
	Welecome:SayMessage("Roblox Registered! Player Name is "..PlayerName.."!", "All")
	for _,player in pairs(game.Players:GetChildren()) do
		local PlayerLogGui = player.PlayerGui:FindFirstChild("Log")
		local AutoScrollEnabled
		if PlayerLogGui then
			local Template = PlayerLogGui.Window.Frame.PlayerTemplate:Clone()
			AutoScrollEnabled = PlayerLogGui.Window.Frame.CanvasPosition.Y <= MaxPos
			MaxPos = MaxPos + 50
			PlayerLogGui.Window.Frame.CanvasSize = UDim2.new(0,0,0,MaxPos)
			Template.Position = UDim2.new(0,0,0,MaxPos)
			Template.Visible = true
			Template.Parent = PlayerLogGui.Window.Frame
			Template.PlayerName.Text = PlayerName
			Template.Time.Text = getTime()
		else
			game.ReplicatedStorage.Log:Clone().Parent = player.PlayerGui
			PlayerLogGui = player.PlayerGui:FindFirstChild("Log")
			local Template = PlayerLogGui.Window.Frame.PlayerTemplate:Clone()
			AutoScrollEnabled = PlayerLogGui.Window.Frame.CanvasPosition.Y <= MaxPos
			MaxPos = MaxPos + 50
			PlayerLogGui.Window.Frame.CanvasSize = UDim2.new(0,0,0,MaxPos)
			Template.Position = UDim2.new(0,0,0,MaxPos)
			Template.Visible = true
			Template.Parent = PlayerLogGui.Window.Frame
			Template.PlayerName.Text = PlayerName
			Template.Time.Text = getTime()
		end
		if AutoScrollEnabled then
			PlayerLogGui.Window.Frame.CanvasPosition = Vector2.new(0,MaxPos)
		end
	end
end

local f=function(x)
	local y,r=pcall(function()return game.Players:GetNameFromUserIdAsync(x)end)
	if y then return r end
end
local x=0
for i=30,0,-1 do
	print("Isolating range ("..i..")")
	local n=x+2^i
	if f(n)or f(n+1)or f(n+2)then
		x=n
	end
end
print("Newest user located")

while wait() do
	spawn(function()
		local ox = x
		while wait() do
			local newuser = f(ox)
			if newuser ~= nil then
				print("new user: "..newuser.." (".. tostring(x) ..")")
				WelecomeMessage(newuser)
				local Panel = game.ReplicatedStorage.UserPanel:Clone()
				Panel.Parent = workspace
				Panel.UserImage.Texture = "https://www.roblox.com/headshot-thumbnail/image?userId=" .. tostring(x-1) .. "&width=420&height=420&format=png"
				Panel.Gui.PlayerName.Text = newuser .. " (".. tostring(x) ..")"
				game.Debris:AddItem(Panel,30)
				break
			end
		end
	end)
	x = x + 1
end

割と下の方の

while wait() do
	spawn(function()

が、非同期で多重起動しているところだと教えてもらいました。
function を spawn してるんですね。なるほど。

f:id:toriko0413:20200222224659p:plain

また、「Players Log」という画面は改造して作成したとのことで、これを開くと登録時刻とユーザー名を確認することができます。

f:id:toriko0413:20200222224526p:plain

最近はOSいじりとRobloxとYouTube鑑賞が主な活動の息子くん。
もうすぐ中学生なんです。
中学はN中で、入学手続き済ませました。どんな感じになるかなー。

Roblox関連記事

以前公開したライブラリ

siroitori.hatenablog.com
siroitori.hatenablog.com

乗っ取り被害にあったらカスタマーサポートに問い合わせましょう

siroitori.hatenablog.com

Robloxのすべての記事はこちら

siroitori.hatenablog.com



スター・はてブとても嬉しいです!

M5StackでIKEAふう回転置き時計を作った

(今回は息子の母作品ブログです。)

私、ひらめきました。
M5StackでIKEAのクロック(回転していろいろ表示するやつ)が作れる!!作りたい!!

↓ 我が家のIKEAのクロック、クロッキス。壊れてしまって傾けても表示が変わらない…。
f:id:toriko0413:20200221215348j:plain

そして情報収集すべくネットを検索すると…なんともうすでに作っている方がいらっしゃいました( ゚Д゚)

qiita.com

しかもビンゴでM5Stack FIREで作っていらっしゃる!!
では私はこれをベースにさせていただき、さらに環境センサからBLEで受信した値を出力してみよう!と思いました。

M5Stackの傾きの取得

このQiita記事を参考に「MPU9250」を使用したんですがこれがどーーーしてもうまくいかない。

M5Stack FIREの加速度センサは何度かモデルチェンジされており、今回はMPU9250内蔵モデルを使用している

↑ Qiita記事より

調べてみたら私が2020年1月に買ったM5Stack FireではMPU9250ではなくMPU6886というものが搭載されていることが判明。

f:id:toriko0413:20200221200746j:plain
付属のちっちゃい説明書の赤丸部分。

それでMPU9250について検索したら

#define M5STACK_MPU6886 

ってやってから

  M5.IMU.getGyroData(&gyroX,&gyroY,&gyroZ);
  M5.IMU.getAccelData(&accX,&accY,&accZ);
  M5.IMU.getAhrsData(&pitch,&roll,&yaw);
  M5.IMU.getTempData(&temp);

のように書くとどうやら各種値がとれるらしい!と判明したのでまずはこの値が確認できるように実装。

そして動かしてみて各値がどのように変化したかを確認。
どうやらgetAhrsDataで取得したpitchとrollで回転状態が判断できそうということがわかりこれを利用することにしました。

ところで今回使おうと思うgetAhrsData以外のものについても調べてみたところ
getGyroData …ジャイロの値を取得
getAccelData …加速度の値を取得
getAhrsData …3次元空間での回転状態を取得
ということでした。
私ハード初心者なもので「ジャイロ」って何。。って具合なもので、各値についてもっと勉強すべきなのですが、テキトーな性格なものでとりあえず早く動くものを作りたいということでまたの機会に(;^ω^)

typedef enum
{
  APP_STATE_0 = 0,        // 正位置:日付時刻
  APP_STATE_1 = 1,        // 左が下:湿度計 %
  APP_STATE_2 = 2,        // 右が下:温度計 ℃
  APP_STATE_3 = 3,        // 逆位置:気圧 hPa
} app_state_e;

app_state_e currentAngle = APP_STATE_0;           // 現在の方向

~中略~

app_state_e getAngle(){
  app_state_e angle = APP_STATE_0;

  M5.IMU.getAhrsData(&pitch,&roll,&yaw);
//  Serial.printf(" %5.2f   %5.2f   %5.2f   ", pitch, roll, yaw);
//  Serial.println("");

  if (pitch > -50.0F && pitch < 50.0F){
    if (roll < 0.0F){
      return APP_STATE_3;
    }else{
      return APP_STATE_0;
    }
  }else if (pitch >= 50.0F){
    return APP_STATE_2;
  }else if (pitch < -50.0F){
    return APP_STATE_1;
  }

  return angle;
}

getAngle関数で現在の回転方向を決定するように作りました。
これでなんとなく回転方向が取れそうです。

出来上がったのがこちら

わーはじめてTwitter埋め込んでみた^^

全体のソースコード

// define must ahead #include <M5Stack.h>
#define M5STACK_MPU6886 

// LCD
#define LCD_LARGE_BAR_WIDTH (10)
#define LCD_LARGE_BAR_LENGTH (30)
#define LCD_LARGE_BAR_CORNER_RADIUS (6)
#define LCD_LARGE_BAR_GAP (LCD_LARGE_BAR_WIDTH >> 1)

#define LCD_SMALL_BAR_WIDTH (4)
#define LCD_SMALL_BAR_LENGTH (12)
#define LCD_SMALL_BAR_CORNER_RADIUS (3)
#define LCD_SMALL_BAR_GAP (LCD_SMALL_BAR_WIDTH >> 1)

#define LCD_DIGITS_CLEAR_ELM_NO (8)

// Thermometer Screen
#define LCD_THERMOMETER_ICON_DISP_Y_POS (80)
#define LCD_THERMOMETER_TEMPERATURE_DISP_Y_POS (80)

#include "BLEDevice.h"
#include <M5Stack.h>
#include <WiFi.h>

static String omronSensorAdress = "fd:e5:99:72:2c:29";
static BLEUUID serviceUUID("0c4c3000-7700-46f4-aa96-d5e974e32a54");
static BLEUUID    charUUID("0c4C3001-7700-46F4-AA96-D5E974E32A54");

float pitch = 0.0F;
float roll  = 0.0F;
float yaw   = 0.0F;

// OMRON環境センサの値
long seq;
long temp;
long humid;
long light;
long uv;
long pressValue;
long noise;
long accelX;
long accelY;
long accelZ;
long batt;
      
typedef enum
{
  APP_STATE_0 = 0,        // 正位置:日付時刻
  APP_STATE_1 = 1,        // 左が下:湿度計 %
  APP_STATE_2 = 2,        // 右が下:温度計 ℃
  APP_STATE_3 = 3,        // 逆位置:気圧 hPa
} app_state_e;

app_state_e currentAngle = APP_STATE_0;           // 現在の方向
app_state_e changingCurrentAngle;   // 方向変更中(変更中は値が定まらないのでテンポラリとして格納)
int32_t changingAngleCount;     // 方向変更後の連続数(連続して方向が定まったときに角度変更確定とする)

static int ANGLE_DECISION_TIME =  50;

boolean is_state_changed = true;
boolean is_drawing = false;
boolean is_initialize = true;     // 初回起動時

// NTP
//const char *ntp_server_1st = "ntp.nict.jp";
const char *ntp_server_1st = "time.windows.com";
const char *ntp_server_2nd = "time.google.com";
const long gmt_offset_sec = 9 * 3600; // 時差(秒換算)
const int daylight_offset_sec = 0;    // 夏時間

boolean is_blink = true;

const uint16_t digits_V_long[] =
    {
        0b0000001111111111, // 0
        0b0000001111000000, // 1
        0b0000011100111001, // 2
        0b0000011111100001, // 3
        0b0000011111000110, // 4
        0b0000010011100111, // 5
        0b0000010011111111, // 6
        0b0000001111000111, // 7
        0b0000011111111111, // 8
        0b0000011111100111, // 9
        0b0000000000000000, // off
};

void drawNumberVLong(uint8_t x_start, uint8_t y_start, uint8_t number, uint8_t bar_width, uint8_t bar_length, uint8_t bar_gap, uint8_t corner_radius, uint16_t color_value)
{
  if (number > 10)
  {
    number = 10;
  }
  // top
  if (digits_V_long[number] & 0b0000000000000001)
    M5.Lcd.fillRoundRect(x_start, y_start, bar_length, bar_width, corner_radius, color_value);
  // upper-left
  if (digits_V_long[number] & 0b0000000000000010)
    M5.Lcd.fillRoundRect((x_start - bar_gap * 2), (y_start + bar_gap), bar_width, bar_length, corner_radius, color_value);
  if (digits_V_long[number] & 0b0000000000000100)
    M5.Lcd.fillRoundRect((x_start - bar_gap * 2), (y_start + bar_gap + bar_length * 1), bar_width, bar_length, corner_radius, color_value);
  // under-left
  if (digits_V_long[number] & 0b0000000000001000)
    M5.Lcd.fillRoundRect((x_start - bar_gap * 2), (y_start + bar_gap + bar_length * 2), bar_width, bar_length, corner_radius, color_value);
  if (digits_V_long[number] & 0b0000000000010000)
    M5.Lcd.fillRoundRect((x_start - bar_gap * 2), (y_start + bar_gap + bar_length * 3), bar_width, bar_length, corner_radius, color_value);
  // bottom
  if (digits_V_long[number] & 0b0000000000100000)
    M5.Lcd.fillRoundRect(x_start, (y_start + bar_length * 4), bar_length, bar_width, corner_radius, color_value);
  // under-right
  if (digits_V_long[number] & 0b0000000001000000)
    M5.Lcd.fillRoundRect((x_start + bar_length), (y_start + bar_gap + bar_length * 3), bar_width, bar_length, corner_radius, color_value);
  if (digits_V_long[number] & 0b0000000010000000)
    M5.Lcd.fillRoundRect((x_start + bar_length), (y_start + bar_gap + bar_length * 2), bar_width, bar_length, corner_radius, color_value);
  // upper-right
  if (digits_V_long[number] & 0b0000000100000000)
    M5.Lcd.fillRoundRect((x_start + bar_length), (y_start + bar_gap + bar_length * 1), bar_width, bar_length, corner_radius, color_value);
  if (digits_V_long[number] & 0b0000001000000000)
    M5.Lcd.fillRoundRect((x_start + bar_length), (y_start + bar_gap), bar_width, bar_length, corner_radius, color_value);
  // center
  if (digits_V_long[number] & 0b0000010000000000)
    M5.Lcd.fillRoundRect(x_start, (y_start + bar_length * 2), bar_length, bar_width, corner_radius, color_value);
}

const uint8_t digits_normal[] =
    {
        0b00111111, // 0
        0b00110000, // 1
        0b01101101, // 2
        0b01111001, // 3
        0b01110010, // 4
        0b01011011, // 5
        0b01011111, // 6
        0b00110011, // 7
        0b01111111, // 8
        0b01111011, // 9
        0b00000000, // off
};

void drawNumberNormal(uint8_t x_start, uint8_t y_start, uint8_t number, uint8_t bar_width, uint8_t bar_length, uint8_t bar_gap, uint8_t corner_radius, uint16_t color_value)
{
  if (number > 10)
  {
    number = 10;
  }
  // top
  if (digits_normal[number] & 0b0000000000000001)
    M5.Lcd.fillRoundRect(x_start, y_start, bar_length, bar_width, corner_radius, color_value);
  // upper-left
  if (digits_normal[number] & 0b0000000000000010)
    M5.Lcd.fillRoundRect((x_start - bar_gap * 2), (y_start + bar_gap), bar_width, bar_length, corner_radius, color_value);
  // under-left
  if (digits_normal[number] & 0b0000000000000100)
    M5.Lcd.fillRoundRect((x_start - bar_gap * 2), (y_start + bar_gap + bar_length * 1), bar_width, bar_length, corner_radius, color_value);
  // bottom
  if (digits_normal[number] & 0b0000000000001000)
    M5.Lcd.fillRoundRect(x_start, (y_start + bar_length * 2), bar_length, bar_width, corner_radius, color_value);
  // under-right
  if (digits_normal[number] & 0b0000000000010000)
    M5.Lcd.fillRoundRect((x_start + bar_length), (y_start + bar_gap + bar_length * 1), bar_width, bar_length, corner_radius, color_value);
  // upper-right
  if (digits_normal[number] & 0b0000000000100000)
    M5.Lcd.fillRoundRect((x_start + bar_length), (y_start + bar_gap), bar_width, bar_length, corner_radius, color_value);
  // center
  if (digits_normal[number] & 0b0000000001000000)
    M5.Lcd.fillRoundRect(x_start, (y_start + bar_length * 1), bar_length, bar_width, corner_radius, color_value);
}

app_state_e getAngle(){
  app_state_e angle = APP_STATE_0;

  M5.IMU.getAhrsData(&pitch,&roll,&yaw);
//  Serial.printf(" %5.2f   %5.2f   %5.2f   ", pitch, roll, yaw);
//  Serial.println("");

  if (pitch > -50.0F && pitch < 50.0F){
    if (roll < 0.0F){
      return APP_STATE_3;
    }else{
      return APP_STATE_0;
    }
  }else if (pitch >= 50.0F){
    return APP_STATE_2;
  }else if (pitch < -50.0F){
    return APP_STATE_1;
  }

  return angle;
}

void monitorAngleTask(void* arg){
  while (1) {
     // 方向を検出
    app_state_e body_angle = getAngle();
    if (body_angle != currentAngle){
      if (changingCurrentAngle != body_angle){
        changingCurrentAngle = body_angle;
        changingAngleCount = 1;
      }else{
        changingAngleCount++;
      }
      // 変更後方向
      if (changingAngleCount >= ANGLE_DECISION_TIME){
        // 一定回数以上方向が同じ方向と認識したら変更後の方向に確定
        Serial.println("!!ANGLE CHANGE!!");
        switch (body_angle){
        case APP_STATE_0:
          Serial.println("UPSIDE");
          M5.Lcd.setRotation(1);
          break;
        case APP_STATE_1:
          Serial.println("LEFT");
          M5.Lcd.setRotation(2);
          break;
        case APP_STATE_2:
          Serial.println("RIGHT");
          M5.Lcd.setRotation(0);
          break;
        case APP_STATE_3:
          Serial.println("BOTTOM");
          M5.Lcd.setRotation(3);
          break;
        }
        currentAngle = body_angle;
        changingAngleCount = 0;
        is_state_changed = true;

        // 画面表示
        setDisplay();
        is_state_changed = false;
      }   
    }
    
    delay(1);
  }
}

long getParamFromAdvertisedData(std::string advertisedDataString, int pos, int length, String paramName) {
  // 38桁の16進表記文字列から対象のパラメータ位置で抜き出して10進数に変換して戻す
  std::string substrData = "";
  String manufData = "";
  Serial.print(paramName);
  Serial.print(": [hex] ");
  substrData = advertisedDataString.substr(pos, length); // 指定位置から指定桁数抜き出す
  Serial.print(substrData.c_str());

  if (length > 2){
    // 2桁以上(今回は4桁しかない)のときは反対にする 例)7d08→087d
    for(int i=length-2; i>=0; i=i-2){
      manufData.concat(substrData.substr(i, 2).c_str());  
    }
  }else{
    manufData = substrData.c_str();
  }

  // 16進数→10進数変換
  long ret = strtol(manufData.c_str(), NULL, 16);
  Serial.print(" [dec] ");
  Serial.println(ret);
  
  return ret;
}

/**
 * Scan for BLE servers and find the first one that advertises the service we are looking for.
 */
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
 /**
   * Called for each advertising BLE server.
   */
  void onResult(BLEAdvertisedDevice advertisedDevice) {
    Serial.println("BLE Advertised Device found: ");
    std::string advertisedDataString = advertisedDevice.toString().c_str();
    Serial.println(advertisedDataString.c_str());  // このなかの文字列 manufacturer data:d502のあとがデータになる
  
    String deviceAddress = advertisedDevice.getAddress().toString().c_str();
    Serial.println(deviceAddress.c_str());
    //ペリフェラルをアドレスで判断
    if(deviceAddress.equalsIgnoreCase(omronSensorAdress)){

      int manufPos = advertisedDataString.find("manufacturer data: d502");
      Serial.print("manufPos: ");
      Serial.println(manufPos);

      seq = getParamFromAdvertisedData(advertisedDataString, manufPos+23, 2, "seq");
      
      temp = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2, 4, "temp");
      humid = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4, 4, "humid");
      light = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4+4, 4, "light");
      uv = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4+4+4, 4, "uv");
      pressValue = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4+4+4+4, 4, "press");
      noise = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4+4+4+4+4, 4, "noise");
      accelX = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4+4+4+4+4+4, 4, "accelX");
      accelY = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4+4+4+4+4+4+4, 4, "accelY");
      accelZ = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4+4+4+4+4+4+4+4, 4, "accelZ");
      batt = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4+4+4+4+4+4+4+4+4, 2, "batt");
      
    }// Found our server
  } // onResult
}; // MyAdvertisedDeviceCallbacks

/**
 * 温度計を表示する
 */
void displayThermometerScreen(){
  uint16_t bkground_color = M5.Lcd.color565(0, 180, 0);
  static long temperature_prev = 0;
  if (is_state_changed == true){
    M5.Lcd.fillScreen(bkground_color);
//    is_state_changed = false;
  }

  // draw thermometer icon
  drawThermometerIcon(40, LCD_THERMOMETER_ICON_DISP_Y_POS, bkground_color);

  uint8_t tens_place, ones_place;

  // 温度は20.95度のとき2095のようになるのでまず小数点以上を取り出してから10の位と1の位に分ける
  long temperature = temp / 100;
  tens_place = static_cast<uint8_t>(temperature / 10);
  ones_place = static_cast<uint8_t>(temperature % 10);
  
  Serial.print("temperature = ");
  Serial.println(temperature);
  Serial.print("tens_place = ");
  Serial.println(tens_place);
  Serial.print("ones_place = ");
  Serial.println(ones_place);

  if (temperature != temperature_prev)
  {
    // デジタル表示値をクリア
    drawNumberVLong(60, LCD_THERMOMETER_TEMPERATURE_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, bkground_color);
    drawNumberVLong(130, LCD_THERMOMETER_TEMPERATURE_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, bkground_color);

  }
  // デジタル値を表示
  drawNumberVLong(60, LCD_THERMOMETER_TEMPERATURE_DISP_Y_POS, tens_place, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, TFT_BLACK);
  drawNumberVLong(130, LCD_THERMOMETER_TEMPERATURE_DISP_Y_POS, ones_place, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, TFT_BLACK);

  // ℃の°
  M5.Lcd.drawEllipse(130 + LCD_LARGE_BAR_WIDTH + LCD_LARGE_BAR_LENGTH + LCD_LARGE_BAR_GAP, LCD_THERMOMETER_TEMPERATURE_DISP_Y_POS, 3, 3, TFT_BLACK);
  // ℃のC
  M5.Lcd.drawChar(130 + LCD_LARGE_BAR_WIDTH + LCD_LARGE_BAR_LENGTH + LCD_LARGE_BAR_GAP + 10, LCD_THERMOMETER_TEMPERATURE_DISP_Y_POS, 'C', TFT_BLACK, bkground_color, 4);

  temperature_prev = temperature;
}

/**
 * 湿度計を表示する
 */
void displayHumidityScreen(){
  uint16_t bkground_color = M5.Lcd.color565(0, 180, 0);
//  static uint8_t temperature_prev = 0;
  static long humid_prev = 0;
  if (is_state_changed == true)
  {
    M5.Lcd.fillScreen(bkground_color);
//    is_state_changed = false;
  }

  uint8_t tens_place, ones_place;

  // 湿度は39.97%のとき3997のようになるのでまず小数点以上を取り出してから10の位と1の位に分ける
  long humidity = humid / 100;
  tens_place = static_cast<uint8_t>(humidity / 10);
  ones_place = static_cast<uint8_t>(humidity % 10);
  
  Serial.print("humidity = ");
  Serial.println(humidity);
  Serial.print("tens_place = ");
  Serial.println(tens_place);
  Serial.print("ones_place = ");
  Serial.println(ones_place);

  if (humidity != humid_prev)
  {
    // デジタル表示値をクリア
    drawNumberVLong(60, LCD_THERMOMETER_TEMPERATURE_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, bkground_color);
    drawNumberVLong(130, LCD_THERMOMETER_TEMPERATURE_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, bkground_color);

  }
  // デジタル値を表示
  drawNumberVLong(60, LCD_THERMOMETER_TEMPERATURE_DISP_Y_POS, tens_place, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, TFT_BLACK);
  drawNumberVLong(130, LCD_THERMOMETER_TEMPERATURE_DISP_Y_POS, ones_place, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, TFT_BLACK);

  // %
  M5.Lcd.drawChar(130 + LCD_LARGE_BAR_WIDTH + LCD_LARGE_BAR_LENGTH + LCD_LARGE_BAR_GAP + 10, LCD_THERMOMETER_TEMPERATURE_DISP_Y_POS, '%', TFT_BLACK, bkground_color, 4);

  humid_prev = humidity;
}

/**
 * 温度計アイコンを表示する
 */
void drawThermometerIcon(uint32_t base_x_pos, uint32_t base_y_pos, uint16_t bk_color)
{
  M5.Lcd.drawEllipse(base_x_pos, base_y_pos, 7, 7, TFT_BLACK);
  M5.Lcd.drawRoundRect(base_x_pos - 3, base_y_pos - 35, 8, 35, 4, TFT_BLACK);
  M5.Lcd.fillEllipse(base_x_pos, base_y_pos, 6, 6, bk_color);
  M5.Lcd.fillEllipse(base_x_pos, base_y_pos, 4, 4, TFT_BLACK);
  M5.Lcd.fillRoundRect(base_x_pos - 1, base_y_pos - 35 + 8, 4, 25, 2, TFT_BLACK);
}


// System Clock
class SystemClock
{
private:
  struct tm time_info;
  time_t timer;

public:
  uint32_t year = 0;
  uint32_t month = 0;
  uint32_t day = 0;
  uint32_t hour = 0;
  uint32_t minute = 0;
  uint32_t week_day = 0;
  uint32_t second = 0;
  uint32_t prev_year = 0;
  uint32_t prev_month = 0;
  uint32_t prev_day = 0;
  uint32_t prev_hour = 0;
  uint32_t prev_minute = 0;
  uint32_t prev_week_day = 0;
  uint32_t prev_second = 0;
  void backupCurrentTime(void);
  void updateByNtp(void);
  void updateBySoftTimer(uint32_t elasped_second);
};

void SystemClock::backupCurrentTime(void)
{
  prev_year = year;
  prev_month = month;
  prev_day = day;
  prev_hour = hour;
  prev_minute = minute;
  prev_week_day = week_day;
  prev_second = second;
}

void SystemClock::updateBySoftTimer(uint32_t elasped_second)
{
  struct tm *local_time;
  time_t timer_add = timer + elasped_second;
  local_time = localtime(&timer_add);
  year = local_time->tm_year + 1900;
  month = local_time->tm_mon + 1;
  day = local_time->tm_mday;
  hour = local_time->tm_hour;
  minute = local_time->tm_min;
  week_day = local_time->tm_wday;
  second = local_time->tm_sec;
}

void SystemClock::updateByNtp(void)
{
  Serial.println("---NTP ACCESS---");
  if (!getLocalTime(&time_info))
  {
    year = 0;
    month = 0;
    day = 0;
    hour = 0;
    minute = 0;
    week_day = 0;
    second = 0;
    timer = 0;
  }
  else
  {
    year = time_info.tm_year + 1900;
    month = time_info.tm_mon + 1;
    day = time_info.tm_mday;
    hour = time_info.tm_hour;
    minute = time_info.tm_min;
    week_day = time_info.tm_wday;
    second = time_info.tm_sec;
    timer = mktime(&time_info);
  }
}

SystemClock cl_system_clock;
#define NTP_ACCESS_MS_INTERVAL (300000)

#define LCD_CLOCK_YMD_DISP_Y_POS (10)
#define LCD_CLOCK_MD_STR_DISP_Y_POS (45)
#define LCD_CLOCK_HM_DISP_Y_POS (100)
#define LCD_CLOCK_PM_STR_DISP_Y_POS (175)
#define LCD_CLOCK_ICON_DISP_Y_POS (200)
#define LCD_CLOCK_WEEK_STR_DISP_Y_POS (220)

/**
 * 時計を表示する
 */
void displayDateTimeScreen()
{
  static boolean ntp_access_flag = true;
  static uint32_t base_milli_time;
  uint32_t elasped_second = 0;
  uint32_t diff_milli_time = 0;

  uint16_t bkground_color = M5.Lcd.color565(200, 0, 0);
  if (is_state_changed == true)
  {
    M5.Lcd.fillScreen(bkground_color);
    ntp_access_flag = true;
//    is_state_changed = false;
  }

  if (ntp_access_flag == true)
  {
    base_milli_time = millis();
    Serial.print("base_milli_time:");
    Serial.println(base_milli_time);
    cl_system_clock.updateByNtp();
    ntp_access_flag = false;
  }
  else
  {
    diff_milli_time = millis() - base_milli_time;
    if (diff_milli_time > NTP_ACCESS_MS_INTERVAL)
    {
      ntp_access_flag = true;
    }
    // Serial.print("diff_milli_time:");
    // Serial.println(diff_milli_time);
    elasped_second = diff_milli_time / 1000;
    cl_system_clock.updateBySoftTimer(elasped_second);
  }

  M5.Lcd.setTextColor(TFT_BLACK);
  M5.Lcd.setTextSize(2);

  // Month
  if (cl_system_clock.month != cl_system_clock.prev_month)
  {
    drawNumberNormal(10, LCD_CLOCK_YMD_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_SMALL_BAR_WIDTH, LCD_SMALL_BAR_LENGTH, LCD_SMALL_BAR_GAP, LCD_SMALL_BAR_CORNER_RADIUS, bkground_color);
    drawNumberNormal(35, LCD_CLOCK_YMD_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_SMALL_BAR_WIDTH, LCD_SMALL_BAR_LENGTH, LCD_SMALL_BAR_GAP, LCD_SMALL_BAR_CORNER_RADIUS, bkground_color);
  }
  drawNumberNormal(10, LCD_CLOCK_YMD_DISP_Y_POS, (cl_system_clock.month / 10), LCD_SMALL_BAR_WIDTH, LCD_SMALL_BAR_LENGTH, LCD_SMALL_BAR_GAP, LCD_SMALL_BAR_CORNER_RADIUS, TFT_BLACK);
  drawNumberNormal(35, LCD_CLOCK_YMD_DISP_Y_POS, (cl_system_clock.month % 10), LCD_SMALL_BAR_WIDTH, LCD_SMALL_BAR_LENGTH, LCD_SMALL_BAR_GAP, LCD_SMALL_BAR_CORNER_RADIUS, TFT_BLACK);

  M5.Lcd.drawLine(60, LCD_SMALL_BAR_LENGTH * 2 + 10, 70, 10, TFT_BLACK);

  // Day
  if (cl_system_clock.day != cl_system_clock.prev_day)
  {
    drawNumberNormal(80, LCD_CLOCK_YMD_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_SMALL_BAR_WIDTH, LCD_SMALL_BAR_LENGTH, LCD_SMALL_BAR_GAP, LCD_SMALL_BAR_CORNER_RADIUS, bkground_color);
    drawNumberNormal(105, LCD_CLOCK_YMD_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_SMALL_BAR_WIDTH, LCD_SMALL_BAR_LENGTH, LCD_SMALL_BAR_GAP, LCD_SMALL_BAR_CORNER_RADIUS, bkground_color);
  }
  drawNumberNormal(80, LCD_CLOCK_YMD_DISP_Y_POS, (cl_system_clock.day / 10), LCD_SMALL_BAR_WIDTH, LCD_SMALL_BAR_LENGTH, LCD_SMALL_BAR_GAP, LCD_SMALL_BAR_CORNER_RADIUS, TFT_BLACK);
  drawNumberNormal(105, LCD_CLOCK_YMD_DISP_Y_POS, (cl_system_clock.day % 10), LCD_SMALL_BAR_WIDTH, LCD_SMALL_BAR_LENGTH, LCD_SMALL_BAR_GAP, LCD_SMALL_BAR_CORNER_RADIUS, TFT_BLACK);

  // Year
  if (cl_system_clock.year != cl_system_clock.prev_year)
  {
    drawNumberNormal(180, LCD_CLOCK_YMD_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_SMALL_BAR_WIDTH, LCD_SMALL_BAR_LENGTH, LCD_SMALL_BAR_GAP, LCD_SMALL_BAR_CORNER_RADIUS, bkground_color);
    drawNumberNormal(205, LCD_CLOCK_YMD_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_SMALL_BAR_WIDTH, LCD_SMALL_BAR_LENGTH, LCD_SMALL_BAR_GAP, LCD_SMALL_BAR_CORNER_RADIUS, bkground_color);
    drawNumberNormal(230, LCD_CLOCK_YMD_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_SMALL_BAR_WIDTH, LCD_SMALL_BAR_LENGTH, LCD_SMALL_BAR_GAP, LCD_SMALL_BAR_CORNER_RADIUS, bkground_color);
    drawNumberNormal(255, LCD_CLOCK_YMD_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_SMALL_BAR_WIDTH, LCD_SMALL_BAR_LENGTH, LCD_SMALL_BAR_GAP, LCD_SMALL_BAR_CORNER_RADIUS, bkground_color);
  }
  drawNumberNormal(180, LCD_CLOCK_YMD_DISP_Y_POS, (cl_system_clock.year / 1000), LCD_SMALL_BAR_WIDTH, LCD_SMALL_BAR_LENGTH, LCD_SMALL_BAR_GAP, LCD_SMALL_BAR_CORNER_RADIUS, TFT_BLACK);
  drawNumberNormal(205, LCD_CLOCK_YMD_DISP_Y_POS, ((cl_system_clock.year % 1000) / 100), LCD_SMALL_BAR_WIDTH, LCD_SMALL_BAR_LENGTH, LCD_SMALL_BAR_GAP, LCD_SMALL_BAR_CORNER_RADIUS, TFT_BLACK);
  drawNumberNormal(230, LCD_CLOCK_YMD_DISP_Y_POS, (((cl_system_clock.year % 1000) % 100) / 10), LCD_SMALL_BAR_WIDTH, LCD_SMALL_BAR_LENGTH, LCD_SMALL_BAR_GAP, LCD_SMALL_BAR_CORNER_RADIUS, TFT_BLACK);
  drawNumberNormal(255, LCD_CLOCK_YMD_DISP_Y_POS, (((cl_system_clock.year % 1000) % 100) % 10), LCD_SMALL_BAR_WIDTH, LCD_SMALL_BAR_LENGTH, LCD_SMALL_BAR_GAP, LCD_SMALL_BAR_CORNER_RADIUS, TFT_BLACK);

  // hour
  if (cl_system_clock.hour != cl_system_clock.prev_hour)
  {
    drawNumberNormal(30, LCD_CLOCK_HM_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, bkground_color);
    drawNumberNormal(95, LCD_CLOCK_HM_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, bkground_color);
    M5.Lcd.setTextColor(bkground_color);
    M5.Lcd.drawString("PM", 20, LCD_CLOCK_PM_STR_DISP_Y_POS);
    M5.Lcd.setTextColor(TFT_BLACK);
  }
  drawNumberNormal(30, LCD_CLOCK_HM_DISP_Y_POS, (cl_system_clock.hour / 10), LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, TFT_BLACK);
  drawNumberNormal(95, LCD_CLOCK_HM_DISP_Y_POS, (cl_system_clock.hour % 10), LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, TFT_BLACK);

  if (is_blink == true)
  {
    M5.Lcd.fillEllipse(150, 120, 4, 4, TFT_BLACK);
    M5.Lcd.fillEllipse(150, 150, 4, 4, TFT_BLACK);
  }
  else
  {
    M5.Lcd.fillEllipse(150, 120, 4, 4, bkground_color);
    M5.Lcd.fillEllipse(150, 150, 4, 4, bkground_color);
  }

  // minute
  if (cl_system_clock.minute != cl_system_clock.prev_minute)
  {
    drawNumberNormal(185, LCD_CLOCK_HM_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, bkground_color);
    drawNumberNormal(250, LCD_CLOCK_HM_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, bkground_color);
  }
  drawNumberNormal(185, LCD_CLOCK_HM_DISP_Y_POS, (cl_system_clock.minute / 10), LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, TFT_BLACK);
  drawNumberNormal(250, LCD_CLOCK_HM_DISP_Y_POS, (cl_system_clock.minute % 10), LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, TFT_BLACK);

  // MONTH DATE
  const String s_month = "MONTH";
  const String s_date = "DATE";
  M5.Lcd.drawString(s_month, 10, LCD_CLOCK_MD_STR_DISP_Y_POS);
  M5.Lcd.drawString(s_date, 80, LCD_CLOCK_MD_STR_DISP_Y_POS);

  uint8_t week_count = 0;
  const char aweek[7][4] = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"};
  uint8_t day_week_now = 0;
  // day_week_now = subZeller(t_date_now.npt_year, t_date_now.ntp_month, t_date_now.ntp_day);
  day_week_now = cl_system_clock.week_day;
  for (week_count = 0; week_count < 7; ++week_count)
  {
    if ((week_count == day_week_now) && (is_blink == false))
    {
      M5.Lcd.setTextColor(bkground_color);
      M5.Lcd.drawString(aweek[week_count], week_count * 45 + 10, LCD_CLOCK_WEEK_STR_DISP_Y_POS);
      M5.Lcd.setTextColor(TFT_BLACK);
    }
    else
    {
      M5.Lcd.drawString(aweek[week_count], week_count * 45 + 10, LCD_CLOCK_WEEK_STR_DISP_Y_POS);
    }
  }
  is_blink = !is_blink;

  // clock icon
//  drawClockIcon(300, LCD_CLOCK_ICON_DISP_Y_POS);

  // t_date_prev = t_date_now;
  cl_system_clock.backupCurrentTime();
}

/**
 * 気圧計を表示する
 */
void displayPressScreen(){
  uint16_t bkground_color = M5.Lcd.color565(128, 128, 255);
  static long pressure_prev = 0;
  if (is_state_changed == true)
  {
    M5.Lcd.fillScreen(bkground_color);
//    is_state_changed = false;
  }

  uint8_t milions_place, hundreds_place, tens_place, ones_place;

  // 気圧は1032.6hPaのとき10326のようになるのでまず小数点以上を取り出してから位に分ける
  long pressure = pressValue / 10;
  milions_place = static_cast<uint8_t>(pressure / 1000);
  hundreds_place = static_cast<uint8_t>((pressure / 100) %10);
  tens_place = static_cast<uint8_t>((pressure / 10) % 100);
  ones_place = static_cast<uint8_t>(pressure % 10);
  
  Serial.print("pressure = ");
  Serial.println(pressure);
  Serial.print("milions_place = ");
  Serial.println(milions_place);
  Serial.print("hundreds_place = ");
  Serial.println(hundreds_place);
  Serial.print("tens_place = ");
  Serial.println(tens_place);
  Serial.print("ones_place = ");
  Serial.println(ones_place);

  if (pressure != pressure_prev)
  {
    // デジタル表示値をクリア
    drawNumberNormal(30, LCD_CLOCK_HM_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, bkground_color);
    drawNumberNormal(95, LCD_CLOCK_HM_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, bkground_color);
    drawNumberNormal(160, LCD_CLOCK_HM_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, bkground_color);
    drawNumberNormal(225, LCD_CLOCK_HM_DISP_Y_POS, LCD_DIGITS_CLEAR_ELM_NO, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, bkground_color);
  }
  // デジタル値を表示
  drawNumberNormal(30, LCD_CLOCK_HM_DISP_Y_POS, milions_place, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, TFT_BLACK);
  drawNumberNormal(95, LCD_CLOCK_HM_DISP_Y_POS, hundreds_place, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, TFT_BLACK);
  drawNumberNormal(160, LCD_CLOCK_HM_DISP_Y_POS, tens_place, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, TFT_BLACK);
  drawNumberNormal(225, LCD_CLOCK_HM_DISP_Y_POS, ones_place, LCD_LARGE_BAR_WIDTH, LCD_LARGE_BAR_LENGTH, LCD_LARGE_BAR_GAP, LCD_LARGE_BAR_CORNER_RADIUS, TFT_BLACK);

  // hPa
  M5.Lcd.drawChar(240, 65, 'h', TFT_BLACK, bkground_color, 4);
  M5.Lcd.drawChar(265, 65, 'P', TFT_BLACK, bkground_color, 4);
  M5.Lcd.drawChar(285, 65, 'a', TFT_BLACK, bkground_color, 4);

  pressure_prev = pressure;
}

/**
 * 現在の角度で判断して画面を作成する
 */
void setDisplay(){

  // 別タスクで画面描画中の時は待つ
  while(is_drawing){
    delay(1000);
  }
  is_drawing = true;

  switch (currentAngle){
    case APP_STATE_0:
      // 時計
      displayDateTimeScreen();
      break;
    case APP_STATE_1:
      // 湿度計
      displayHumidityScreen();
      break;
    case APP_STATE_2:
      // 温度計
      displayThermometerScreen();
      break;
    case APP_STATE_3:
      displayPressScreen();
      break;
  }
  is_drawing = false;
  
}

/**
 * 画面表示用スレッド
 */
void displayTask(void* arg){
  while (1) {
    if (is_initialize == true || is_state_changed == false){
      // 画面表示 (is_state_changed=trueのときはいったん無視)
      setDisplay();
      is_initialize = false;
    }
    delay(1000); // Delay a second between loops.
  }
}

/**
 * setup
 */
void setup() {
  
  M5.begin(true,false,true);
  M5.Power.begin();
  
  //以下時計の角度計測のため必要
  M5.IMU.Init();
  
  Serial.println("Starting Arduino BLE Client application...");
  BLEDevice::init("");

  WiFi.mode(WIFI_MODE_STA);
  WiFi.begin(ssid, password);
  while (WiFi.waitForConnectResult() != WL_CONNECTED)
  {
    delay(100);
  }
  // init time setting
  configTime(gmt_offset_sec, daylight_offset_sec, ntp_server_1st, ntp_server_2nd);
  struct tm time_info;
  if (!getLocalTime(&time_info))
  {
    M5.Lcd.fillScreen(TFT_RED);
    delay(3000);
  }
  
  // 方向検知用スレッド
  xTaskCreatePinnedToCore(monitorAngleTask, "monitorAngleTask", 4096, NULL, 1, NULL, 0);

  // 画面表示用スレッド
  xTaskCreatePinnedToCore(displayTask, "displayTask", 4096, NULL, 1, NULL, 0);
  
} // End of setup.


/**
 * This is the Arduino main loop function.
 */
void loop() {
  BLEScan* pBLEScan = BLEDevice::getScan();
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pBLEScan->setActiveScan(true);
  pBLEScan->start(5);

  delay(1000); // Delay a second between loops.
} // End of loop

WiFi.begin(ssid, password);
SSIDとPASSWORDは環境に応じて値を設定してください。

f:id:toriko0413:20200221220503j:plain

大きな流れと工夫したところ

大きな流れとして、メインスレッドでBLE受信して、そのほかに方向検知用スレッドと画面表示用スレッドを動かすようにしました。

方向検知用スレッドで方向が変わったのを認識した後すぐに描画しており、そのあと画面表示用スレッドがかぶってしまうと描画がおかしなことになるので相互に制御を行いました。(is_drawingとis_state_changedのフラグ)

回転方向は回転途中に値が定まらないため、方向変更中の方向を保持して連続して方向が定まったときに角度変更確定とするようにしました。
ANGLE_DECISION_TIMEの値で何回連続して方向が定まったときに確定とするかの値を保持させるようにしました。もし画面がチラチラする場合はこの値を増やしたら良いかと思います。

参考にさせていただいた部分

画面にデジタル数字を表示させる関数
drawNumberVLong()
drawNumberNormal()
については、前述のこちらのQiita記事をまるっとそのまま使わせていただいています。ありがとうございます。

日時表示の画面を作る関数
displayDateTimeScreen()
温度の画面を作る関数
displayThermometerScreen()
も、ほぼこちらのQiita記事そのまま使わせていただいています。
ただし温度はBLEでオムロン環境センサから取得した値を表示しています。

オムロン環境センサからのセンサー値

温度の他、湿度・気圧の画面もオムロン環境センサからの値を表示させました。
↓ 過去記事参照
siroitori.hatenablog.com

日付の取得

日付の取得部分。Quiita記事では

const char *ntp_server_1st = "ntp.nict.jp";

とあったものをそのまま使ってたんですが、マイクロソフト信者の息子くんから下記に書き換えられました(;^ω^)

const char *ntp_server_1st = "time.windows.com";

どちらも正常に動作します。

余談ですが息子くんに教えてもらいましたけど、Windowsでもこれ設定するところあるんですね。そりゃそうかとは思いますが、全然知りませんでした。
f:id:toriko0413:20200221213655p:plain
Windowsで時刻サーバを設定する画面

さいごに

今回これを作ってみて、とても勉強になりました。
MPU9250の使い方とか、日付・時刻を取得するところとか。

そして地味にこれ超便利です。時計として!時刻ずれないし。当たり前ですけど。
今後もデスク用の時計として普段使いしながら、さらに開発していきたいと思います。

実はわたし先週から首と腕の痛みがなおらないなーとおもっていたら今日MRI検査で頸椎の椎間板ヘルニアということが判明・・・。
健康のありがたみを感じますね。痛みと闘いながら頑張ります^^

そうそう、M5Stack FIREはマイクがあるのでこれで何かやりたいな~とかも考えているところです。



スター・はてブとても嬉しいです

小6息子くんと陶芸体験でオリジナリティ溢れるものづくり

地域の子ども会行事で陶芸教室がありました。

息子くんはもちろん、私も子ども会役員やってる特権で一緒に作らせてもらいました。
めちゃくちゃ楽しかったです。
創作活動はいいですね。

息子くんの作品

土をこねこねして作っていきます。
(相棒のぬいぐるみのめめたん、陶芸製作中は汚れそうだから置いといてって言ったんだけど手放してくれなかった…)

おや、これは!

f:id:toriko0413:20200216213638j:plain

AppleMicrosoftの夢のコラボ皿でした!息子くんらしい!

「MADE BY APPLE MICROSOFT
だって(笑)

ポンポンポンポーン、Intel入ってる?

そういえば昔Microsoftモチーフではんだづけ練習してたの思い出しました。
siroitori.hatenablog.com

母の作品

いっぽう私の作った作品です。
お皿とかカップとかじゃつまらない!ということで、M5Stackのいれものを作ってみました。

f:id:toriko0413:20200216213609j:plain

焼きあがると2割ほど縮むらしいので、大きめに作ってみたけどどうだろう。
できあがって入らなかったらいやだな~。そのときはまた別の使い道考えなくちゃです。

現在開発中のM5Stackファービーを入れる場合、

上の方にもしゃもしゃをつけて下記のようなイメージになる予定です。

ファービーじゃない場合も、M5Stackをいい感じに飾れそう、かな?
あっ、ボタンが押せないという事実に今気が付いてしまいました・・Σ( ̄ロ ̄lll)
だけどM5Stackの底に何か挟んで高さを出せばボタン触れる位置に置けそうですね。

M5Stackのケースを3Dプリンタで自作することはあっても陶器で手作りする人はなかなかいないのでは?と自己満。

焼き上がり予定は3月末!
二つとも、うまく焼きあがるといいな~と楽しみにしています。



スター・はてブとても嬉しいです

小6息子くん身近な範囲でPC周りのサポートをはじめました

最近Windows7のサポートが終了しましたね。

でも、Windows7以降のOSをWindows10へアップデート無料でできるっていう事実、意外と知られてないです。
裏技でもなんでもなく、マイクロソフト公式サイトからできます。(「参考情報」参照)

ここ最近私のママ友界隈を中心に、パソコンを買い替えたという話を数件聞きました。
きけばWindows7はもう使えなくなるから、と。
スペックにもよりますが「あなたのパソコンまだ使えます」です。

ほかに、店員さんに勧められるまま結構な高額PC買ってるお知り合いもいました。
その方はネット閲覧が主な用途。

ママ友界隈はとくにPC周りに弱い人が過半数です。
今はネットの時代。調べればほとんどのことがわかりますが、読んでもどうしていいかわからない、そもそも調べ方がわからないともなると難しいですよね。
料理が苦手な私に「あるものでちゃちゃっと作って」といわれるくらいに難しいと思います。ガクブル!

そんな話を私から聞いたり身近な近所のお友達の家のパソコンを見てあげたりは前からちょくちょくやっている息子くん。
「そういうのを手助けしてあげよう!」
と言い始めました。

と書くと親切な子供やん!感じですが、というよりPCや周辺機器を触るのがとても好きなので(どこのメーカーのがどーのこーの言うのが好き)ただただ自分が色々なの触ってみたい。
そしてあわよくばお小遣いをもらってパーツ代に充てたいという魂胆です( ̄д ̄)

なるほど、それいいかも。
と、早速Facebookでつながっている顔見知りの知人をターゲットに拡散してみてやってみました。

PC周りのべんり屋さん

こんなことができます、とうたってみました。

  • Windows10へアップグレード
  • パソコンが動かない部分があるので見てほしい。(起動しない/ドライブが認識しない/変なエラーが出る)
  • 家に眠っている古いPCの使い道を教えてほしい。
  • ウイルスが入ってしまったので駆除してほしい。
  • パソコンを高速化したい!(HDD→SSD化、機材あるのでできます。メモリの増設もできます。)
  • その他ご相談に応じます。

早速いろいろな友達から連絡があり、相談で解決するレベルから作業依頼もいただきました。
息子くん喜んでいます!

コミュ障の息子くんの代わりに窓口は私ですが、対応は息子くんがすべて行っています。


↑ 全然関係ないけど先日家族旅行行って特急に乗っている息子くん。

これまでの実績と受付済み依頼内容

  • 【Yさん】中古PCをメンテナンスしてWindows10にして安価でお譲りしました。
  • 【Tさん】MP3プレイヤーの専用ソフトがおかしなエラーが続出するようになったと相談を受け解決しました。
  • 【Aさん】Windows7で使えなくなったと思いこんでいたPCを10にアップグレードしました。
  • 【Sさん】買った新しいPCに必要ソフトをセットアップして周辺機器もこちらで購入して動くように設定しました。(息子くんによる遠隔サポートもできるように設定済み)
  • 【Aさん】WiFiがすべての部屋に届かないため、中継器をセットアップしてすべての部屋に届くように設定しました。
  • 【Yさん】パソコンにウイルスが入ってるみたいということで対応しました。(「参考情報」参照)
  • 【Kさん】動画をDVDに焼く対応をしました。
  • 【Kさん】PC性能アップ見積もり(SSD化とメモリ増設)
  • 【息子くんのおばあちゃん】WiFiがつながらないことがあるということで調査しました。まさかのIPアドレスの競合が起こっていることがわかり対処しました。

どれひとつ写真がない…撮っておけばよかったです。

以下は、受付済みだけどPCが手元にないため作業未着手のもの

  • 【Yさん】古いPCのウイルス駆除
  • 【Nさん】Win7→Win10アップデートと初期化。

参考情報

Windows10へのアップデート

下記マイクロソフト公式サイトからアップデート可能ですよ。
www.microsoft.com

Yさんのウイルス

実績のところに書いたウイルスの話です。
息子くんの同級生のお友達(Yさんの息子さん)のPCの話で、実際に私は見てないうちに対応が終わっていたのであとで写真撮ればよかったーと思いました。

現象としては、PCにエロい広告がずっとでたままになる、と(´Д`)

息子くんに聞いた話では、この広告はGoogle Chromeの通知機能を使って出ていたそうです。

通知機能とは、Windowsのタスクバーの右端にある吹き出しのような絵のマーク(↓ 赤丸でか囲っている部分)
f:id:toriko0413:20200213234324p:plain

これをクリックすると、現在隠れている通知が表示されます。

f:id:toriko0413:20200213234336p:plain
↑ ここの赤丸で囲っているのがGoogle Chromeの通知ですね。この画面ではChromeの通知機能を介してFacebookが通知を出しています。

この通知はGoogle Chromeの設定でどの通知を出すか、またどんなふうに通知を出すかの設定ができ、これを利用した悪質な広告がPC上にずっと画面を開いて居座るということになっていたみたいです。(たぶん最初に「〇〇からの通知を許可しますか?」と尋ねられたはずです。「はい」ってやっちゃったんだと思われます。)

こんなときは、Chromeの設定から、[詳細設定] をクリックして[プライバシーとセキュリティ] → [サイトの設定] →[通知] から、通知を削除しましょう。

通知を利用したそんな悪質なのがあるんですねえ。びっくりしました。

それとはまた別に、そのPCにはウイルスが入っていたそうで駆除したって言ってました。それもびっくり。

さいごに

色々な人から必要とされるというのは嬉しいことです。
ずっと後ろめたい気持ちを抱えている不登校だからなおさら「きみのおかげで助かった」とか言われたら嬉しいんじゃないかなって思います。

私も産後のブランクの後パートで仕事再開したとき、社会から必要とされている感でとても幸せに感じたのを思い出します。

最近はOSいじりばかりしててあまりプログラミングというプログラミングをしてるところを見かけませんが、ハマっててすごい勢いでアプリを作っていた去年ごろまで息子くんは「役に立つアプリを作りたい」ってずっと言ってました。そういえば。
「実用的なもの」という意味もあるでしょうけど、人の役に立ちたい気持ちが割とあるのかなーと思いました。



スター・はてブとても嬉しいです

M5Stackでオムロンの環境センサから受信したデータを表示した

息子くん母が最近ドはまりしているM5Stackのことを書こうと思います。
(今回は息子くんの作品ではありません。)

M5Stack FIRE

先日紹介しましたが小型でかわいらしいデバイスです。
siroitori.hatenablog.com
この記事の下の方に

今現在、自宅にあるオムロンの環境センサから温度や湿度などの値をBLEで受信するコーディングをして表示することもできました。

と書いていて絶賛開発中です。

ひととおり完成したらブログ書こうかなと思ったのですが、つまづいたところも多くあり、書くと長くなりそう(もう書きたくなくなりそうな予感も…)だったのでとりあえず
オムロンの環境センサからデータを受信する話だけを書くことにしました。

f:id:toriko0413:20200208223331j:plain

オムロンの環境センサ

以前(2年前)、ラズパイで開発をしました。
siroitori.hatenablog.com
この記事の「優良賞をいただいた作品の紹介」から下の部分です。

このときは、ラズパイで値を受信しており、pythonでコーディングしていました。
今回はM5StackなのでArduinoでコーディングしました。

C++は業務で昔少しだけかじったことはあるのですが、人のコードをちょっといじって何かの値を変えるくらいの修正をするレベルで、自分でプログラム書いたことは皆無です!
C言語といえばポインタとかね。なんとなくの概念しかありません…。

でもこんな私でもできた!なんでもチャレンジですね。

プログラム

まずは参考になりそうなサイトを検索

しかし検索してもなかなか情報が無く。
M5Stackから送信するプログラムはたくさんヒットするのですが受信するものがあまりない?

① M5Stack →→Bluetooth→→ どこか
(M5Stack のサーバー用途)はあるが、
② どこか →→Bluetooth→→ M5Stack
(M5Stack のクライアント用途)がない。

Twitterの親切な方からESP32やセントラル/ペリフェラルとか入れて検索ワードを変えるといいよとアドバイスいただきました!感謝です。

ESP32
 M5Stackは内部でESP32という名前のマイコンが入っています。これは今回いろいろ検索しながら知りました。
セントラル/ペリフェラル
 初耳でした。上記の①がペリフェラルで②がセントラル、になります。いいこと聞きました!

そしてネットの海から情報を検索します。
できれば、オムロンの環境センサからデータを受信するプログラムの情報が載ったサイトがあれば望ましいのですが…

M5StackでBLE環境センサー端末を作る – Ambient
これは逆方向ですね。(先にこれ見てたのにセントラル・プリフェラルって用語スルーしてることに後で気が付く)

あっ!ありました!やりたいのこれ。
qiita.com

そっくりそのまま動かしてみる

上記公開されているプログラムで、環境センサのMACアドレスを定義している部分

static String omronSensorAdress = "ff:f7:ef:33:f4:31";

だけ自分のものに書き換えて実行してみました。

すると・・・
あれれ、コネクトでうまくいかない。うーんうーん。
(本当はコネクトの前段階でもArduino初心者のためわからなくて試行錯誤してたんですが何につまづいていたのか忘れてしまいました…)

ブロードキャストモードの仕様

原因は、環境センサの設定でした。

オムロンの環境センサには、モードが2種類あります。
f:id:toriko0413:20200208213402j:plain
https://ambidata.io/samples/m5stack/m5stack_ble_sensor/ より引用)

初期時には「コネクトモード」になっているのですが、
以前ラズパイからpythonで取得していた時に「ブロードキャストモード」に変えてたんでした!そういえば!

この図からわかるように、ブロードキャストモードではコネクトする必要がありません。
アドバタイズ時に一気にセンサーのデータが取得できます!

上記プログラムのonResultの中の

Serial.println(advertisedDevice.toString().c_str());

では、ブロードキャストモードの時以下のような値が取得されます。

Name: , Address: fd:e5:99:72:2c:29, manufacturer data: d502d77207a514990002000628c80d9600affffe26a2

あ、そうそう、Serial.printの値もどこに出てくるのかわからず悩んでたんだった。
Arduinoの[ツール]→[シリアルモニタ]で画面を出すことを知りました。
息子くんに訪ねて一発解決^^;
ほんとそんなとこから!?って感じですよ。

そして話を戻して、この取得されたmanufacturer data: 以降の値に環境センサのデータが埋まってるんですねぇ!

データの仕様は以下
f:id:toriko0413:20200208214807j:plain

行番号以降のデータについては、「ブロードキャストモード」ではどうやら以下ではなく、
f:id:toriko0413:20200208214702p:plain

こちらを見る模様。
f:id:toriko0413:20200208220103p:plain

上記仕様にあてはめて、
d502d77207a514990002000628c80d9600affffe26a2
だと、下記のように分割して解釈し10進数に変換します。
d502 :カンパニーID(固定)
d7 :シーケンス番号 → [dec] 215
7207 :温度 → [dec] 1906 → 19.6℃
a514 :湿度 → [dec] 5285 → 52.85%
9900 :照度 → [dec] 153
0200 :UV → [dec] 2
0628 :気圧 → [dec] 10246
c80d :騒音 → [dec] 3528
9600 :加速度X → [dec] 150
afff :加速度Y → [dec] 65455
fe26 :加速度Z → [dec] 9982
a2 :電池 → [dec] 162

これは同じことを2年前にpythonで書いてたから、pythonのコードとマニュアルを読み返してネットも調べてなんとか思い出しました。
こうやって思い出せないからブログに書いとくの大事ですね^^;;

Arduinoで実装

慣れないArduinoのコード。
もっと良い書き方がありそうな気がしていますが…
そしてこれは定数にするべきでしょうっていう箇所も見られますが…
(私がコードレビューのレビュワーなら一番に指摘するでしょう)

とりあえず、恥を忍んで公開します。

#include "BLEDevice.h"
#include <M5Stack.h>

static String omronSensorAdress = "fd:e5:99:72:2c:29";
static BLEUUID serviceUUID("0c4c3000-7700-46f4-aa96-d5e974e32a54");
static BLEUUID    charUUID("0c4C3001-7700-46F4-AA96-D5E974E32A54");

static BLEAddress *pServerAddress;
static boolean doConnect = false;
static BLERemoteCharacteristic* pRemoteCharacteristic;

long getParamFromAdvertisedData(std::string advertisedDataString, int pos, int length, String paramName) {
  // 38桁の16進表記文字列から対象のパラメータ位置で抜き出して10進数に変換して戻す
  std::string substrData = "";
  String manufData = "";
  Serial.print(paramName);
  Serial.print(": [hex] ");
  M5.Lcd.print(paramName);
  substrData = advertisedDataString.substr(pos, length); // 指定位置から指定桁数抜き出す
  Serial.print(substrData.c_str());

  if (length > 2){
    // 2桁以上(今回は4桁しかない)のときは反対にする 例)7d08→087d
    for(int i=length-2; i>=0; i=i-2){
      manufData.concat(substrData.substr(i, 2).c_str());  
    }
  }else{
    manufData = substrData.c_str();
  }

  // 16進数→10進数変換
  long ret = strtol(manufData.c_str(), NULL, 16);
  Serial.print(" [dec] ");
  Serial.println(ret);
  M5.Lcd.print(": ");
  M5.Lcd.println(ret);
  
  return ret;
}

/**
 * Scan for BLE servers and find the first one that advertises the service we are looking for.
 */
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks {
 /**
   * Called for each advertising BLE server.
   */
  void onResult(BLEAdvertisedDevice advertisedDevice) {
    BLEScan* pBLEScan = BLEDevice::getScan();
    Serial.println("BLE Advertised Device found: ");
    std::string advertisedDataString = advertisedDevice.toString().c_str();
    Serial.println(advertisedDataString.c_str());  // このなかの文字列 manufacturer data:d502のあとがデータになる

    String deviceAddress = advertisedDevice.getAddress().toString().c_str();
    Serial.println(deviceAddress.c_str());
    //ペリフェラルをアドレスで判断
    if(deviceAddress.equalsIgnoreCase(omronSensorAdress)){

      int manufPos = advertisedDataString.find("manufacturer data: d502");
      Serial.print("manufPos: ");
      Serial.println(manufPos);

      M5.Lcd.fillScreen(WHITE);
      M5.Lcd.setCursor(0,0);

      long seq = getParamFromAdvertisedData(advertisedDataString, manufPos+23, 2, "seq");
      
      long temp = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2, 4, "temp");
      long humid = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4, 4, "humid");
      long light = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4+4, 4, "light");
      long uv = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4+4+4, 4, "uv");
      long press = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4+4+4+4, 4, "press");
      long noise = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4+4+4+4+4, 4, "noise");
      long accelX = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4+4+4+4+4+4, 4, "accelX");
      long accelY = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4+4+4+4+4+4+4, 4, "accelY");
      long accelZ = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4+4+4+4+4+4+4+4, 4, "accelZ");
      long batt = getParamFromAdvertisedData(advertisedDataString, manufPos+23+2+4+4+4+4+4+4+4+4+4, 2, "batt");
      
     doConnect = true;
    }// Found our server
  } // onResult
}; // MyAdvertisedDeviceCallbacks

void setup() {
  Serial.begin(115200);
  M5.begin(true,false,true);
  M5.Lcd.fillScreen(WHITE);
  M5.Lcd.setTextSize(2);
  Serial.println("Step 0");
  Serial.println("Starting Arduino BLE Client application...");
  BLEDevice::init("");

} // End of setup.


// This is the Arduino main loop function.
void loop() {
  BLEScan* pBLEScan = BLEDevice::getScan();
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pBLEScan->setActiveScan(true);
  pBLEScan->start(5);

  delay(1000); // Delay a second between loops.
} // End of loop


まとめ

まともにCやったことないハード系はちんぷんかんぷんな私でも頑張って実装することができました。
できあがると超うれしいですね。

今現在はこれをまだ改造していっているのですが、続きもおいおい書いていこうと思います。
それと並行してM5Stackでファービーを作ろうとしているので、それも書こうかなと思っています。

作りながらまだよくわからない部分もありながら書いているところもあり、よく勉強しないといけないなって思っています。

毎日かなり開発に意欲的な息子くんから刺激をうけてはじめたArduinoな世界です。
息子くんに感謝をするとともに、
息子には負けてられん!
という気持ちでこれからも頑張ります^^



スター・はてブとても嬉しいです

小6息子くんRobloxで乗っ取り被害にあいゲーム内通貨を奪われ、返してもらうまでのいきさつ

衝撃のタイトルでございます!

先日、息子くんRobloxで乗っ取り被害にあっていた模様。
ゲーム内通貨であるRobux(ロバックス)をありったけ持っていかれました。。
でも、息子くん自力でカスタマーサポートと交渉し無事に取り返すことが出来ました!
良かったねー!

その経緯について書こうと思います。

ロバックスについて

ブロックスにログインすると、右上の端の方に設定の歯車アイコンがあるんですがその隣に出ている数字が、今持っているゲーム内通貨ロバックスの額。

↓ これは私がログインした画面。課金も何もしていないので「0」と表示されています。
f:id:toriko0413:20200203212507p:plain

息子くんのアカウントは年間会費払って「ビルダーズクラブ」というものに加入しているので定期的に月1回ロバックスが貰えてるんです。
(ビルダーズクラブって言葉、何度聞いてもマッチョマンの集団思い浮かべてプッてなるんですけどーw)
ビルダーズクラブの特典はほかにもいくつかあるらしいです。

しかーし、今はもうビルダーズクラブは無くなってて、代わりに「Roblox Premium」(ロブロックスプレミアム)というものになってるみたいです。

f:id:toriko0413:20200203214713p:plain

年会費会員っていうのはなくなったみたいですね。

1月31日(金) ロバックスがなくなっていることに気づく

いつものようにロブロックスで遊んでいたら、ふと手持ちのロバックスが0になってたことに気づいたそうです。
履歴を見ると、知らないアイテムが購入されています。
日付は昨日。しかも、手持ちの額全部1770ロバックス

f:id:toriko0413:20200203213845p:plain

そんな残高ちょうどの額のアイテムを間違って購入したとは思えない。怪しい、怪しすぎる!!

f:id:toriko0413:20200203224146p:plain

しかも何このいけてない服!こんなの1770ロバックスも出して買うわけないじゃない!
というわけでこれはやっぱり盗難です。

息子くんから
「乗っ取りでロバックス盗まれた・・
でも詐欺でカスタマーサポートに連絡すればもどってくるかもしれないからやってみる」
と報告を受けました。

Support - Roblox
↑ ここから問い合わせを行ったそうです。英語で。

この記事書くのに今書きながら息子くんに聞きながら私がまとめてるんですけど、この問い合わせページどこから行くか難しすぎ。。
メインの画面からはたどれないようでした。

またそれと同時に二段階認証アカウントPINの設定を行ってセキュリティ対策を強くしていました。

  • 二段階認証

en.help.roblox.com

  • アカウントPINの設定

en.help.roblox.com

もう乗っ取らせないぞ!

2月2日(日) カスタマーサポートから返信

問い合わせから2日後の早朝5時に最初の返事を受信しました。

要約すると、

  1. セキュリティの設定(二段階認証・アカウントPIN)やってくださいよ
  2. アカウント(ロバックス)の復元、やっても1回限りですよ
  3. 被害にあった状況をスクリーンショット撮って送ってくださいよ

ということでした。

セキュリティの設定は、初日にやったのでOK。
スクリーンショットを撮って送りました。

送ってすぐその日に返信があり、スクリーンショットのどのアイテムがその被害のものかわからないのでマークをつけてくれとのこと。
すぐに印をつけた画像を返信していました。

その日の夕方もう一度メールを受信。
審査するので1~10営業日待ってください、とのこと。

2月4日(月)ロバックスが戻ってきた!

1,770のRobuxがあなたのアカウントに復元されました。
というメッセージで、無事1770ロバックスが戻ってきました!
わー!ぱちぱち。

↓ 右上に1,770と表示されました♪
f:id:toriko0413:20200203230706j:plain

ただ、メッセージ内に「1回だけです。セキュリティしっかりしてね」と念押しが。

そして気になる記載が!

If you are using Chrome, please take a moment to check for and remove any Roblox related extensions. We also recommend checking and removing all browser extensions. Some browser extensions can steal login information or cookies and this allows someone else to access your account. We recommend not using any browser add-ons or extensions unless you are 100% sure they are from trusted sources.

Google翻訳

Chromeを使用している場合は、Roblox関連の拡張機能を確認して削除してください。すべてのブラウザ拡張機能を確認して削除することもお勧めします。一部のブラウザー拡張機能は、ログイン情報またはCookieを盗む可能性があり、これにより他のユーザーがアカウントにアクセスできます。信頼できるソースからのものであることを100%確信がない限り、ブラウザーのアドオンや拡張機能を使用しないことをお勧めします。

これは…。Cookieのセッション情報を読み取れば簡単にハッキングできるということですね。

息子くんは実は知ってたそうです。
実際にログイン中のブラウザのCookie情報からセッションID抜き出して別のPCからアクセスしてみたら、ログインせずに簡単に自分のアカウントに入れたそうで。
セキュリティ甘いんだよね~ロブロックスって~、って。

『2/4追記』
よく調べました。
このセッション管理方式は一般的なんですね。
対策をしてないこちら側が悪い。
私これでもWEBシステム開発者なんですけど…いつも誰かが作ったセッション管理のベースの上での作業しかしてないので深くまで知らなかったです。バカチンですね~。→ CookieのセッションIDを移植するだけでは乗っ取れない技術的な仕組みがあるのだろうと思っていました。
勝手な情報を配信してしまって、しかもロブロックスディスるようなこと?書いてごめんなさい!!
Cookieの情報を盗まれないように十分対策をとることが重要ですね。
また盗まれても大丈夫なように、他のはじめての端末からのアクセス時には確認が必要になる二段階認証の対策をする。
息子くんともしっかり情報を共有しました。

セキュリティ対策はしっかりしてないといけないね。

今回はたかだかゲーム内通貨の話ですが、こういうのって「まさか自分が」ってなりますよね。
私もいろいろ気をつけていこ~と思いました。



スター・はてブとても嬉しいです

小6息子くんRobloxで出会いがしらの車衝突シミュレーションを作った

久しぶりに息子くんがロブロックスのゲームを公開しました。

ブロックスとは

ネットゲームで、遊ぶことはもちろん開発環境も公開されてるのでゴリゴリ作ることも出来ます。

↓ 過去記事。
siroitori.hatenablog.com

ブロックスに出会ったのは1年とちょっと前ですね。時間のたつのは早いものです。

今回作ったのは車のシミュレーション

あまりゲームというゲームではないのですが、車が出会いがしらで衝突するシミュレーションを作っていました。
この車は後述しますが、公開されているモデルを改造して衝撃によって煙を出してバラバラになるものを作って使っています。

遊び方

こちらのリンクから遊べます。PC・スマホどちらでもいけるようにしています。
www.roblox.com

f:id:toriko0413:20200131212635p:plain

ワールドに入ると、ビルの上のほうにいます。

f:id:toriko0413:20200131212847p:plain

ビルから降りてもいいのですが、ここではちょっと視点だけ変えてみます。

f:id:toriko0413:20200131213004p:plain
f:id:toriko0413:20200131213022p:plain

画面左の方と中央に車がいますね。この2台が衝突事故しちゃいます。

【ボタン説明】

  • Start ボタン … 車が走り出して衝突します
  • Reset ボタン … 最初の位置に車がリセットされます

Start してみると、こんな風になります。

やるたびに車が壊れたり壊れなかったりと結果が違うのでいろいろやってみてください。
またいろいろな方向から衝突に遭遇し事故の目撃者となりましょう!

なんていうかただそれだけのゲームなんですが、、、
(いやゲームと呼んではいけないシミュレーションだ!と息子くんに言われました)

その他の遊び方としては、

  • 道路に降りてみて車にはねられてみる
  • 車に乘って操作することで衝突を回避する

などの遊び方ができます。

モジュール解説

今回クラッシュする車を作った

クラッシュタイプの車です。(蒟蒻畑じゃないよ!)
車のモデル自体は公開されているものを撮ってきただけなんですが、それを今回壁や物に追突したりして衝撃をうけたら黒煙を出してダメージが蓄積したらばらっばらになってしまうという改造をしたそうです。

ライブラリ公開しています

Roblox Studioで作成するゲームに組み込んむことが出来るようにライブラリとしても公開してますので、開発者の方どうぞ使ってみてください。コードの内部が覗けます。

↓ ここから入手できます。「Crash Script」
www.roblox.com

ライブラリの導入方法

まず、今回作成した「Crash Script」をリンクから取得します。
Crash Script - Roblox

f:id:toriko0413:20200131220716p:plain
「ゲットする」クリック

f:id:toriko0413:20200131220737p:plain
「今すぐゲット」

それが終わったら、Roblox Studioを起動してテンプレートを選択します。
f:id:toriko0413:20200131174859p:plain
ここでは「Suburban」を選択してみました。
(中にはスクリプトが組み込めないテンプレートもあるそうですのでご注意を)

f:id:toriko0413:20200131220308p:plain

Roblox Studioの画面左のほうの「Toolbox」から■が4つのマークのタブを選択すると、先ほどゲットした「Crash Script」が表示されているので、それをドラッグして中央のウインドウのどこか好きな場所に置きます。
f:id:toriko0413:20200131220532p:plain

これでゲーム内にクラッシュの車が配置できました。
乘ってみてください。壁や障害物にぶつかったらクラッシュします。


クラッシュする車に追加したスクリプト

ちなみにクラッシュする車の内部はこのようになっています。
息子くんの書いたコードBrokeScriptというスクリプトで、この中の全部自分で書いたものだそうです。

local Speed

function InsertBrokeScript(Object)
	for _,part in pairs(Object:GetChildren()) do
		if part:IsA("BasePart") then
			if part.Parent == script.Parent.PassParts then else
				local Clone = script:Clone()
				Clone.Disabled = true
				Clone.Parent = part
				Clone.Speed.Value = Speed
				Clone.IsClone.Value = true
				Clone.Disabled = false
			end
		else
			InsertBrokeScript(part)
		end
	end
end

if not script.IsClone.Value then
	print("Not clone")
	Speed = Instance.new("IntValue")
	Speed.Name = "Speed"
	Speed.Parent = script.Parent
	
	InsertBrokeScript(script.Parent)
	
	while wait(0.1) do
		Speed.Value = script.Parent.PrimaryPart.Velocity.Magnitude
	end
end

if script.IsClone.Value then
	Damage = Instance.new("IntValue")
	Damage.Parent = script
	Damage.Name = "Damage"
	Damage.Value = 0
	script.Parent.Touched:Connect(function(part)
		if game.Players:GetPlayerFromCharacter(part.Parent) == nil and game.Players:GetPlayerFromCharacter(part.Parent.Parent) == nil then
			Damage.Value = Damage.Value + script.Speed.Value.Value / 10
			print(script.Parent:GetFullName() .. " has damaged.")
			if Damage.Value >= 5 then
				if script:FindFirstChild("Smoke") ~= nil then
					script.Smoke.Parent = script.Parent
					script.Parent.Smoke.Enabled = true
				end
			end
			if Damage.Value >= 10 then
				script.Parent:ClearAllChildren()
			end
		else
			print("Skipped, Debug: " .. tostring(game.Players:GetPlayerFromCharacter(part.Parent)) .. tostring(game.Players:GetPlayerFromCharacter(part.Parent.Parent)))
		end
	end)
end

えーと、どういう処理をやっているのか聞いたところによると、
前半部分では、このスクリプトを車を構成するすべてのパーツにクローンして関連付けさせていて、
後半部分では、車へのダメージをカウントしていると。

ダメージ5以上だと煙を出して、10以上だと
script.Parent:ClearAllChildren()
ってやってますね。
すべてのパーツの親子関係を解消させてるようです。
要は接続している状態でなくなるということなので例えば車が車体から外れ、全部バラバラになっちゃう感じなのでしょう。

さいごに

最近OSいじりにハマってあまり作品作りというものをしていない息子くんでしたが、久々に紹介できるものを作ってくれたので紹介しました。
私の最近ハマっているM5Stackで試行錯誤して作っている隣で、息子くんはこれをさくさくっと作ってました。(がM5Stackも気になるようでちょいちょい覗かれてました)

前回は親子ペアプログラミングと書きましたが、今日は親子もくもく会でしたw

OSのほうもなかなかディープなことやってるっぽいんですがなにせ私の理解が追いつかないのでブログで説明ができないという・・・

これからも親子がんばります。

Roblox関連記事

以前公開したライブラリ
siroitori.hatenablog.com

これまでに作ったゲーム
siroitori.hatenablog.com
siroitori.hatenablog.com
siroitori.hatenablog.com

Robloxのすべての記事はこちら
siroitori.hatenablog.com



スター・はてブとても嬉しいです!