2019年12月23日月曜日

真空管アンプ本

現在,低電圧で駆動する真空管と半導体バッファを用いて,ブレッドボードと12VのACアダプターで動くハイブリッド真空管アンプをLTspiceで設計しよう,という本を執筆中なのですが,ページ数が予定の半分の超えたので,PDFファイルを公開しておきます.

低電圧真空管アンプ本(2019/12/23)

2019年12月17日火曜日

画像プログラミング(C言語)の授業資料

画像プログラミングの授業を実施しなくなったので,授業の資料とプログラムを公開します.
資料は全てPDF形式です.プログラムは,Windowsの24bit BMPプログラムを入出力するように書かれていますので,他の形式の画像ファイルを24bit BMP形式に変換するためのVisual Basicのプログラムも一緒に公開します.
プログラムファイルは,全て普通のANSI Cで書かれているので,ファイルを持っていけば,MacでもLinuxでも動くと思います.

  1. 資料0 (PDFファイル)
  2. 資料1 (PDFファイル)
  3. 資料2 (PDFファイル)
  4. 資料3 (PDFファイル)
  5. 資料4 (PDFファイル)
  6. 資料5 (PDFファイル)
  7. 資料6 (PDFファイル)
  8. 資料7 (PDFファイル)
  9. 資料8 (PDFファイル)
  10. 資料9 (PDFファイル)
  11. 資料10 (PDFファイル)
  12. 資料11 (PDFファイル)
  13. 資料12 (PDFファイル)
  14. 資料13 (PDFファイル)
  15. 資料14 (PDFファイル)
  16. 資料15 (期末試験,PDFファイル)

2019年8月12日月曜日

ESP-WROOM-02(ESP8266)で,9軸センサーBMX055から取得した値をパソコン(mac)にOSCフォーマットで送信する.

前の記事で,9軸センサーの値をUDPでテキスト形式でパソコンに送信できたので,いよいよESP-WROOM-02でBMX055から取得した9軸データをOSCフォーマットでパソコン(mac)に送信することにします.

Arduino用のOSCライブラリーはここの物を使いました.「Clone or Download」ボタンをクリックしてzipファイルをダウンロードし,Arduinoの「Sketch」メニュー→「Include Library」→「Add .ZIP Library...」を選択してzipファイルを指定し,「Choose」ボタンをクリックしてインストールします.再度「Sketch」メニュー→「Include Library」を選択すると,「Contributed libraries」の下の方に「OSC」が表示されます.

ESP-WROOM-02のプログラムを以下のように書き換えます.

//================================================================//
//  AE-BMX055             ESP8266                            //
//    3V3                    +3.3V(1)                                  //
//    GND                    GND(17)                                  //
//    SDA                    IO4(12)                              //
//    SCL                    IO5(16)                              //
//                                                                //
//   (JP6,JP4,JP5はショートした状態)                              //
//   http://akizukidenshi.com/catalog/g/gK-13010/                 //
//   -> 3.3Vで使うのでJP6はショートしない
//================================================================//

// 2019/8/12
// send accel,gyro,compass data to the destination port
// by text data
// add OSC library from https://github.com/CNMAT/OSC

#include 
#include 
#include 
#include 
#include  // OSC library https://github.com/CNMAT/OSC
#include  // sprintf()

WiFiUDP UDP;

IPAddress HOSTIP(192, 168, 3, 4); // IP address of the destination
const int remoteUdpPort = 30000;  // port number of the destination

IPAddress myIP(192, 168, 3, 100);  // this address is needed for WiFi config
const int localUdpPort = 31000;   // local port number

// since mac is connected to the internet through iPhone,
// iPhone is the WiFi router.
const char *ssid = "iPhone7PlusBlack";
const char *password = "qatacygd60y8u";

// BMX055 加速度センサのI2Cアドレス  
#define Addr_Accl 0x19  // (JP1,JP2,JP3 = Openの時)
// BMX055 ジャイロセンサのI2Cアドレス
#define Addr_Gyro 0x69  // (JP1,JP2,JP3 = Openの時)
// BMX055 磁気センサのI2Cアドレス
#define Addr_Mag 0x13   // (JP1,JP2,JP3 = Openの時)

// センサーの値を保存するグローバル関数
float xAccl = 0.00;
float yAccl = 0.00;
float zAccl = 0.00;
float xGyro = 0.00;
float yGyro = 0.00;
float zGyro = 0.00;
short int   xMag  = 0;
short int   yMag  = 0;
short int   zMag  = 0;

void connectWiFi() {
  Serial.println("WiFi setup start");
  WiFi.begin(ssid, password);
  WiFi.config(myIP, WiFi.gatewayIP(), WiFi.subnetMask());
  Serial.println("WiFi setup done");
  Serial.println("start_connect");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("CONNECTED!");
}

void reconnectWiFi() {
  if (WiFi.status() != WL_CONNECTED) {
    WiFi.disconnect();
    Serial.println("disconnected! reconnect WiFi.");
    connectWiFi();
  }
}

void setup()
{
  // Wire(Arduino-I2C)の初期化
  Wire.begin();
  // デバック用シリアル通信は9600bps
  Serial.begin(9600);
  //BMX055 初期化
  BMX055_Init();
  delay(300);
  //WiFi初期化
  WiFi.mode(WIFI_STA);
  UDP.begin(localUdpPort);
  connectWiFi();
}

void loop()
{
  char buf[128];
  Serial.println("--------------------------------------"); 

  //BMX055 加速度の読み取り
  BMX055_Accl();
  sprintf(buf, "/ZIGSIM/hh3-zig/accel: (%f, %f, %f)",
          xAccl, yAccl, zAccl);
  Serial.println(buf);
  OSCMessage msgAccel("/ZIGSIM/hh3-zig/accel");
  msgAccel.add(xAccl).add(yAccl).add(zAccl);
  UDP.beginPacket(HOSTIP, remoteUdpPort);
  msgAccel.send(UDP);
  UDP.endPacket();
  msgAccel.empty();  
  delay(100);
  reconnectWiFi();

  //BMX055 ジャイロの読み取り
  BMX055_Gyro();
  sprintf(buf, "/ZIGSIM/hh3-zig/gyro: (%f, %f, %f)",
          xGyro, yGyro, zGyro);
  Serial.println(buf);
  OSCMessage msgGyro("/ZIGSIM/hh3-zig/gyro");
  msgGyro.add(xGyro).add(yGyro).add(zGyro);
  UDP.beginPacket(HOSTIP, remoteUdpPort);
  msgGyro.send(UDP);
  UDP.endPacket();
  msgGyro.empty();
  delay(100);
  reconnectWiFi();
  
  //BMX055 磁気の読み取り
  BMX055_Mag();
  sprintf(buf, "/ZIGSIM/hh3-zig/mag: (%d, %d, %d)",
          xMag, yMag, zMag);
  Serial.println(buf);
  OSCMessage msgMag("/ZIGSIM/hh3-zig/mag");
  msgMag.add(xMag).add(yMag).add(zMag);
  UDP.beginPacket(HOSTIP, remoteUdpPort);
  msgMag.send(UDP);
  UDP.endPacket();
  msgMag.empty();
  delay(100);
  reconnectWiFi();

  delay(700);
}

