電子工作」カテゴリーアーカイブ

WindowsからI2Cデバイスを利用

I/F ボードとしてCH341TまたはFT232Hを利用する。

Raspberry piやESPシリーズなどの組み込みシステムの制御に適したマイコンには、GPIO,I2CやSPIなどのポートが備わってるのが特徴の一つです。これらのI/Fを通じて様々なセンサー(例えば温度、湿度、気圧など)や小型表示器、PWM制御器などに接続できます。

一方、WindowsPCなどに広く使われているx86には、残念ながらセンサーなどのハードウェアを直接制御可能なポートがありません。 PCにはUSBポートがありますが、このUSBポートからシリアルやI2Cへ変換するハードウェアを探してみたらCH341Tがamazonなどで安価に販売されていました。

そこで、USBからCH341TとFT232H経由で、次のような機能を試してみました。
(1)AHT20で温度、湿度のデータを取得
(2)OLEDディスプレイSSD1306へ文字表示
(3)PCA9685でサーボモータを駆動

https://github.com/crescentvenus/USBtoI2C

NotebookLMで音声による解説

Power Pins
5V – this is the 5V power from the USB input.
GND – this is the common ground for all power and logic.
3V power output – The new version has a 3.3V power output pin for up to 500mA
GPIO Pins
D4 to D7 – can be used as either digital inputs or outputs.
C0 to C7 – can be used as either digital inputs or outputs.
I2C Pins
SCL – the I2C clock signal is on D0.
SDA – the I2C data is on D1+D2.
I2C switch – The new version has a switch that connects D1 and D2 for easy I2C interfacing. Move the switch to ON to use I2C and/or the STEMMA QT connector. You can then use either D1 or D2 for SDA.
On the original version only: Note that there are two pins (D1 and D2) which must be tied together and treated as one to use I2C.

SPI Pins
SCLK – the SPI clock signal is on D0.
MOSI – Microcontroller Out, Serial In is on D1.
MISO – Microcontroller In, Serial Out is on D2.
CS0 – Chip Select is on D3. This is not used by Blinka, instead use one of the GPIO pins from above (see example section).

ST7789ピン FT232Hピン 説明

VCC 3.3V 電源(3.3V推奨、5Vは不可の場合あり)

GND   GND グランド

SCL    D0 (SCLK)

SPIクロック信号  SDA D1 (MOSI) SPIデータ入力(Master Out Slave In)

RES     D5 リセットピン

DC     D6 データ/コマンド選択ピン

BLK     3.3VまたはD7 バックライト(オプション、3.3VまたはGPIOで制御)

CircuitPython Libraries on any Computer with FT232H

https://learn.adafruit.com/circuitpython-on-any-computer-with-ft232h/windows

GPIOサンプル

OUTPUT サンプル
import time
import board
import digitalio

led = digitalio.DigitalInOut(board.C0)
led.direction = digitalio.Direction.OUTPUT

while True:
led.value = True
time.sleep(0.5)
led.value = False
time.sleep(0.5)

INPUT サンプル
import board
import digitalio

led = digitalio.DigitalInOut(board.C0)
led.direction = digitalio.Direction.OUTPUT

button = digitalio.DigitalInOut(board.C1)
button.direction = digitalio.Direction.INPUT

while True:
led.value = button.value

I2Cサンプル
# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries
# SPDX-License-Identifier: MIT

import time

import board

from adafruit_bme280 import basic as adafruit_bme280

# Create sensor object, using the board’s default I2C bus.
i2c = board.I2C() # uses board.SCL and board.SDA
# i2c = board.STEMMA_I2C() # For using the built-in STEMMA QT connector on a microcontroller
bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c)

# OR create sensor object, using the board’s default SPI bus.
# import digitalio
# spi = board.SPI()
# bme_cs = digitalio.DigitalInOut(board.D10)
# bme280 = adafruit_bme280.Adafruit_BME280_SPI(spi, bme_cs)

# change this to match the location’s pressure (hPa) at sea level
bme280.sea_level_pressure = 1013.25

while True:
print(“\nTemperature: %0.1f C” % bme280.temperature)
print(“Humidity: %0.1f %%” % bme280.relative_humidity)
print(“Pressure: %0.1f hPa” % bme280.pressure)
print(“Altitude = %0.2f meters” % bme280.altitude)
time.sleep(2)

SPIサンプル
import board
import digitalio
import adafruit_rgb_display.ili9341 as ili9341
from PIL import Image

# Setup display
cs_pin = digitalio.DigitalInOut(board.C0)
dc_pin = digitalio.DigitalInOut(board.C1)
disp = ili9341.ILI9341(board.SPI(), cs=cs_pin, dc=dc_pin, baudrate=64000000)

# Load image and convert to RGB
image = Image.open(‘blinka.bmp’).convert(‘RGB’)

# Display it (rotated by 90 deg)
disp.image(image, 90)


influxdbのデータを削除