//=====================================================================================//
void BMX055_Init()
{
  //------------------------------------------------------------//
  Wire.beginTransmission(Addr_Accl);
  Wire.write(0x0F); // Select PMU_Range register
  Wire.write(0x03);   // Range = +/- 2g
  Wire.endTransmission();
  delay(100);
 //------------------------------------------------------------//
  Wire.beginTransmission(Addr_Accl);
  Wire.write(0x10);  // Select PMU_BW register
  Wire.write(0x08);  // Bandwidth = 7.81 Hz
  Wire.endTransmission();
  delay(100);
  //------------------------------------------------------------//
  Wire.beginTransmission(Addr_Accl);
  Wire.write(0x11);  // Select PMU_LPW register
  Wire.write(0x00);  // Normal mode, Sleep duration = 0.5ms
  Wire.endTransmission();
  delay(100);
 //------------------------------------------------------------//
  Wire.beginTransmission(Addr_Gyro);
  Wire.write(0x0F);  // Select Range register
  Wire.write(0x04);  // Full scale = +/- 125 degree/s
  Wire.endTransmission();
  delay(100);
 //------------------------------------------------------------//
  Wire.beginTransmission(Addr_Gyro);
  Wire.write(0x10);  // Select Bandwidth register
  Wire.write(0x07);  // ODR = 100 Hz
  Wire.endTransmission();
  delay(100);
 //------------------------------------------------------------//
  Wire.beginTransmission(Addr_Gyro);
  Wire.write(0x11);  // Select LPM1 register
  Wire.write(0x00);  // Normal mode, Sleep duration = 2ms
  Wire.endTransmission();
  delay(100);
 //------------------------------------------------------------//
  Wire.beginTransmission(Addr_Mag);
  Wire.write(0x4B);  // Select Mag register
  Wire.write(0x83);  // Soft reset
  Wire.endTransmission();
  delay(100);
  //------------------------------------------------------------//
  Wire.beginTransmission(Addr_Mag);
  Wire.write(0x4B);  // Select Mag register
  Wire.write(0x01);  // Soft reset
  Wire.endTransmission();
  delay(100);
  //------------------------------------------------------------//
  Wire.beginTransmission(Addr_Mag);
  Wire.write(0x4C);  // Select Mag register
  Wire.write(0x00);  // Normal Mode, ODR = 10 Hz
  Wire.endTransmission();
 //------------------------------------------------------------//
  Wire.beginTransmission(Addr_Mag);
  Wire.write(0x4E);  // Select Mag register
  Wire.write(0x84);  // X, Y, Z-Axis enabled
  Wire.endTransmission();
 //------------------------------------------------------------//
  Wire.beginTransmission(Addr_Mag);
  Wire.write(0x51);  // Select Mag register
  Wire.write(0x04);  // No. of Repetitions for X-Y Axis = 9
  Wire.endTransmission();
 //------------------------------------------------------------//
  Wire.beginTransmission(Addr_Mag);
  Wire.write(0x52);  // Select Mag register
  Wire.write(0x16);  // No. of Repetitions for Z-Axis = 15
  Wire.endTransmission();
}
//=====================================================================================//
void BMX055_Accl()
{
  unsigned char data[6];
  for (int i = 0; i < 6; i++)
  {
    Wire.beginTransmission(Addr_Accl);
    Wire.write((2 + i));// Select data register
    Wire.endTransmission();
    Wire.requestFrom(Addr_Accl, 1);// Request 1 byte of data
    // Read 6 bytes of data
    // xAccl lsb, xAccl msb, yAccl lsb, yAccl msb, zAccl lsb, zAccl msb
    if (Wire.available() == 1)
      data[i] = Wire.read();
  }
  // Convert the data to 12-bits
  xAccl = ((data[1] * 256) + (data[0] & 0xF0)) / 16;
  if (xAccl > 2047)  xAccl -= 4096;
  yAccl = ((data[3] * 256) + (data[2] & 0xF0)) / 16;
  if (yAccl > 2047)  yAccl -= 4096;
  zAccl = ((data[5] * 256) + (data[4] & 0xF0)) / 16;
  if (zAccl > 2047)  zAccl -= 4096;
  xAccl = xAccl * 0.0098; // renge +-2g
  yAccl = yAccl * 0.0098; // renge +-2g
  zAccl = zAccl * 0.0098; // renge +-2g
}
//=====================================================================================//
void BMX055_Gyro()
{
  unsigned char data[6];
  for (int i = 0; i < 6; i++)
  {
    Wire.beginTransmission(Addr_Gyro);
    Wire.write((2 + i));    // Select data register
    Wire.endTransmission();
    Wire.requestFrom(Addr_Gyro, 1);    // Request 1 byte of data
    // Read 6 bytes of data
    // xGyro lsb, xGyro msb, yGyro lsb, yGyro msb, zGyro lsb, zGyro msb
    if (Wire.available() == 1)
      data[i] = Wire.read();
  }
  // Convert the data
  xGyro = (data[1] * 256) + data[0];
  if (xGyro > 32767)  xGyro -= 65536;
  yGyro = (data[3] * 256) + data[2];
  if (yGyro > 32767)  yGyro -= 65536;
  zGyro = (data[5] * 256) + data[4];
  if (zGyro > 32767)  zGyro -= 65536;

  xGyro = xGyro * 0.0038; //  Full scale = +/- 125 degree/s
  yGyro = yGyro * 0.0038; //  Full scale = +/- 125 degree/s
  zGyro = zGyro * 0.0038; //  Full scale = +/- 125 degree/s
}
//=====================================================================================//
void BMX055_Mag()
{
  unsigned short int data[8];
  for (int i = 0; i < 8; i++)
  {
    Wire.beginTransmission(Addr_Mag);
    Wire.write((0x42 + i));    // Select data register
    Wire.endTransmission();
    Wire.requestFrom(Addr_Mag, 1);    // Request 1 byte of data
    // Read 6 bytes of data
    // xMag lsb, xMag msb, yMag lsb, yMag msb, zMag lsb, zMag msb
    if (Wire.available() == 1)
      data[i] = Wire.read();
  }
  // Convert the data
  xMag = ((data[1] <<8 data="">>3));
  if (xMag > 4095)  xMag -= 8192;
  yMag = ((data[3] <<8 data="">>3));
  if (yMag > 4095)  yMag -= 8192;
  zMag = ((data[5] <<8 data="">>3));
  if (zMag > 16383)  zMag -= 32768;
}


OSCデータを受け取って表示するパソコン側のPythonプログラムは以下のとおりです.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Python 3.7.3 on macOS 10.15 Catalina Beta
# 2019/8/7
# receive OSC data by UDP

import argparse
from socket import *
from pythonosc import dispatcher
from pythonosc import osc_server

def handle_zigsim_osc_message(addr, *args):
    try:
        print("{0}: {1}".format(addr, args))
    except ValueError:
        print("ValueError occrurred")

def start_OSC_server(ip_addr, port_num):
    print("starting on address {}, port {}".format(ip_addr, port_num))

    # OSC dispatcher
    dispatch = dispatcher.Dispatcher()
    dispatch.map("/ZIGSIM/*", handle_zigsim_osc_message )

    server = osc_server.ThreadingOSCUDPServer(
        (ip_addr, port_num), dispatch)
    print("Serving on {}".format(server.server_address))
    server.serve_forever()

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--ip", default="127.0.0.1",
                        help="IP address to listen on")
    parser.add_argument("--port", type=int, default=30000,
                        help="port to listen on")
    args = parser.parse_args()
    start_OSC_server(args.ip, args.port)


動作環境は以下のとおりです.

  • パソコン側のIPアドレス:192.168.3.4
  • OSCデータを受け取るUDPポート番号:30000
  • 送信側ESP-WROOM-02のUUID:hh3_zig
パソコン側のターミナルソフトで以下のようにPythonプログラムを動かします.

% ./UDPserver2.py --ip 192.168.3.4 --port 30000


ESP-WROOM-02にプログラムを書き込んでリセットボタンを押すと,パソコンで以下のようにデータが受信できていることが確認できました.

/ZIGSIM/hh3-zig/accel: (-0.11760000139474869, 0.2547999918460846, 9.790200233459473)
/ZIGSIM/hh3-zig/gyro: (-0.3571999967098236, -0.20520000159740448, -0.04179999977350235)
/ZIGSIM/hh3-zig/mag: (2049, -762, -237)
/ZIGSIM/hh3-zig/accel: (-0.09799999743700027, 0.24500000476837158, 9.829400062561035)
/ZIGSIM/hh3-zig/gyro: (-0.47119998931884766, -0.2013999968767166, 0.11779999732971191)
/ZIGSIM/hh3-zig/mag: (1822, -762, -237)


ESP-WROOM-02(ESP8266)で,9軸センサーBMX055から取得した値をパソコンにUDPで送信する.

前の記事で,ESP-WROOM-02 (ESP8266)9軸センサーBMX055からの値を取得できたので,これをUDPでパソコンのPythonプログラムに送信しました. 

IPアドレスの設定は以下の通りです.
  • パソコン側のIPアドレス:172.20.10.4
パソコンの受け側のUDPのポート番号を以下の値とします.
  • パソコン側のUDPポート番号:30000
パソコン側のPythonのプログラムは以下のとおりです.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Python 3.7.3 on macOS 10.15 Catalina Beta
# 2019/8/4
# receive JSON data by UDP

import argparse
import threading
import time
from socket import *

class ServerThread(threading.Thread):
    def __init__(self, ADDRESS="127.0.0.1", PORT=30000):
        threading.Thread.__init__(self)
        self.data = None
        self.kill_flag = False
        # line information
        self.ADDRESS = ADDRESS
        self.PORT = PORT
        self.BUFSIZE = 1024
        self.ADDR = (gethostbyname(self.ADDRESS), self.PORT)
        print(self.ADDR)
        # bind
        self.udpServSock = socket(AF_INET, SOCK_DGRAM)
        self.udpServSock.bind(self.ADDR) # HOST, PORTでbinding

    def run(self):
        while True:
            try:
                # データ受信
                data, self.addr = self.udpServSock.recvfrom(self.BUFSIZE)
                if len(data) > 0:
                    print("receive {} bytes".format(len(data)))
                    print("received data: {}".format(data))
                # OSCはデコードできなくてexceptionが起こる
                # JSONだとテキストなのでデコードできる
                self.data = data.decode()
                print("decoded data: {}".format(self.data))
            except:
                print("exception occrurred")
                #pass

def start_UDP_server(ip_address, port_number):
    print("starting on address {}, port {}".format(ip_address, port_number))
    th = ServerThread(ip_address, port_number)
    th.setDaemon(True)
    th.start()

    while True:
        time.sleep(1.0)
        if not th.data:
            continue
        print(th.data)

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--ip", default="127.0.0.1",
                        help="IP address to listen on")
    parser.add_argument("--port", type=int, default=30000,
                        help="port to listen on")
    args = parser.parse_args()
    start_UDP_server(args.ip, args.port)


ESP-WROOM-02側のプログラムは以下のとおりです.