$ influx
Connected to http://localhost:8086 version 1.8.10
InfluxDB shell version: 1.8.10
> show databases
name: databases
name
----
_internal
iot01
munin
> use iot01
Using database iot01

> show tag keys
name: ESP32
tagKey
------
device
name: measurements
tagKey
------
device

name: pi2B
tagKey
------
host
place
.........
.........

>DROP SERIES FROM ESP32

atom pico

https://qiita.com/Ninagawa123/items/df3930cdecb2e1500f96


STAMP PICO	USB-SERIAL
3v3	        3v3/Vcc※1
Tx0	        RX
Rx0	        TX
EN	        DTR(RTSと接続が正解?)
0	        GND※2
GND	        GND
※2 書き込み時はSTAMP PICOの0と書いてあるピンをGNDに接地させる。

#define BUTTON_PIN   39 // atom nano の内蔵PINは39
#define PIXEL_PIN    27  // Digital IO pin connected to the NeoPixels.
#define PIXEL_COUNT 16  // Number of NeoPixels

Weather StationのWiFi化

  • センサーの方式確認
    • 風向  2個のホール素子
    • 風速  回転するとON/OFFするスイッチ
    • 雨量  調査中(ベランダの庇があって正確な測定は困難なので、必須としない)
    • 気温と湿度は、BME280に置き換える
  • コントローラ(CPU)
    • ESP8266
  • データ転送
    • influxDB clientと、ESP8266のWiFi機能を利用

jetson nanoでcv2(cuda有効化)

ソースからインストール

python3でcv2をimportすると crashしてコアーダンプ。

問題の解決:export OPENBLAS_CORETYPE=ARMV8

こちらを参照して解決。Jetson NanoのPython3環境でIllegal instruction (cpre dumped)

処理速度の比較測定:約3倍

$ sudo nvpmodel -m 0
$ sudo jetson_clocks
$ python3 opencv_cuda.py
CPU = 2.7655137538909913[msec]
GPU = 1.0501614570617677[msec]
1
$ python3 opencv_cuda.py
CPU = 2.7816075325012206[msec]
GPU = 0.9869620561599731[msec]
1

opencv_cuda.py

import sys
import time
import cv2

### VALUES
NUM_REPEAT = 10000

### Read source image
img_src = cv2.imread("resource/lena.jpg")
cv2.imshow('img_src', img_src)


### Run with CPU
time_start = time.time()
for i in range (NUM_REPEAT):
    img_dst = cv2.resize(img_src, (300, 300))
time_end = time.time()
print ("CPU = {0}".format((time_end - time_start) * 1000 / NUM_REPEAT) + "[msec]")
cv2.imshow('CPU', img_dst)


### Run with GPU
img_gpu_src = cv2.cuda_GpuMat() # Allocate device memory only once, as memory allocation seems to take time...
img_gpu_dst = cv2.cuda_GpuMat()
time_start = time.time()
for i in range (NUM_REPEAT):
    img_gpu_src.upload(img_src)
    img_gpu_dst = cv2.cuda.resize(img_gpu_src, (300, 300))
    img_dst = img_gpu_dst.download()
time_end = time.time()
print ("GPU = {0}".format((time_end - time_start) * 1000 / NUM_REPEAT) + "[msec]")
cv2.imshow('GPU', img_dst)


key = cv2.waitKey(0)
cv2.destroyAllWindows()

print(cv2.cuda.getCudaEnabledDeviceCount())

cudaで利用できる機能を表示してみる

import cv2
cv2.__version__
dir(cv2.cuda)
['ALPHA_ATOP',
 'ALPHA_ATOP_PREMUL',
 'ALPHA_IN',
 'ALPHA_IN_PREMUL',
 'ALPHA_OUT',
 'ALPHA_OUT_PREMUL',
 'ALPHA_OVER',
 'ALPHA_OVER_PREMUL',
 'ALPHA_PLUS',
 'ALPHA_PLUS_PREMUL',
 'ALPHA_PREMUL',
 'ALPHA_XOR',
 'ALPHA_XOR_PREMUL',
 'BroxOpticalFlow_create',
 'COLOR_BAYER_BG2BGR_MHT',
 'COLOR_BAYER_BG2GRAY_MHT',
 'COLOR_BAYER_BG2RGB_MHT',
 'COLOR_BAYER_GB2BGR_MHT',
 'COLOR_BAYER_GB2GRAY_MHT',
 'COLOR_BAYER_GB2RGB_MHT',
 'COLOR_BAYER_GR2BGR_MHT',
 'COLOR_BAYER_GR2GRAY_MHT',
 'COLOR_BAYER_GR2RGB_MHT',
 'COLOR_BAYER_RG2BGR_MHT',
 'COLOR_BAYER_RG2GRAY_MHT',
 'COLOR_BAYER_RG2RGB_MHT',
 'COLOR_BayerBG2BGR_MHT',
 'COLOR_BayerBG2GRAY_MHT',
 'COLOR_BayerBG2RGB_MHT',
 'COLOR_BayerGB2BGR_MHT',
 'COLOR_BayerGB2GRAY_MHT',
 'COLOR_BayerGB2RGB_MHT',
 'COLOR_BayerGR2BGR_MHT',
 'COLOR_BayerGR2GRAY_MHT',
 'COLOR_BayerGR2RGB_MHT',
 'COLOR_BayerRG2BGR_MHT',
 'COLOR_BayerRG2GRAY_MHT',
 'COLOR_BayerRG2RGB_MHT',
 'CascadeClassifier_create',
 'DEVICE_INFO_COMPUTE_MODE_DEFAULT',
 'DEVICE_INFO_COMPUTE_MODE_EXCLUSIVE',
 'DEVICE_INFO_COMPUTE_MODE_EXCLUSIVE_PROCESS',
 'DEVICE_INFO_COMPUTE_MODE_PROHIBITED',
 'DYNAMIC_PARALLELISM',
 'DensePyrLKOpticalFlow_create',
 'DescriptorMatcher_createBFMatcher',
 'DeviceInfo_ComputeModeDefault',
 'DeviceInfo_ComputeModeExclusive',
 'DeviceInfo_ComputeModeExclusiveProcess',
 'DeviceInfo_ComputeModeProhibited',
 'EVENT_BLOCKING_SYNC',
 'EVENT_DEFAULT',
 'EVENT_DISABLE_TIMING',
 'EVENT_INTERPROCESS',
 'Event_BLOCKING_SYNC',
 'Event_DEFAULT',
 'Event_DISABLE_TIMING',
 'Event_INTERPROCESS',
 'Event_elapsedTime',
 'FEATURE_SET_COMPUTE_10',
 'FEATURE_SET_COMPUTE_11',
 'FEATURE_SET_COMPUTE_12',
 'FEATURE_SET_COMPUTE_13',
 'FEATURE_SET_COMPUTE_20',
 'FEATURE_SET_COMPUTE_21',
 'FEATURE_SET_COMPUTE_30',
 'FEATURE_SET_COMPUTE_32',
 'FEATURE_SET_COMPUTE_35',
 'FEATURE_SET_COMPUTE_50',
 'FarnebackOpticalFlow_create',
 'FastFeatureDetector_create',
 'GLOBAL_ATOMICS',
 'GpuMat_defaultAllocator',
 'GpuMat_setDefaultAllocator',
 'HOG_create',
 'HOST_MEM_PAGE_LOCKED',
 'HOST_MEM_SHARED',
 'HOST_MEM_WRITE_COMBINED',
 'HostMem_PAGE_LOCKED',
 'HostMem_SHARED',
 'HostMem_WRITE_COMBINED',
 'NATIVE_DOUBLE',
 'NVIDIA_OPTICAL_FLOW_1_0_NV_OF_PERF_LEVEL_FAST',
 'NVIDIA_OPTICAL_FLOW_1_0_NV_OF_PERF_LEVEL_MAX',
 'NVIDIA_OPTICAL_FLOW_1_0_NV_OF_PERF_LEVEL_MEDIUM',
 'NVIDIA_OPTICAL_FLOW_1_0_NV_OF_PERF_LEVEL_SLOW',
 'NVIDIA_OPTICAL_FLOW_1_0_NV_OF_PERF_LEVEL_UNDEFINED',
 'NvidiaOpticalFlow_1_0_NV_OF_PERF_LEVEL_FAST',
 'NvidiaOpticalFlow_1_0_NV_OF_PERF_LEVEL_MAX',
 'NvidiaOpticalFlow_1_0_NV_OF_PERF_LEVEL_MEDIUM',
 'NvidiaOpticalFlow_1_0_NV_OF_PERF_LEVEL_SLOW',
 'NvidiaOpticalFlow_1_0_NV_OF_PERF_LEVEL_UNDEFINED',
 'NvidiaOpticalFlow_1_0_create',
 'ORB_create',
 'OpticalFlowDual_TVL1_create',
 'SHARED_ATOMICS',
 'SURF_CUDA_ANGLE_ROW',
 'SURF_CUDA_HESSIAN_ROW',
 'SURF_CUDA_LAPLACIAN_ROW',
 'SURF_CUDA_OCTAVE_ROW',
 'SURF_CUDA_ROWS_COUNT',
 'SURF_CUDA_SIZE_ROW',
 'SURF_CUDA_X_ROW',
 'SURF_CUDA_Y_ROW',
 'SparsePyrLKOpticalFlow_create',
 'StereoBeliefPropagation_estimateRecommendedParams',
 'StereoConstantSpaceBP_estimateRecommendedParams',
 'Stream_Null',
 'TargetArchs_has',
 'TargetArchs_hasBin',
 'TargetArchs_hasEqualOrGreater',
 'TargetArchs_hasEqualOrGreaterBin',
 'TargetArchs_hasEqualOrGreaterPtx',
 'TargetArchs_hasEqualOrLessPtx',
 'TargetArchs_hasPtx',
 'WARP_SHUFFLE_FUNCTIONS',
 '__doc__',
 '__loader__',
 '__name__',
 '__package__',
 '__spec__',
 'abs',
 'absSum',
 'absdiff',
 'add',
 'addWeighted',
 'alphaComp',
 'bilateralFilter',
 'bitwise_and',
 'bitwise_not',
 'bitwise_or',
 'bitwise_xor',
 'blendLinear',
 'buildWarpAffineMaps',
 'buildWarpPerspectiveMaps',
 'calcAbsSum',
 'calcHist',
 'calcNorm',
 'calcNormDiff',
 'calcSqrSum',
 'calcSum',
 'cartToPolar',
 'compare',
 'copyMakeBorder',
 'countNonZero',
 'createBackgroundSubtractorMOG',
 'createBackgroundSubtractorMOG2',
 'createBoxFilter',
 'createBoxMaxFilter',
 'createBoxMinFilter',
 'createCLAHE',
 'createCannyEdgeDetector',
 'createColumnSumFilter',
 'createContinuous',
 'createConvolution',
 'createDFT',
 'createDerivFilter',
 'createDisparityBilateralFilter',
 'createGaussianFilter',
 'createGeneralizedHoughBallard',
 'createGeneralizedHoughGuil',
 'createGoodFeaturesToTrackDetector',
 'createHarrisCorner',
 'createHoughCirclesDetector',
 'createHoughLinesDetector',
 'createHoughSegmentDetector',
 'createLaplacianFilter',
 'createLinearFilter',
 'createLookUpTable',
 'createMedianFilter',
 'createMinEigenValCorner',
 'createMorphologyFilter',
 'createRowSumFilter',
 'createScharrFilter',
 'createSeparableLinearFilter',
 'createSobelFilter',
 'createStereoBM',
 'createStereoBeliefPropagation',
 'createStereoConstantSpaceBP',
 'createTemplateMatching',
 'cvtColor',
 'demosaicing',
 'dft',
 'divide',
 'drawColorDisp',
 'ensureSizeIsEnough',
 'equalizeHist',
 'evenLevels',
 'exp',
 'findMinMax',
 'findMinMaxLoc',
 'flip',
 'gammaCorrection',
 'gemm',
 'getCudaEnabledDeviceCount',
 'getDevice',
 'histEven',
 'histRange',
 'integral',
 'log',
 'magnitude',
 'magnitudeSqr',
 'max',
 'meanShiftFiltering',
 'meanShiftProc',
 'meanShiftSegmentation',
 'meanStdDev',
 'merge',
 'min',
 'minMax',
 'minMaxLoc',
 'mulAndScaleSpectrums',
 'mulSpectrums',
 'multiply',
 'norm',
 'normalize',
 'phase',
 'polarToCart',
 'pow',
 'printCudaDeviceInfo',
 'printShortCudaDeviceInfo',
 'pyrDown',
 'pyrUp',
 'rectStdDev',
 'reduce',
 'registerPageLocked',
 'remap',
 'reprojectImageTo3D',
 'resetDevice',
 'resize',
 'rotate',
 'setBufferPoolConfig',
 'setBufferPoolUsage',
 'setDevice',
 'split',
 'sqr',
 'sqrIntegral',
 'sqrSum',
 'sqrt',
 'subtract',
 'sum',
 'threshold',
 'transpose',
 'unregisterPageLocked',
 'warpAffine',
 'warpPerspective']