//================================================================//
//  AE-BMX055             ESP8266                            //
//    3V3                    +3.3V(1)                                  //
//    GND                    GND(17)                                  //
//    SDA                    IO4(12)                              //
//    SCL                    IO5(16)                              //
//                                                                //
//   (JP6,JP4,JP5はショートした状態)                              //
//   http://akizukidenshi.com/catalog/g/gK-13010/                 //
//   -> 3.3Vで使うのでJP6はショートしない
//================================================================//

#include 
#include 
#include 
#include 
#include  // sprintf()

WiFiUDP UDP;
IPAddress HOSTIP(172, 20, 10, 4);
const int remoteUdpPort = 30000;
IPAddress myIP(172, 20, 10, 10);
const int localUdpPort = 31000;
const char *ssid = "iPhone7PlusBlack";
const char *password = "qatacygd60y8u";

// BMX055 加速度センサのI2Cアドレス  
#define Addr_Accl 0x19  // (JP1,JP2,JP3 = Openの時)
// BMX055 ジャイロセンサのI2Cアドレス
#define Addr_Gyro 0x69  // (JP1,JP2,JP3 = Openの時)
// BMX055 磁気センサのI2Cアドレス
#define Addr_Mag 0x13   // (JP1,JP2,JP3 = Openの時)

// センサーの値を保存するグローバル関数
float xAccl = 0.00;
float yAccl = 0.00;
float zAccl = 0.00;
float xGyro = 0.00;
float yGyro = 0.00;
float zGyro = 0.00;
short int   xMag  = 0;
short int   yMag  = 0;
short int   zMag  = 0;

void connectWiFi() {
  Serial.println("WiFi setup start");
  WiFi.begin(ssid, password);
  WiFi.config(myIP, WiFi.gatewayIP(), WiFi.subnetMask());
  Serial.println("WiFi setup done");
  Serial.println("start_connect");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("CONNECTED!");
}

void sendWiFi(char byteData[]) {
  if (UDP.beginPacket(HOSTIP, remoteUdpPort)) {
    UDP.write(byteData);
    UDP.endPacket();
    Serial.println(byteData);
  }
}

void reconnectWiFi() {
  if (WiFi.status() != WL_CONNECTED) {
    WiFi.disconnect();
    Serial.println("disconnected! reconnect WiFi.");
    connectWiFi();
  }
}

void setup()
{
  // Wire(Arduino-I2C)の初期化
  Wire.begin();
  // デバック用シリアル通信は9600bps
  Serial.begin(9600);
  //BMX055 初期化
  BMX055_Init();
  delay(300);
  //WiFi初期化
  WiFi.mode(WIFI_STA);
  UDP.begin(localUdpPort);
  connectWiFi();
}

void loop()
{
  char buf[128];
  Serial.println("--------------------------------------"); 

  //BMX055 加速度の読み取り
  BMX055_Accl();
  sprintf( buf, "Accel= %f,%f,%f", xAccl, yAccl, zAccl);
  sendWiFi(buf);
  delay(500);
  reconnectWiFi();

  //BMX055 ジャイロの読み取り
  BMX055_Gyro();
  sprintf( buf, "Gyro= %f,%f,%f", xGyro, yGyro, zGyro);
  sendWiFi(buf);
  delay(500);
  reconnectWiFi();
  
  //BMX055 磁気の読み取り
  BMX055_Mag();
  sprintf( buf, "Mag= %d,%d,%d", xMag, yMag, zMag);
  sendWiFi(buf);
  delay(500);
  reconnectWiFi();  
}

//=====================================================================================//
void BMX055_Init()
{
  //------------------------------------------------------------//
  Wire.beginTransmission(Addr_Accl);
  Wire.write(0x0F); // Select PMU_Range register
  Wire.write(0x03);   // Range = +/- 2g
  Wire.endTransmission();
  delay(100);
 //------------------------------------------------------------//
  Wire.beginTransmission(Addr_Accl);
  Wire.write(0x10);  // Select PMU_BW register
  Wire.write(0x08);  // Bandwidth = 7.81 Hz
  Wire.endTransmission();
  delay(100);
  //------------------------------------------------------------//
  Wire.beginTransmission(Addr_Accl);
  Wire.write(0x11);  // Select PMU_LPW register
  Wire.write(0x00);  // Normal mode, Sleep duration = 0.5ms
  Wire.endTransmission();
  delay(100);
 //------------------------------------------------------------//
  Wire.beginTransmission(Addr_Gyro);
  Wire.write(0x0F);  // Select Range register
  Wire.write(0x04);  // Full scale = +/- 125 degree/s
  Wire.endTransmission();
  delay(100);
 //------------------------------------------------------------//
  Wire.beginTransmission(Addr_Gyro);
  Wire.write(0x10);  // Select Bandwidth register
  Wire.write(0x07);  // ODR = 100 Hz
  Wire.endTransmission();
  delay(100);
 //------------------------------------------------------------//
  Wire.beginTransmission(Addr_Gyro);
  Wire.write(0x11);  // Select LPM1 register
  Wire.write(0x00);  // Normal mode, Sleep duration = 2ms
  Wire.endTransmission();
  delay(100);
 //------------------------------------------------------------//
  Wire.beginTransmission(Addr_Mag);
  Wire.write(0x4B);  // Select Mag register
  Wire.write(0x83);  // Soft reset
  Wire.endTransmission();
  delay(100);
  //------------------------------------------------------------//
  Wire.beginTransmission(Addr_Mag);
  Wire.write(0x4B);  // Select Mag register
  Wire.write(0x01);  // Soft reset
  Wire.endTransmission();
  delay(100);
  //------------------------------------------------------------//
  Wire.beginTransmission(Addr_Mag);
  Wire.write(0x4C);  // Select Mag register
  Wire.write(0x00);  // Normal Mode, ODR = 10 Hz
  Wire.endTransmission();
 //------------------------------------------------------------//
  Wire.beginTransmission(Addr_Mag);
  Wire.write(0x4E);  // Select Mag register
  Wire.write(0x84);  // X, Y, Z-Axis enabled
  Wire.endTransmission();
 //------------------------------------------------------------//
  Wire.beginTransmission(Addr_Mag);
  Wire.write(0x51);  // Select Mag register
  Wire.write(0x04);  // No. of Repetitions for X-Y Axis = 9
  Wire.endTransmission();
 //------------------------------------------------------------//
  Wire.beginTransmission(Addr_Mag);
  Wire.write(0x52);  // Select Mag register
  Wire.write(0x16);  // No. of Repetitions for Z-Axis = 15
  Wire.endTransmission();
}
//=====================================================================================//
void BMX055_Accl()
{
  unsigned char data[6];
  for (int i = 0; i < 6; i++)
  {
    Wire.beginTransmission(Addr_Accl);
    Wire.write((2 + i));// Select data register
    Wire.endTransmission();
    Wire.requestFrom(Addr_Accl, 1);// Request 1 byte of data
    // Read 6 bytes of data
    // xAccl lsb, xAccl msb, yAccl lsb, yAccl msb, zAccl lsb, zAccl msb
    if (Wire.available() == 1)
      data[i] = Wire.read();
  }
  // Convert the data to 12-bits
  xAccl = ((data[1] * 256) + (data[0] & 0xF0)) / 16;
  if (xAccl > 2047)  xAccl -= 4096;
  yAccl = ((data[3] * 256) + (data[2] & 0xF0)) / 16;
  if (yAccl > 2047)  yAccl -= 4096;
  zAccl = ((data[5] * 256) + (data[4] & 0xF0)) / 16;
  if (zAccl > 2047)  zAccl -= 4096;
  xAccl = xAccl * 0.0098; // renge +-2g
  yAccl = yAccl * 0.0098; // renge +-2g
  zAccl = zAccl * 0.0098; // renge +-2g
}
//=====================================================================================//
void BMX055_Gyro()
{
  unsigned char data[6];
  for (int i = 0; i < 6; i++)
  {
    Wire.beginTransmission(Addr_Gyro);
    Wire.write((2 + i));    // Select data register
    Wire.endTransmission();
    Wire.requestFrom(Addr_Gyro, 1);    // Request 1 byte of data
    // Read 6 bytes of data
    // xGyro lsb, xGyro msb, yGyro lsb, yGyro msb, zGyro lsb, zGyro msb
    if (Wire.available() == 1)
      data[i] = Wire.read();
  }
  // Convert the data
  xGyro = (data[1] * 256) + data[0];
  if (xGyro > 32767)  xGyro -= 65536;
  yGyro = (data[3] * 256) + data[2];
  if (yGyro > 32767)  yGyro -= 65536;
  zGyro = (data[5] * 256) + data[4];
  if (zGyro > 32767)  zGyro -= 65536;

  xGyro = xGyro * 0.0038; //  Full scale = +/- 125 degree/s
  yGyro = yGyro * 0.0038; //  Full scale = +/- 125 degree/s
  zGyro = zGyro * 0.0038; //  Full scale = +/- 125 degree/s
}
//=====================================================================================//
void BMX055_Mag()
{
  unsigned short int data[8];
  for (int i = 0; i < 8; i++)
  {
    Wire.beginTransmission(Addr_Mag);
    Wire.write((0x42 + i));    // Select data register
    Wire.endTransmission();
    Wire.requestFrom(Addr_Mag, 1);    // Request 1 byte of data
    // Read 6 bytes of data
    // xMag lsb, xMag msb, yMag lsb, yMag msb, zMag lsb, zMag msb
    if (Wire.available() == 1)
      data[i] = Wire.read();
  }
  // Convert the data
  xMag = ((data[1] <<8 data="">>3));
  if (xMag > 4095)  xMag -= 8192;
  yMag = ((data[3] <<8 data="">>3));
  if (yMag > 4095)  yMag -= 8192;
  zMag = ((data[5] <<8 data="">>3));
  if (zMag > 16383)  zMag -= 32768;
}