M-JPEG streamerとOpenCV(CV2)でタイムラプス動画を作成

10秒間隔で画像を400枚 jpgファイルとして保存

事前にM-JPEG streamerがインストールされ、アドレスxxx.xxx.xxx.xxx:8080で稼働している環境で、ファイル名 T0000.jpgからT0399.jpgとして画像を保存します。

import time
import cv2

# VideoCapture オブジェクトを取得します
URL = "http://xxx.xxx.xxx.xxx:8080/?action=stream"
capture = cv2.VideoCapture(URL)
for n in range(400):
    ret, frame = capture.read()
    name = "T" + '{:04d}'.format(n)+".jpg"
    print(name)
    cv2.imwrite(name, frame)
    time.sleep(10)

print("Done!")

jpgファイルからmp4動画を生成

import glob
import cv2

img_array = []
for filename in sorted(glob.glob("*.jpg")):
    print(filename)
    img = cv2.imread(filename)
    height, width, layers = img.shape
    size = (width, height)
    img_array.append(img)

name = 'project.mp4'
out = cv2.VideoWriter(name, cv2.VideoWriter_fourcc(*'mp4v'), 5.0, size)

for i in range(len(img_array)):
    out.write(img_array[i])
out.release()

生成したmp4動画の編集(不要部分の削除、回転、再生速度の調整など)には、Windows10標準のソフト「フォト」が便利。

作例

Raspberry Pi3BでRepetier-Serverを利用する

Raspberry Pi3Bの環境

 $ uname -a
Linux ps2 5.10.52-v7+ #1440 SMP Tue Jul 27 09:54:13 BST 2021 armv7l GNU/Linux

$ cat /etc/os-release
PRETTY_NAME="Raspbian GNU/Linux 10 (buster)"
NAME="Raspbian GNU/Linux"
VERSION_ID="10"
VERSION="10 (buster)"
VERSION_CODENAME=buster
ID=raspbian
ID_LIKE=debian
HOME_URL="http://www.raspbian.org/"
SUPPORT_URL="http://www.raspbian.org/RaspbianForums"
BUG_REPORT_URL="http://www.raspbian.org/RaspbianBugs"

ダウンロードサイトからLinux 32bit ARM(armfh)をダウンロードしてインストール

$ sudo dpkg -i  Repetier-Server-1.1.2-Linux.deb

インストールしたら、ブラウザで http://xxx.xxx.xxx.xxx:3344 をアクセスすると、プリンターの設定画面が表示されるので、マニュアルに従ってプリンターの追加・設定を行います

Webcam のインストール

ここを参照して、Webcamを設定

Repetier-ServerでWebcamの機能を利用するには、有料のプロバージョンが必要なため、 次のような画面となって、サーバーでは表示できません。

ただし、インストールしたWebcamのストリーミング機能は動作しているようで、ブラウザーで http://xxx.xxx.xxx.xxx:8080 をアクセスすると次のようにストリーミング映像を見ることができました。(カメラは窓の外向けに設置)

Start, Stop and Restart Repetier-Server

# Start server
sudo service RepetierServer start
sudo /etc/init.d/RepetierServer start
# Stop server
sudo service RepetierServer stop
sudo /etc/init.d/RepetierServer stop
# Restart server
sudo service RepetierServer stop
sudo /etc/init.d/RepetierServer restart

streamlitを試す

streamlitはpythonコードだけで、Webブラウザーからアクセスできるアプリを公開できる優れものです。磁気センサーのオフセット可視化で試してみました。jupyterのコードへ多少手を加えるだけでOK。デバッグはjupyterで動作確認を行い、完成したら必要に応じてstreamlit化するのが良さそう。streamlitはコマンドラインから次のように実行し、表示されたurlをブラウザーでアクセスします。

$ treamlit run mag-offset.py

  You can now view your Streamlit app in your browser.

  Network URL: http://192.168.68.122:8501
  External URL: http://203.165.226.42:8501

ブラウザーでアクセスした様子

mag-offset.py

import matplotlib

import os
import pandas as pd
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
import math
import numpy as np

import streamlit as st
st.title('Magnetic compass offset plot')
### データの読み込み
df = pd.read_csv('BMX055/data4.csv')
#print(df)
a_x=np.average(df['Mag_x'])
a_y=np.average(df['Mag_y'])
a_z=np.average(df['Mag_z'])
#print(round(a_x,2),round(a_y,2),round(a_z,2))
# ここからグラフ描画

# グラフの入れ物を用意する。
fig = plt.figure()
#ax = Axes3D(fig)     <--- warningとなるので、次の行に書き換え
ax = fig.add_subplot(111, projection='3d')
# 軸のラベルを設定する。
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
st.table(pd.DataFrame({
    'Center X': [a_x],
    'Center Y': [a_y],
    'Center Z': [a_z]
}))
# グラフを表示する。
ax.scatter3D(df['Mag_x'],df['Mag_y'],df['Mag_z'],color="blue")
ax.scatter3D(a_x,a_y,a_z,color="red")
#plt.show()       <--- plt.show()を st.write(fig)へ置き換える。
st.write(fig)

ESP32/DCモータ/Websocket UIで倒立振子

PID制御のパラメータと自立のための目標角度(TARGET)をWebsocket UIでスマホから設定します。今のところ、パラメータの値が適切でないようで、硬い床面上では短時間で倒れます。やわらかい素材の上では倒れないので、この状態で適切なパラメータを試行錯誤で探索中、、、WebベースのUIのためか、細かなパラメータ設定の操作性がいまいちなので、改善が必要かもしれない。


倒立振子のコードは、https://qiita.com/Google_Homer/items/3897e7ffef9d247e2f56 を参考にしています。

Websocketのコードは、https://github.com/mgo-tec/ESP32_SPIFFS_EasyWebSocket を参考にしています。

#include <WiFi.h>
#include <WiFiMulti.h>
#include "ESP32_SPIFFS_EasyWebSocket.h" //beta ver 1.60

#include <MadgwickAHRS.h>
Madgwick MadgwickFilter;
#include "I2Cdev.h"
#define MPU6050_PWR_MGMT_1   0x6B
#define MPU_ADDRESS  0x68