パソコン側で以下のようにPythonプログラムを起動します.

% ./UDPserver1.py --ip 172.20.10.4 --port 30000


ESP-WROOM-02側のシリアルモニタでは,以下のようにデータが送信できています.

--------------------------------------
Accel= -0.098000,0.098000,9.780400
Gyro= -0.049400,-0.262200,0.095000
Mag= -503,11,-498
--------------------------------------


パソコン側では以下のようにデータが受信できています.

receive 34 bytes
received data: b'Accel= -0.098000,0.098000,9.780400'
decoded data: Accel= -0.098000,0.098000,9.780400
Accel= -0.098000,0.098000,9.780400
receive 34 bytes
received data: b'Gyro= -0.049400,-0.262200,0.095000'
decoded data: Gyro= -0.049400,-0.262200,0.095000
receive 17 bytes
received data: b'Mag= -503,11,-498'
decoded data: Mag= -503,11,-498
Mag= -503,11,-498


ESP-WROOM-02で9軸センサーBMX055を使う

I2Cで使えるBMX055 9軸センサーモジュール秋月のESP-WROOM-02ボードから使ってみました.

I2Cの配線について

BMX055は以下の端子が出ています.

  • ピン1: GND
  • ピン2: SDA (I2Cデータ)
  • ピン3: SCL: (I2Cクロック)
  • ピン4: 3V3 (BMX055用電源端子)

一方,こちらのページを参照すると,ESP8266のI2C端子は以下のようになっています.

  • SDA: 10(IO4)
  • SCL: 14(IO5)
これが,秋月のESP-WROOM-02ボードでは以下のピンに出ています.
  • ピン12: IO4 (SDA)
  • ピン16: IO5 (SCL)
ESP8266用のWireライブラリでも,これを使うことになっているようです.そこで,以下のように配線しました.

接続にあたって,Arduinoの場合と同様,JP4とJP5をpull upのために半田付けしました.電源は3.3Vを取っているので,とりあえず他の場所は触っていません.


プログラム

まずは,9軸センサーから読んだデータをシリアルポートに書き出して,Arduino IDEのシリアルモニタで表示することにしました.

秋月電子のBMX055のページにあるサンプルプログラムそのままでは動きませんでした.
27行目から29行目の
int xMag = 0;
int yMag = 0;
int zMag = 0;

short int xMag = 0;
short int yMag = 0;
short int zMag = 0;
に,
154行目と180行目の
int data[6];

unsigned char data[6];
に,
207行目の
int data[8];

unsigned short int data[8];
に書き換えたところ,それらしいデータが出てくるようになりました.

プログラムを動かすには,プログラムを書き込んだ後,リセットボタンを押す必要があります.

参照ページ:
ESP-WROOM-02ボードのI2C関係のピン配置 http://radiopench.blog96.fc2.com/blog-entry-661.html

ESP-WROOM-02からmacOSにUDP通信

マイコンボードESP-WROOM-02からmacOSにUDP接続して通信テストを行った.

macはiPhoneにテザリングして,iPhoneをWiFiアクセスポイントとする.この時点で,iPhoneのWiFiアクセスポイントの設定は以下のとおり.

  • SSID: iPhone7PlusBlack
  • PASSWORD: qatacygd60y8u

macの設定は以下のとおり.

  • IP address: 172.20.10.4
  • router: 172.20.10.1

これに,ESP-WROOM-02から以下のようにUDPで接続する.

  • mac側UDPポート: 30000
  • ESP-WROOM-02 UDPポート: 31000
  • ESP-WROOM-02 IP address: 172.20.10.10

送信は,とりあえず1秒ごとに"abcd"という文字列を送信するだけ.
ESP-WROOM-02のプログラムは以下のとおり.
#include <esp8266wifi .h>
#include <wificlient .h>
#include <wifiudp .h>

WiFiUDP UDP;

IPAddress HOSTIP(172, 20, 10, 4);
const int remoteUdpPort = 30000;

IPAddress myIP(172, 20, 10, 10);
const int localUdpPort = 31000;

const char *ssid = "iPhone7PlusBlack";
const char *password = "qatacygd60y8u";

void connectWiFi() {
  Serial.println("WiFi setup start");
  WiFi.begin(ssid, password);
  WiFi.config(myIP, WiFi.gatewayIP(), WiFi.subnetMask());
  Serial.println("start_connect");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("CONNECTED!");
  Serial.println("WiFi setup done");
}

void setup() {
  Serial.begin(115200);
  Serial.println();
  Serial.println("Serial setup done");

  WiFi.mode(WIFI_STA);
  UDP.begin(localUdpPort);
  connectWiFi();
}

void sendWiFi(char byteData[]) {
  if (UDP.beginPacket(HOSTIP, remoteUdpPort)) {
    UDP.write(byteData);
    UDP.endPacket();
    Serial.println(byteData);
  }
}

void loop() {
  char a[4];
  a[0] = 'a';
  a[1] = 'b';
  a[2] = 'c';
  a[3] = 'd';
  sendWiFi(a);
  delay(1000);
  end_loop();
}

void end_loop() {
  if (WiFi.status() != WL_CONNECTED) {
    WiFi.disconnect();
    Serial.println("disconnect!");
    connectWiFi();
  }
}

mac側のPythonプログラムは以下のとおり.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Python 3.7.3 on macOS 10.15 Catalina Beta
# 2019/8/4
# receive JSON data by UDP

import argparse
import threading
import time
from socket import *

class ServerThread(threading.Thread):
    def __init__(self, ADDRESS="127.0.0.1", PORT=30000):
        threading.Thread.__init__(self)
        self.data = None
        self.kill_flag = False
        # line information
        self.ADDRESS = ADDRESS
        self.PORT = PORT
        self.BUFSIZE = 1024
        self.ADDR = (gethostbyname(self.ADDRESS), self.PORT)
        print(self.ADDR)
        # bind
        self.udpServSock = socket(AF_INET, SOCK_DGRAM)
        self.udpServSock.bind(self.ADDR) # HOST, PORTでbinding

    def run(self):
        while True:
            try:
                # データ受信
                data, self.addr = self.udpServSock.recvfrom(self.BUFSIZE)
                if len(data) > 0:
                    print("receive {} bytes".format(len(data)))
                    print("received data: {}".format(data))
                # OSCはデコードできなくてexceptionが起こる
                # JSONだとテキストなのでデコードできる
                self.data = data.decode()
                print("decoded data: {}".format(self.data))
            except:
                print("exception occrurred")
                #pass

def start_UDP_server(ip_address, port_number):
    print("starting on address {}, port {}".format(ip_address, port_number))
    th = ServerThread(ip_address, port_number)
    th.setDaemon(True)
    th.start()

    while True:
        time.sleep(1.0)
        if not th.data:
            continue
        print(th.data)

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--ip", default="127.0.0.1",
                        help="IP address to listen on")
    parser.add_argument("--port", type=int, default=30000,
                        help="port to listen on")
    args = parser.parse_args()
    start_UDP_server(args.ip, args.port)

mac側で,ターミナル上でプログラムを以下のように立ち上げると,mac側では以下のように表示される.

% ./UDPserver1.py --ip 172.20.10.4 --port 30000
starting on address 172.20.10.4, port 30000
('172.20.10.4', 30000)
receive 4 bytes
received data: b'abcd'
decoded data: abcd
abcd
receive 4 bytes
received data: b'abcd'
decoded data: abcd
abcd
receive 4 bytes
received data: b'abcd'
decoded data: abcd
abcd
^C


一方,Arduino側のシリアルモニタでは以下のように表示される.

Serial setup done
WiFi setup start
start_connect
.......CONNECTED!
WiFi setup done
abcd
abcd
abcd


参照ページ:



秋月のESP-WROOM-02開発キットをmac OS上のArduino IDEから使えるようにする

Arduino開発環境からArduinoと同様にプログラミングができて,WiFiやBluetoothが使えるマイコンボードとして,秋月電子通商でESP-WROOM-02開発キットを購入しました.このボードは書き込み用のUSBジャックまで付いているので,パソコンとUSBケーブルで接続するだけで,すぐにプログラミング・書き込みが可能です.

ただ,素のArduino開発環境ではこのボードに対応していないので,追加インストールが必要です.

まず,Arduino IDEをインストールします.http://www.arduino.ccにアクセスして,上のメニューから「SOFTWARE」→「DOWNLOADS」をクリックします.

使用環境がmacOS 10.15 (Catalina) betaなので,ここではnightly buildsをダウンロード・インストールします.