#include "Wire.h"
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_RESET     4 // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3C ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

//PIDパラメータ調整
//PID係数
//#define TARGET            0.4
//#define KP                100.0
//#define KI                2.0
//#define KD                2.0

//Motor
#define LED_PIN            27                      // 内蔵LED
#define MOTOR_PIN_F        A4                      // to DC Motor Driver FIN
#define MOTOR_PIN_R        A18                      // to DC Motor Driver RIN
#define MOTOR_PWM_F        0                       // PWM CHANNEL
#define MOTOR_PWM_R        1                       // PWM CHANNEL
#define MOTOR_POWER_MIN    50
int MOTOR_POWER_MAX = 200;

#define DPS                1000                    // Gscale [deg/s]

const char* ssid = "*******"; //ご自分のルーターのSSIDに書き換えてください
const char* password = "******"; //ご自分のルーターのパスワードに書き換えてください

const char* HTM_head_file1 = "/EWS/LIPhead1.txt"; //HTMLヘッダファイル1
const char* HTM_head_file2 = "/EWS/LIPhead2.txt"; //HTMLヘッダファイル2
const char* HTML_body_file = "/EWS/dummy.txt"; //HTML body要素ファイル(ここではダミーファイルとしておく)
const char* dummy_file = "/EWS/dummy.txt"; //HTMLファイル連結のためのダミーファイル

ESP32_SPIFFS_EasyWebSocket ews;
WiFiMulti wifiMulti;

IPAddress LIP; //ローカルIPアドレス自動取得用

String ret_str; //ブラウザから送られてくる文字列格納用
String txt = "text send?"; //ブラウザから受信した文字列を ESP32から再送信する文字列

int PingSendTime = 10000; //ESP32からブラウザへPing送信する間隔(ms)

long ESP32_send_LastTime;
int ESP32_send_Rate = 300;
byte cnt = 0;

//PID
float power = 0, I = 0, preP = 0, preTime;
float now = 0, Duty = 0, pitch, roll, yaw;
float KP = 100.0, KI = 2.0, KD = 2.0, TARGET = 0.0;
boolean f_disp = false;

void setup() {
  Wire.begin();
  Serial.begin(38400);
  Wire.beginTransmission(MPU_ADDRESS);
  Wire.write(MPU6050_PWR_MGMT_1);  //MPU6050_PWR_MGMT_1レジスタの設定
  Wire.write(0x00);
  Wire.endTransmission();

  Serial.print(F("Connecting to "));
  Serial.println(ssid);

  wifiMulti.addAP(ssid, password);

  Serial.println(F("Connecting Wifi..."));
  if (wifiMulti.run() == WL_CONNECTED) {
    Serial.println("");
    Serial.println(F("WiFi connected"));
    Serial.println(F("IP address: "));
    LIP = WiFi.localIP(); //ESP32のローカルIPアドレスを自動取得
    Serial.println(WiFi.localIP());
  }
  ews.EWS_server_begin();
  Serial.println(); Serial.println("Initializing SPIFFS ...");

  if (!SPIFFS.begin()) {
    Serial.println("SPIFFS failed, or not present");
    return;
  }

  Serial.println("SPIFFS initialized. OK!");
  MadgwickFilter.begin(100); //100Hz
  // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
  if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    Serial.println(F("SSD1306 allocation failed"));
    for (;;); // Don't proceed, loop forever
  }
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, HIGH);
  pinMode(MOTOR_PIN_F, OUTPUT);
  pinMode(MOTOR_PIN_R, OUTPUT);

  ledcSetup(MOTOR_PWM_F, 312500, 8); //CHANNEL, FREQ, BIT
  ledcSetup(MOTOR_PWM_R, 312500, 8);
  ledcAttachPin(MOTOR_PIN_F, MOTOR_PWM_F);
  ledcAttachPin(MOTOR_PIN_R, MOTOR_PWM_R);
  // 初期化

  delay(10);

  // Clear the buffer
  display.clearDisplay();
  // テキストサイズを設定
  display.setTextSize(1);
  // テキスト色を設定
  display.setTextColor(WHITE);
  display.setCursor(20, 5);
  display.println("Start...");
  display.setCursor(20, 25);
  display.println(WiFi.localIP());
  display.display();

  delay(10); // Pause for 2 seconds
  preTime = micros();
  //  TaskHandle_t th; //ESP32 マルチタスク ハンドル定義
  //  xTaskCreatePinnedToCore(Task1, "Task1", 4096, NULL, 5, &th, 0); //マルチタスク core 0 実行

  ESP32_send_LastTime = millis();
}

void loop() {
  websocket_handshake();

  if (ret_str != "_close") {
    if (millis() - ESP32_send_LastTime > ESP32_send_Rate) {
      if (cnt > 3) {
        cnt = 0;
      }
      websocket_send(cnt, txt);
      cnt++;
      ESP32_send_LastTime = millis();
    }
    ret_str = ews.EWS_ESP32CharReceive(PingSendTime);
    if (ret_str != "\0") {
      Serial.println(ret_str);
      if (ret_str != "Ping") {
        if (ret_str[0] != 't') {
          int ws_data = (ret_str[0] - 0x30) * 100 + (ret_str[1] - 0x30) * 10 + (ret_str[2] - 0x30);
          switch (ret_str[4]) {
            case '!':
              ESP32_send_Rate = ws_data;
              break;
            case 'B':
              TARGET = float(5.0 - ws_data / 20.0);
              break;
            case 'G':
              KP = float(ws_data / 1.8) ;
              break;
            case 'R':
              KI = float(ws_data / 50.0);
              break;
            case '_':
              KD = float(ws_data / 50.0);
              break;
            case 'A':
              f_disp = ! f_disp;
              break;
            case 'O':
              KP = 100, KI = 2.0, KD = 2.0, TARGET = 0.0;
              break;
          }
        } else if (ret_str[0] == 't') {
          txt = ret_str.substring(ret_str.indexOf('|') + 1, ret_str.length() - 1);
          Serial.println(txt);
        }
      }
    }
  } else if (ret_str == "_close") {
    ESP32_send_LastTime = millis();
    ret_str = "";
  }
  PID();
}

//************* 倒立振り子 ****************************************
void PID() {
  float  P, D, dt, Time;
  static float  powerI = 0;

  Wire.beginTransmission(0x68);
  Wire.write(0x3B);
  Wire.endTransmission(false);
  Wire.requestFrom(0x68, 14, true);
  while (Wire.available() < 14);
  int16_t axRaw, ayRaw, azRaw, gxRaw, gyRaw, gzRaw, Temperature;

  axRaw = Wire.read() << 8 | Wire.read();
  ayRaw = Wire.read() << 8 | Wire.read();
  azRaw = Wire.read() << 8 | Wire.read();
  Temperature = Wire.read() << 8 | Wire.read();
  gxRaw = Wire.read() << 8 | Wire.read();
  gyRaw = Wire.read() << 8 | Wire.read();
  gzRaw = Wire.read() << 8 | Wire.read();

  // 加速度値を分解能で割って加速度(G)に変換する
  float acc_x = axRaw / 16384.0;  //FS_SEL_0 16,384 LSB / g
  float acc_y = ayRaw / 16384.0;
  float acc_z = azRaw / 16384.0;

  // 角速度値を分解能で割って角速度(degrees per sec)に変換する
  float gyro_x = gxRaw / 131.0;  // (度/s)
  float gyro_y = gyRaw / 131.0;
  float gyro_z = gzRaw / 131.0;

  /*
    //c.f. Madgwickフィルターを使わずに、PRY(pitch, roll, yaw)を計算
    double roll  = atan2(acc_y, acc_z) * RAD_TO_DEG;
    double pitch = atan(-acc_x / sqrt(acc_y * acc_y + acc_z * acc_z)) * RAD_TO_DEG;
  */

  //Madgwickフィルターを用いて、PRY(pitch, roll, yaw)を計算
  MadgwickFilter.updateIMU(gyro_x, gyro_y, gyro_z, acc_x, acc_y, acc_z);

  //PRYの計算結果を取得する
  roll  = MadgwickFilter.getRoll();
  pitch = MadgwickFilter.getPitch();
  yaw   = MadgwickFilter.getYaw();
  now     = TARGET - roll                 ; // 目標角度から現在の角度を引いて偏差を求める
  if (f_disp) {
    display.clearDisplay();
    display.setCursor(20, 5);
    display.println( roll);
    display.setCursor(20, 25);
    display.println( now);
    display.display();
  }
  if (-20 < now && now < 20) {
    Time    = micros()                    ;
    dt      = (Time - preTime) / 1000000  ; // 処理時間を求める
    preTime = Time                        ; // 処理時間を記録
    P       = now / 90                    ; // -90~90→-1.0~1.0
    I      += P * dt                      ; // 偏差を積分する
    D       = (P - preP) / dt             ; // 偏差を微分する
    preP    = P                           ; // 偏差を記録する
    power  += KP * P + KI * I + KD * D    ; // 出力を計算する
    if (power < -1) power = -1            ; // →-1.0~1.0
    if (1 < power)  power =  1            ;
    //Motor駆動
    Duty = (int)((MOTOR_POWER_MAX - MOTOR_POWER_MIN) * abs(power) + MOTOR_POWER_MIN);
    ledcWrite( MOTOR_PWM_F, (power < 0 ?    0 : Duty) );
    ledcWrite( MOTOR_PWM_R, (power < 0 ? Duty :    0) );
    digitalWrite(LED_PIN, HIGH);
    if (f_disp) {
      display.clearDisplay();
      display.setCursor(20, 5);
      display.println( roll);
      display.setCursor(20, 25);
      display.println( Duty);
      display.setCursor(20, 45);
      display.println( power);
      display.display();
    }
  } else {                                                  // 転倒したら停止
    ledcWrite(MOTOR_PWM_F, 0);
    ledcWrite(MOTOR_PWM_R, 0);
    power = 0;
    I = 0;
    digitalWrite(LED_PIN, LOW);
  }

}