zipファイルをダブルクリックするとアプリケーションが解凍されます.これを/Applicationsディレクトリにdrag&dropし,ダブルクリックして起動します.起動の際に,下記のようなワーニングが出て起動できません.
「システム環境設定」アプリの「セキュリティとプライバシー」にアクセスし,「ダウンロードしたアプリケーションの実行許可」で実行許可を出します(結局,自分の環境ではこれでも動かず,SIPをdisableにして実行しました).

環境設定から,ESP8266の開発ができるように追加のボードマネージャーをインストールします.まず,Arduino IDEを起動したら,メニューバーの「Arduino」をクリックし,「Preferences」をクリックして環境設定画面を開きます.「追加のボードマネージャのURL:」欄に以下のように記入します.

http://arduino.esp8266.com/stable/package_esp8266com_index.json

OKボタンを押して環境設定を終了します.

次に,メニューバーの「ツール」→「ボード」から「ボードマネージャ」をクリックし,選択します.

ボードマネージャーが開いたら,一番下までスクロールし,esp8266を確認し,「インストール」ボタンをクリックします.
ボードの定義は44Mbytesほどあるので,待ちます.ダウンロードが終了したら,メニューバーの「ツール」→「ボード」→「Generic ESP8266 Module」が選択可能になっているので,これを選択します.
「ツール」メニューで設定を以下のように書き換えます.
  • 書き換え場所:
  • Upload Speed: 115200 → 921600
  • CPU Frequency: 80MHz → 160MHz
  • Flash Size: 512K (no SPIFFS) → 4M (3M SPIFFS)
  • Flash Mode: DIO → QIO (fast)
  • Flash Frequency: 40MHz → 80MHz


書き換え前:
書き換え後:

シリアルポートを設定します./dev/cu.usbserial-????????に設定します.

これで設定が終了しました.

プログラムの書き込みの際には,以下のようにボタンを押して書き込みモードにする必要があります.
  1. デフォルト状態:RSTとPGMが両方押されていない
  2. リセット状態:RSTとPGMが両方押されている
  3. 書き込み準備状態:PGMが押されて,RSTは離されている.
  4. 書き込み可能状態:RSTとPGMが両方押されていない.

一言でいうと,PGMボタンを押した状態でRSTボタンを押して離し,そのあとでPGMボタンを離すと書き込み可能状態になります.

書き込みの一連の操作は以下のようになります.ここでは,Arduinoを立ち上げてプログラムを何も書いてない状態で書き込みのテストをしてみます.

まず,Arduinoで書き込み操作を行います.ウィンドウ下部に「Connecting......」というメッセージが表示されます.

ここで上記のボタンの操作を行うと,書き込みが始まります.

以下のように表示されれば書き込み終了です.

プログラムを書き込んだ後,そのままではプログラムの動作が始まらないようです.RSTボタンを一回押すと動作が始まります.

これで,Arduino IDEでESP-WROOM-02ボードの開発ができるようになりました.

2019年8月10日土曜日

Processing言語で3Dアニメーション ー 複数の描画パネルを使用する

Processing言語で,ウィンドウ上にこの記事の動画を,それぞれ複数の描画パネルに貼り付けてみました.通常の描画関数は実はPGraphics3Dクラスのメソッドになっているので,明示的にPGraphics3Dクラスのインスタンスを複数作成し,インスタンスを指定してメソッド呼び出しを行うと,複数の描画画面を使用することができます.

なお,PGraphics3Dクラスのインスタンスは実画面に繋がっていないgraphics contextのようで,最後にimage()関数で実ウィンドウにまとめて描画します.

ソースコードは以下のとおりです.重複するコードが多いので,Javaのサブラクスを作成すると,もっと簡潔になると思います.

// 2019/8/10
// 明示的にPGraphicsを用いて描画する
// 2つのPGraphicsを作って,別の描画をする

int loopCount; // draw()が呼ばれるたびにインクリメントされる変数
PGraphics3D pg1, pg2;
int panelWidth, panelHeight;

void setup() {
  size(950, 500, P3D); // 400x400のウィンドウ
  panelWidth = 400;
  panelHeight = 400;
  pg1 = (PGraphics3D)createGraphics(panelWidth, panelHeight, P3D);
  pg2 = (PGraphics3D)createGraphics(panelWidth, panelHeight, P3D);
  loopCount = 0;
  // 日本語フォントが効かない.サイズも反映されない.
  PFont font = createFont("MS Gothic", 48, true);
  pg1.textFont(font);
  pg1.textSize(24);
  pg1.hint(ENABLE_DEPTH_SORT); // z bufferが視点方向で正常に機能するように
  pg2.textFont(font);
  pg2.textSize(24);
  pg2.hint(ENABLE_DEPTH_SORT); // z bufferが視点方向で正常に機能するように
  frameRate(60);
}

void draw() {
  loopCount++;
  //
  pg1.beginDraw();
  pg1.background(192); // light grayの背景
  // ウィンドウの中心が(x, y)の原点になる
  pg1.translate(panelWidth/2, panelHeight/2, 0); // 原点を画面の中身にずらす
  // 座標軸を回転する(Processingは左手系)
  // x:右, y:下, z:手前 -> x:右, y:手前, z:上
  pg1.rotateX(PI/2);
  // この座標系で視点だけ回転させる
  // z軸が上の斜め上方向から原点を眺める
  pg1.camera(300.0*cos(radians(loopCount)),
          300.0*sin(radians(loopCount)),
          300.0,
          0.0, 0.0, 0.0,
          0.0, 0.0, -1.0);

  // 座標軸の描画
  pg1.fill(255, 0, 0); // 赤
  pg1.textAlign(CENTER); // x方向をセンタリング,y方向の座標はベースライン
  pg1.text("x軸", 150, 0, 0); // XY平面上に書く
  pg1.text("y軸", 0, 150, 0); // XY平面上に書く 
  pg1.text("z軸", 0, 0, 150); // Z=150のXY平面上に書く
  pg1.stroke(0, 0, 0); // 黒線
  pg1.line(0, 0, 0, 130, 0, 0); // X軸
  pg1.line(0, 0, 0, 0, 130, 0); // Y軸
  pg1.line(0, 0, 0, 0, 0, 145); // Z軸

  // 3Dオブジェクトの描画
  pg1.fill(0x7F0000FF); // 青の塗り潰し,透明度半分
  pg1.stroke(0, 255, 0); // 緑線
  pg1.box(100, 100, 100); // 原点中心,一辺のサイズ100のの立方体

  pg1.translate(-100, -100, 0); // 原点をずらす
  pg1.stroke(0, 0, 255); // 青線
  pg1.fill(0x7F00FFFF); // シアンの塗り潰し,透明度半分
  pg1.sphere(100); // 原点中心,半径100の球
  pg1.endDraw();
  //
  pg2.beginDraw();
  pg2.background(192); // light grayの背景
  // ウィンドウの中心が(x, y)の原点になる
  pg2.translate(panelWidth/2, panelHeight/2, 0); // 原点を画面の中身にずらす
  // 座標軸を回転する(Processingは左手系)
  // x:右, y:下, z:手前 -> x:右, y:手前, z:上
  pg2.rotateX(PI/2);
  // この座標系で視点だけ回転させる
  // z軸が上の斜め上方向から原点を眺める
  pg2.camera(300.0*cos(radians(-loopCount)),
          300.0*sin(radians(-loopCount)),
          300.0,
          0.0, 0.0, 0.0,
          0.0, 0.0, -1.0);

  // 座標軸の描画
  pg2.fill(255, 0, 0); // 赤
  pg2.textAlign(CENTER); // x方向をセンタリング,y方向の座標はベースライン
  pg2.text("x軸", 150, 0, 0); // XY平面上に書く
  pg2.text("y軸", 0, 150, 0); // XY平面上に書く 
  pg2.text("z軸", 0, 0, 150); // Z=150のXY平面上に書く
  pg2.stroke(0, 0, 0); // 黒線
  pg2.line(0, 0, 0, 130, 0, 0); // X軸
  pg2.line(0, 0, 0, 0, 130, 0); // Y軸
  pg2.line(0, 0, 0, 0, 0, 145); // Z軸

  // 3Dオブジェクトの描画
  pg2.fill(0x7FFF0000); // 赤の塗り潰し,透明度半分
  pg2.stroke(0, 255, 0); // 緑線
  pg2.box(100, 100, 100); // 原点中心,一辺のサイズ100のの立方体

  pg2.translate(-100, -100, 0); // 原点をずらす
  pg2.stroke(255, 0, 0); // 赤線
  pg2.fill(0x7FFF00FF); // マゼンタの塗り潰し,透明度半分
  pg2.sphere(100); // 原点中心,半径100の球
  pg2.endDraw();
  //
  image(pg1, 50, 50); // PGに描いた絵を表示
  image(pg2, 500, 50); // PGに描いた絵を表示
}


出来上がったGIFアニメーションは以下のとおりです.1つのウィンドウに2つの描画領域を作成して,それぞれの領域で別のアニメーションを描画しています.以下の不具合があります.

  • フォントの指定がうまくいかない
  • 半透明描画ができていない.


上記のGIFアニメーションを作成するために書き換えたソースコードは以下のとおりです.

// 2019/8/10
// 明示的にPGraphicsを用いて描画する
// 2つのPGraphicsを作って,別の描画をする

import gifAnimation.*;

GifMaker gifExport;
int loopCount; // draw()が呼ばれるたびにインクリメントされる変数
PGraphics3D pg1, pg2;
int panelWidth, panelHeight;

void setup() {
  size(950, 500, P3D); // 400x400のウィンドウ
  panelWidth = 400;
  panelHeight = 400;
  pg1 = (PGraphics3D)createGraphics(panelWidth, panelHeight, P3D);
  pg2 = (PGraphics3D)createGraphics(panelWidth, panelHeight, P3D);
  loopCount = 0;
  // 日本語フォントが効かない.サイズも反映されない.
  PFont font = createFont("MS Gothic", 48, true);
  pg1.textFont(font);
  pg1.textSize(24);
  pg1.hint(ENABLE_DEPTH_SORT); // z bufferが視点方向で正常に機能するように
  pg2.textFont(font);
  pg2.textSize(24);
  pg2.hint(ENABLE_DEPTH_SORT); // z bufferが視点方向で正常に機能するように
  frameRate(60);
  
  gifExport = new GifMaker(this, "export.gif");
  gifExport.setRepeat(0); // 無限ループ
  gifExport.setQuality(10); // default値
  gifExport.setDelay(30); // 30ms単位で1コマ(1秒33コマ)
  gifExport.setTransparent(0, 0, 0); // 黒は透過色
}

void draw() {
  loopCount++;
  //
  pg1.beginDraw();
  pg1.background(192); // light grayの背景
  // ウィンドウの中心が(x, y)の原点になる
  pg1.translate(panelWidth/2, panelHeight/2, 0); // 原点を画面の中身にずらす
  // 座標軸を回転する(Processingは左手系)
  // x:右, y:下, z:手前 -> x:右, y:手前, z:上
  pg1.rotateX(PI/2);
  // この座標系で視点だけ回転させる
  // z軸が上の斜め上方向から原点を眺める
  pg1.camera(300.0*cos(radians(loopCount)),
          300.0*sin(radians(loopCount)),
          300.0,
          0.0, 0.0, 0.0,
          0.0, 0.0, -1.0);

  // 座標軸の描画
  pg1.fill(255, 0, 0); // 赤
  pg1.textAlign(CENTER); // x方向をセンタリング,y方向の座標はベースライン
  pg1.text("x軸", 150, 0, 0); // XY平面上に書く
  pg1.text("y軸", 0, 150, 0); // XY平面上に書く 
  pg1.text("z軸", 0, 0, 150); // Z=150のXY平面上に書く
  pg1.stroke(0, 0, 0); // 黒線
  pg1.line(0, 0, 0, 130, 0, 0); // X軸
  pg1.line(0, 0, 0, 0, 130, 0); // Y軸
  pg1.line(0, 0, 0, 0, 0, 145); // Z軸

  // 3Dオブジェクトの描画
  pg1.fill(0x7F0000FF); // 青の塗り潰し,透明度半分
  pg1.stroke(0, 255, 0); // 緑線
  pg1.box(100, 100, 100); // 原点中心,一辺のサイズ100のの立方体

  pg1.translate(-100, -100, 0); // 原点をずらす
  pg1.stroke(0, 0, 255); // 青線
  pg1.fill(0x7F00FFFF); // シアンの塗り潰し,透明度半分
  pg1.sphere(100); // 原点中心,半径100の球
  pg1.endDraw();
  //
  pg2.beginDraw();
  pg2.background(192); // light grayの背景
  // ウィンドウの中心が(x, y)の原点になる
  pg2.translate(panelWidth/2, panelHeight/2, 0); // 原点を画面の中身にずらす
  // 座標軸を回転する(Processingは左手系)
  // x:右, y:下, z:手前 -> x:右, y:手前, z:上
  pg2.rotateX(PI/2);
  // この座標系で視点だけ回転させる
  // z軸が上の斜め上方向から原点を眺める
  pg2.camera(300.0*cos(radians(-loopCount)),
          300.0*sin(radians(-loopCount)),
          300.0,
          0.0, 0.0, 0.0,
          0.0, 0.0, -1.0);

  // 座標軸の描画
  pg2.fill(255, 0, 0); // 赤
  pg2.textAlign(CENTER); // x方向をセンタリング,y方向の座標はベースライン
  pg2.text("x軸", 150, 0, 0); // XY平面上に書く
  pg2.text("y軸", 0, 150, 0); // XY平面上に書く 
  pg2.text("z軸", 0, 0, 150); // Z=150のXY平面上に書く
  pg2.stroke(0, 0, 0); // 黒線
  pg2.line(0, 0, 0, 130, 0, 0); // X軸
  pg2.line(0, 0, 0, 0, 130, 0); // Y軸
  pg2.line(0, 0, 0, 0, 0, 145); // Z軸

  // 3Dオブジェクトの描画
  pg2.fill(0x7FFF0000); // 赤の塗り潰し,透明度半分
  pg2.stroke(0, 255, 0); // 緑線
  pg2.box(100, 100, 100); // 原点中心,一辺のサイズ100のの立方体

  pg2.translate(-100, -100, 0); // 原点をずらす
  pg2.stroke(255, 0, 0); // 赤線
  pg2.fill(0x7FFF00FF); // マゼンタの塗り潰し,透明度半分
  pg2.sphere(100); // 原点中心,半径100の球
  pg2.endDraw();
  //
  image(pg1, 50, 50); // PGに描いた絵を表示
  image(pg2, 500, 50); // PGに描いた絵を表示
  
  gifExport.addFrame(); // gifアニメーションに現在のフレームを追加
  if (loopCount >= 360) {
    gifExport.finish(); // 1回転したらアニメーション終了
  }
}


1回転だけした時点で,無限繰り返し再生を行うアニメーション画像を書き出しています.

Processing言語で3Dグラフィック ー 視点を回転させるアニメーション

Processing言語で3次元グラフィックが描けるので試してみた.

とりあえず,半透明の立方体と球体を置いて,カメラ視点を変化させてアニメーションにしてみる.XYZ軸も描いて,向きがわかるようにしてみた.

Processingの座標系は,左上が原点,右向きがX軸の正方向,下向きがY軸の正方向,手前向きがZ軸の正方向という左手系の座標系になっている.行列を使って座標軸を変換する方法が良くわからなかったので,とりあえず,右手系のまま,画面中心を原点にして,画面の上方向がZ軸になるように回転すると,X軸が右方向,Y軸が手前方向になった.回転だけでは,X軸が右方向,Y軸が画面の奥方向,Z軸が上方向という右手系にできないので,とりあえずこの座標系で話を進めることにする.

アニメーションの際は,座標系,物体の座標は変化させずに,カメラの座標と向きだけを変化させている,カメラの位置はZ軸方向300の高さで,半径300の円周上を動かし,向きは常に原点方向.

こうやって書いたソースが以下のとおり.

// 2019/8/9

int loopCount; // draw()が呼ばれるたびにインクリメントされる変数

void setup() {
  size(400, 400, P3D); // 400x400のウィンドウに3D描画
  loopCount = 0;
  // 日本語フォント
  PFont font = createFont("MS Gothic", 48, true);
  textFont(font);
  textSize(24);
  hint(ENABLE_DEPTH_SORT); // z bufferが視点方向で正常に機能するように
}

void draw() {
  loopCount++;
  background(192); // light grayの背景
  // ウィンドウの中心が(x, y)の原点になる
  translate(width/2, height/2, 0); // 原点を画面の中身にずらす
  // 座標軸を回転する(Processingは左手系)
  // x:右, y:下, z:手前 -> x:右, y:手前, z:上
  rotateX(PI/2);
  // この座標系で視点だけ回転させる
  // z軸が上の斜め上方向から原点を眺める
  camera(300.0*cos(radians(loopCount)),
          300.0*sin(radians(loopCount)),
          300.0,
          0.0, 0.0, 0.0,
          0.0, 0.0, -1.0);

  // 座標軸の描画
  fill(255, 0, 0); // 赤
  textAlign(CENTER); // x方向をセンタリング,y方向の座標はベースライン
  text("x軸", 150, 0, 0); // XY平面上に書く
  text("y軸", 0, 150, 0); // XY平面上に書く 
  text("z軸", 0, 0, 150); // Z=150のXY平面上に書く
  stroke(0, 0, 0); // 黒線
  line(0, 0, 0, 130, 0, 0); // X軸
  line(0, 0, 0, 0, 130, 0); // Y軸
  line(0, 0, 0, 0, 0, 145); // Z軸

  // 3Dオブジェクトの描画
  fill(0x7F0000FF); // 青の塗り潰し,透明度半分
  stroke(0, 255, 0); // 緑線
  box(100, 100, 100); // 原点中心,一辺のサイズ100のの立方体

  translate(-100, -100, 0); // 原点をずらす
  stroke(0, 0, 255); // 青線
  fill(0x7F00FFFF); // シアンの塗り潰し,透明度半分
  sphere(100); // 原点中心,半径100の球
}


これをGIFアニメーションで表示すると以下のとおりになった.