//**************************************************************
void LED_PWM(byte Led_gr, byte channel, int data_i) {
  Serial.println(data_i);
}
//*********************************************
void websocket_send(uint8_t count, String str_txt) {
  String str, tmp;
  //※WebSocketへのテキスト送信は110 byte 程度なので、全角35文字程度に抑えること
  tmp = "AGL=" + String(roll) + ",TGT=" + String(TARGET) + ",P=" + String(KP) +  ",I=" + String(KI) + ",D=" + String(KD);
  switch (cnt) {
    case 0:
      //str = str_txt;
      str = tmp;
      break;
    case 1:
      str = tmp;
      break;
    case 2:
      str = tmp;
      break;
    case 3:
      str = tmp;
      break;
  }

  ews.EWS_ESP32_Str_SEND(str, "wroomTXT"); //ブラウザに文字列を送信
}
//************************* Websocket handshake **************************************
void websocket_handshake() {
  if (ews.Get_Http_Req_Status()) { //ブラウザからGETリクエストがあったかどうかの判定
    String html_str1 = "", html_str2 = "", html_str3 = "", html_str4 = "", html_str5 = "", html_str6 = "", html_str7 = "";

    //※String変数一つにEWS_Canvas_Slider_T関数は2つまでしか入らない
    html_str1 += "<body style='background:#000; color:#fff;'>\r\n";
    html_str1 += "<font size=3>\r\n";
    html_str1 += "ESP-WROOM-32(ESP32)\r\n";
    html_str1 += "<br>\r\n";
    html_str1 += "ESP32_SPIFFS_EasyWebSocket Beta1.60 Sample\r\n";
    html_str1 += "</font><br>\r\n";
    html_str1 += ews.EWS_BrowserSendRate();
    html_str1 += "<br>\r\n";
    html_str1 += ews.EWS_ESP32_SendRate("!esp32t-Rate");
    html_str1 += "<br>\r\n";
    html_str1 += ews.EWS_BrowserReceiveTextTag2("wroomTXT", "from ESP32 DATA", "#555", 20, "#00FF00");
    html_str1 += "<br>\r\n";
    html_str1 += ews.EWS_Status_Text2("WebSocket Status", "#555", 20, "#FF00FF");
    html_str1 += "<br><br>\r\n";

    html_str2 += ews.EWS_TextBox_Send("txt1", "Hello Easy WebSocket Beta1.60", "送信");
    html_str2 += "<br><br>\r\n";
    html_str2 += "SETTING \r\n";
    html_str2 += ews.EWS_On_Momentary_Button("ALL", "ALL-ON", 80, 25, 15, "#000000", "#AAAAAA");
    html_str2 += ews.EWS_On_Momentary_Button("OUT", "DEFALT", 80, 25, 15, "#FFFFFF", "#555555");
    html_str2 += "<br>\r\n";

    html_str3 += "<br>TGT_ :\r\n";
    html_str3 += ews.EWS_Canvas_Slider_T("BLUE", 240, 40, "#777777", "#0000ff"); //CanvasスライダーはString文字列に2つまでしか入らない
    html_str3 += "<br>PID P:\r\n";
    html_str3 += ews.EWS_Canvas_Slider_T("GREEN", 250, 40, "#777777", "#00ff00"); //CanvasスライダーはString文字列に2つまでしか入らない

    html_str4 += "<br>PID I:\r\n";
    html_str4 += ews.EWS_Canvas_Slider_T("RED", 250, 40, "#777777", "#ff0000"); //CanvasスライダーはString文字列に2つまでしか入らない
    html_str4 += "<br>PID D:\r\n";
    html_str4 += ews.EWS_Canvas_Slider_T("_RGB", 250, 40, "#777777", "#ffff00");

    html_str7 += "<br><br>\r\n";
    html_str7 += ews.EWS_WebSocket_Reconnection_Button2("WS-Reconnect", "grey", 200, 40, "black" , 17);
    html_str7 += "<br><br>\r\n";
    html_str7 += ews.EWS_Close_Button2("WS CLOSE", "#bbb", 150, 40, "red", 17);
    html_str7 += ews.EWS_Window_ReLoad_Button2("ReLoad", "#bbb", 150, 40, "blue", 17);
    html_str7 += "</body></html>";

    //WebSocket ハンドシェイク関数
    ews.EWS_HandShake_main(3, HTM_head_file1, HTM_head_file2, HTML_body_file, dummy_file, LIP, html_str1, html_str2, html_str3, html_str4, html_str5, html_str6, html_str7);
  }
}