Processingのアニメーションからgifアニメーション画像を保存する方向はこちらを参照した.ここで使うgifAnimationライブラリは,Processing Ver.3では動かず,Processing Ver.2で動かす必要がある(Ver.3対応版はこちらにあった).追加ライブラリgifAnimationライブラリの置き場のディレクトリは,自分のスケッチブックの保存ディレクトリの中のlibrariesディレクトリである.ここに,拾ってきたアーカイブファイルを解凍して出てきたgifAnimationディレクトリ以下をコピーすれば良い.書き換えた後のソースコードは以下のとおり.

// 2019/8/10

import gifAnimation.*;

GifMaker gifExport;
int loopCount; // draw()が呼ばれるたびにインクリメントされる変数

void setup() {
  size(400, 400, P3D); // 400x400のウィンドウに3D描画
  loopCount = 0;
  // 日本語フォント
  PFont font = createFont("MS Gothic", 48, true);
  textFont(font);
  textSize(24);
  hint(ENABLE_DEPTH_SORT); // z bufferが視点方向で正常に機能するように
  gifExport = new GifMaker(this, "export.gif");
  gifExport.setRepeat(0); // 無限ループ
  gifExport.setQuality(10); // default値
  gifExport.setDelay(30); // 30ms単位で1コマ(1秒33コマ)
  gifExport.setTransparent(0, 0, 0); // 黒は透過色
}


void draw() {
  loopCount++;
  background(192); // light grayの背景
  // ウィンドウの中心が(x, y)の原点になる
  translate(width/2, height/2, 0); // 原点を画面の中身にずらす
  // 座標軸を回転する(Processingは左手系)
  // x:右, y:下, z:手前 -> x:右, y:手前, z:上
  rotateX(PI/2);
  // この座標系で視点だけ回転させる
  // z軸が上の斜め上方向から原点を眺める
  camera(300.0*cos(radians(loopCount)),
          300.0*sin(radians(loopCount)),
          300.0,
          0.0, 0.0, 0.0,
          0.0, 0.0, -1.0);

  // 座標軸の描画
  fill(255, 0, 0); // 赤
  textAlign(CENTER); // x方向をセンタリング,y方向の座標はベースライン
  text("x軸", 150, 0, 0); // XY平面上に書く
  text("y軸", 0, 150, 0); // XY平面上に書く 
  text("z軸", 0, 0, 150); // Z=150のXY平面上に書く
  stroke(0, 0, 0); // 黒線
  line(0, 0, 0, 130, 0, 0); // X軸
  line(0, 0, 0, 0, 130, 0); // Y軸
  line(0, 0, 0, 0, 0, 145); // Z軸

  // 3Dオブジェクトの描画
  fill(0x7F0000FF); // 青の塗り潰し,透明度半分
  stroke(0, 255, 0); // 緑線
  box(100, 100, 100); // 原点中心,一辺のサイズ100のの立方体

  translate(-100, -100, 0); // 原点をずらす
  stroke(0, 0, 255); // 青線
  fill(0x7F00FFFF); // シアンの塗り潰し,透明度半分
  sphere(100); // 原点中心,半径100の球
  
  gifExport.addFrame(); // gifアニメーションに現在のフレームを追加
  if (loopCount >= 360) {
    gifExport.finish(); // 1回転したらアニメーション終了
  }
}


2019年8月7日水曜日

9軸センサー(加速度,ジャイロ,地磁気)のデータを使う (2)

前の投稿と並行して,クライアントの構築が一番簡単な,iPhoneとZIG SIMアプリを使った9軸データのやり取りをやってみました.

iPhoneのZIG SIMアプリで9軸データを送り,macのPythonプログラムで受信してみます.mac側のプログラムは以下のとおり.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Python 3.7.3 on macOS 10.15 Catalina Beta
# 2019/8/7
# receive OSC data by UDP

import argparse
from socket import *
from pythonosc import dispatcher
from pythonosc import osc_server

def handle_zigsim_osc_message(addr, *args):
    try:
        print("{0}: {1}".format(addr, args))
    except ValueError:
        print("ValueError occrurred")

def start_OSC_server(ip_addr, port_num):
    print("starting on address {}, port {}".format(ip_addr, port_num))

    # OSC dispatcher
    dispatch = dispatcher.Dispatcher()
    dispatch.map("/ZIGSIM/*", handle_zigsim_osc_message )

    server = osc_server.ThreadingOSCUDPServer(
        (ip_addr, port_num), dispatch)
    print("Serving on {}".format(server.server_address))
    server.serve_forever()

if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--ip", default="127.0.0.1",
                        help="IP address to listen on")
    parser.add_argument("--port", type=int, default=30000,
                        help="port to listen on")
    args = parser.parse_args()
    start_OSC_server(args.ip, args.port)

クライアントのZIG SIMアプリの設定(送信データ)は以下のとおり.


ZIG SIMアプリの設定(プロトコル)は以下のとおり.IP ADDRESSにはパソコンのIPアドレスを入れます.ポート番号は30000にしました.


macでサーバープログラムを動かし,iPhoneでクライアントを動かすと,iPhoneから9軸データがサーバーに送られます.このときのクライアントの画面は以下のとおり.


サーバー側では,以下のようにデータが表示されて,ちゃんとデータがOSC形式で受信できていることが分かります.


現在のiPhone上でうごかしてZIG SIMを,「9軸センサー(加速度,ジャイロ,地磁気)のデータを使う (1)」で作成中のArduinoを使ったハードウェアで置き換えるのが目標です.

9軸センサー(加速度,ジャイロ,地磁気)のデータを使う (1)

秋月電子で9軸センサーBMX055モジュールを購入してみました.これは,加速度3軸,ジャイロ3軸,地磁気3軸のセンサーで,Arduinoから簡単にデータが取れるようです.チップ自体は3.3V電源ですが,モジュール上にFXMA2102が載っていて,I2Cの電圧変換をしてくれるので,Arduinoの5V入出力でも使えるようです.

まず,マニュアルに従ってJP4, 5, 6を半田でショートした後,ジャンパーピンを接続し,配線しました.Arduino側がこんな感じ.

BMX055側はこんな感じ.

全体はこんな感じ.

とりあえず,サンプルソースコードを取得して,このソースコードに書いてある通りにArduinoと結線しました.

Arduinoのシリアルコンソールを開くと,9軸の値がちゃんと取れているようです.

あとやることは,以下のとおりです.

  • ジャイロのデータを使って,加速度から,重力加速度とモジュール自体の加速を分離する.
  • OSCデータで送るようにする.
  • WiFiでパソコンに飛ばす.
最終的には,ZIG SIMの置き換えを狙ってます.



2019年7月25日木曜日

Raspberry Piで作るデジタルオーディオシステムのメモ

Raspberry Piで作るデジタルオーディオシステムの現状をざっくりとまとめてみました.

  • 本体
2019年7月現在,Raspberry Pi 4Bが発表されているものの,日本では未発売なので,3B3B+になるでしょうか.両者ともWiFiとbluetooth内蔵です.最初の設定の時だけはディスプレイが必要です.SSHでリモートログインできるようになったらディスプレイは不要です.また,ACアダプターとmicroSDカードも忘れないように購入しましょう.

なお,4Bと3B/3B+はディスプレイ端子と電源端子の形状が変更されているので,ケースに互換性はありません.3Bと3B+も若干形状が異なっており,このケースに3B+を入れるには,加工が少し必要なようです.

  • OS
Raspberry Piをパソコン代わりに使う際には,Debian LinuxベースのRaspbianが提供されていますが,Raspberry Piをオーディオサーバーとして用いる際に最も良く使われているOS(基本ソフトウェア)がVolumioです.日本語のWeb情報も多いです.他にはMoode AudioRuneAudioがあります.面白いのは,OSを変えると音も変わることです.microSDカードを複数枚用意して,OSを入れ替えながら使ってみると良いでしょう.

  • I2S-DAC
I2SというのはIC間でデジタル音声データを伝送するプロトコルです.この仕様のデータを受け入れて,Raspberry PiのGPIOピンに直接挿せるボードがあり,これで,Raspberry Piのデジタルオーディオをアナログに変換します.パソコン+USB-DACと違うのは,USBケーブルにはノイズがいっぱい乗っていると言う話があり,このようなUSBラインのノイズフィルターなどが売られていますが,I2SだとUSBラインのノイズは乗りません(Raspberry Pi自体のデジタル部分のノイズは乗ると思います).

2018年にRaspberry Piオーディオケースの規格ができてこの手のケースを使うと,DACつきのRaspberry Piをちゃんとケースに入れられるようになりました.

気を付けることは,I2Sはシステムクロック,ビットクロック,LRクロック,データ信号の4本の線を使うのですが,Raspberry Piからはシステムクロックが出力されないので,システムクロック入力が不要なDACを選ぶ必要があります.代表的なのがDACチップとしてPCM5122 (32bit, 384kHzまで入力可能)を用いたこのようなボードです.ただし,PCM5122を用いても,このボードのようにシステムクロックをチップの外部から入力することもできます.DACチップとしてPCM5122を用いていれば,Raspberry PiのOSからはHiFiBerry DACもしくはHiFiBerry DAC+として人気できるようです.

PCM5122でも頑張れば,このボードこのボードを組み合わせて,DACボード側でシステムクロックを生成してRaspberry Pi側に送ることもでき,より精度の高いクロックを供給することができます.

注意:デジタル音楽データの伝送エラーは,基本的にほとんど起きないのですが,クロックのゆらぎ(クロックジッタと呼ばれる)があると,これがアナログの波形に影響を与えます.デジタルオーディオの世界では,ルビジウムを用いたマスタークロック供給機器などもあります.

  • 音楽データを保存する機器(NASもしくはタブレットPC,スマホなど)
本体をmicroSDカードで運用する場合に,別に音楽データを入れるのに使います.

NASを用いる場合
Raspberry Piと2.5inch SSD/HDDを内蔵できるこういうケースもあります.電源スイッチも付いているので,Raspberry Pi自体をNASとして運用するには良いと思います.パソコンをNASサーバーとして用いて,パソコンに入れた音楽データにRaspberry Piからアクセスさせることもできます.

スマホなどから音楽データを飛ばす場合
なお,Raspberry Pi上のOSにVolumioなど前述のOSを使うと,AirPlayという機能が使えるので,スマホの音声出力をRaspberry Piに飛ばして,スマホに入っている音楽データをRaspberry Pi経由で再生できます.

  • アンプ
Raspberry Piに合わせるなら,デジタルアンプがコスパが良いと思います.安価な割に定評のあるTA2020を使った物としては,例えばNFJのこれなどでしょうか.最近流行っているオペアンプ差し替えですが,定番はOPA627への交換です.半田付けを頑張るならここあたり,DIP製品がよければこれを2個とこの変換基板を購入します.オペアンプ差し替え済みのこれとか,かなり良いと思います.

TA2020を使った組み立てキットもあるようです.

  • スピーカー
値段別にリストアップしてみます.

1万円コース
  1. スピーカーユニットは毎年7月に発売されている音楽の友社ONTOMO MOOKのスピーカーユニット(2019年はマークオーディオ製8cmスピーカー(Amazon)).スピーカーボックスはFOSTEXの8cm用バスレフ箱P800-Eを2個(Amazon).
  2. FOSTEXかんすぴセットP802-S(Amazon).
  3. その他,8cmだとコイズミ無線で色々選べます.
2万円コース
  1. 2019年7月に,FOSTEXの定番FE83シリーズの新製品FE83NVが出ました.これとバックロードホーンエンクロージャー(完成品組み立て品)もしくはダブルバスレフ型エンクロージャーを組み合わせると良いと思います.
  2. 上記の10cmタイプだとFE103NVバスレフ型エンクロージャー (各2個ずつ)が2万円で構成可能です.
3万円〜5万円コース

この値段になると,オーディオメーカー製のそこそこのスピーカーが入手できます.代表的な物として,以下を挙げておきます.
  1. DALI ZENSOR1 (Amazon).
  2. TEAC S-300NEO (Amazon).
  3. TANNOY MERCURY 7.1 (Amazon)
  4. FOSTEX FE103NV + FOSTEXエンクロージャー (各2個ずつ)

  • 最後に
Raspberry Piでオーディオをやってみるなら,この連載が網羅的にまとまっています.

2019年4月28日日曜日

LTspice XVII on mac関係の記事

LTspice XVII (mac version)関係の記事をまとめたページを作成しました.

LTspice XVII on mac関係の記事一覧

です.

LTspice XVII on macで使う8pinオペアンプのシンボル作成と使用

LTspiceXVIIに含まれているオペアンプのシンボルは正負入力と出力の三端子のものと,正負電源,正負入力,出力の五端子の物しかないので,8ピンに2回路入ったオペアンプのシンボルを作成してみました.

以下では,前の記事で作った1回路入りnjm4558を使って,2回路入りnjm4558のシンボルを作ります.

まず,1回路入りnjm4558を使って,以下のような回路図を作ります.これは一般的に日本で販売されている2回路入りオペアンプの回路図です.ファイル名「njm4558double.asc」として保存します.

図.2回路入りnjm4558の回路図

※上記回路のU2の方については,信号の±,電源の±ともに逆になってます.そのうち回路図を直しますので,作成の際には気をつけてください.
このとき,OUT1などの文字列は「Draft」→「Net Name」で作ります.Port Typeは(none)のままにしておきます.「OK」ボタンをクリックするとマウスカーソルが入力した文字列になるので,文字列の端に付いている小さい正方形を配線の端点に重なるように置きます.すると,文字列は自動的に配線と重ならないように配置されます.

図.端子の名前付け

次に,この回路のためのシンボルを作成します.メニューから「File」→「New」→「New Symbol」をマウスで選択すると,次のようなシンボル編集ウィンドウが開きます.

図.シンボル編集ウィンドウ

次のような図を描きます.

図.シンボル図

右クリック→「Draw」から「Rectangle(四角形)」「Circle(円)」「Arc(円弧)」を用います.描き方は以下の通りです.

Rectangle:
メニューからRectangleを選択した後,左上の座標から右下の座標にマウスをドラッグする.

Circle:
メニューからCircleを選択した後,円が内接する正方形の左上の座標から右下の座標にマウスをドラッグする.

Arc:
メニューからArcを選択した後,この円弧が一部となっている円が内接する正方形の左上の座標,右下の座標をクリックする.その後で円弧の開始点,終了点(たぶん反時計回り)をクリックする.この場合は下半円なので,左,右をクリックする.

初期状態では描かれる線がなぜか点線なので,これを実線に修正します.図形の上で右クリックすると,次の図のようなポップアップが表示されるので,「Dot」を「Solid」に変更して「OK」ボタンをクリックします.

図.線種の変更ポップアップ

全て実線に直します.最後にラベルを書いておきます.マウスを右クリックしてメニューから「Draw」→「Text」を選択し,njm4558と書いて右上に置きます.以上で描画は終了です.以下の図のようになります.

図.シンボルの描画が終了した状態

次に端子を追加します.左上から反時計回りに先ほど作成した回路図のNet Nameと同じ名前の端子を付けていきます.右クリックして「Add Pin」をクリックすると(ショートカットはpキー)次の図のようにポップアップダイアログが表示されるので,名前を入力します.左側の端子(上からOUT1,-IN1,+IN1,V-)は位置としてRIGHTを選択します.このRIGHTは文字列に対する端子の位置です.OKを押して,上から順にシンボルの図の上に置いていきます.

図.端子の追加(左側)

右側の端子(下から+IN2,-IN2,OUT2,V+)は文字列に対する端子の位置としてLEFTを指定します.

図.端子の追加(右側)

このとき重要なのが,ピンを追加する毎にNetlist Orderが1ずつ増えていくので,上記の順序(DIP ICのピン番号の順序)でピンを追加することです.

全てのピンを追加すると,シンボル図は以下の図のようになります.

図.2回路入りnjm4558のシンボル図

これで,シンボル図の編集画面からマウスで右クリック→「Hierarchy」→「Open This Symbol's Schematic」を選択すると回路図の編集画面が表示され,回路図の編集画面からマウスで右クリック→「Hierarchy」→「Open This Sheet's Symbol」を選択するとシンボル図の編集画面が表示されます.これができない時は,同じディレクトリ内に拡張子だけが異なる二つのファイルnjm4558double.ascとnjm4558double.asyが保存されているか確認してください.ただし,回路図の編集画面からシンボル図の編集画面を呼び出すときに「Unknown symbol syntax: "Version 0"」と表示されます.「OK」ボタンをクリックするとそのままシンボル図の編集画面が開きますが,シンボル図の編集画面内でこれを修正する方法が見つからなかったので,LTspice XVIIを一度終了し,ファイルnjm4558double.asyをテキストエディタで開いて修正します.1行目の「Version 0」を「Version 4」に書き換えてください.

次に,作成したシンボルを回路作成時に呼び出せるようにライブラリに追加します.LTspice XVII macのディレクトリ(/Users/ユーザー名/Library/Application Support/LTspice/)の中のシンボルを置くディレクトリ(/Users/ユーザー名/Library/Application Support/LTspice/lib/sym/)の中にNJRというディレクトリを作成し,これら2個のファイルを置きます.macのFinderではホームディレクトリの中のLibraryディレクトリは表示されないので,Finderでホームディレクトリにいる状態で Finderのメニュー「移動」→「フォルダへ移動...」を選択し,「Library」を入力して「移動」ボタンをクリックするとLibraryディレクトリの中へ移動できます.

一度LTspice XVIIを終了,再度起動すると,[NJR]の中にnjm4558doubleが追加されていて選択できるようになっています.

図.回路図エディタから作成した2回路入りnjm4558の呼び出し

以下の図のように,反転増幅回路を作成します.回路図中の「V+」と「V-」「IN」「OUT」は「Net Name」から作成します.また,「SPICE directive」から「.tran 5m」という命令を作成しないとシミュレーションが動きません.

図.2回路入りnjm4558オペアンプで作成した反転増幅回路の回路図 

画面左上のシミュレーション実行ボタンを押すと波形ウィンドウが表示されます.フォーカスを回路図ウィンドウに戻して,赤鉛筆型の電圧プローブでINとOUTをクリックすると,以下の図のようにちゃんと反転増幅の入出力波形が表示されました.

図.シミュレーション結果

これで,三角形のオペアンプのシンボルを使った回路図でなく,8pinオペアンプICの図を使った,実体配線図に近い回路図が描けるようになりました.