いっぺーちゃんの いろいろやってみよ~

micropython on ESP32 でNTPサーバから時刻取得

前回、時刻を設定できるようにしたので、今回はNTPサーバから時刻を取得してみます。
これらを組み合わせると、時刻合わせが自動で行えるようになります。

以下のプログラムをntptime.pyという名前で保存し、upipmでインストールします。

import usocket as socket
try:
    import ustruct as struct
except:
    import struct

def time():
    # (datetime.date(1970, 1, 1) - datetime.date(1900, 1, 1)).days * 24 * 60 * 60
    NTP_DELTA = 2208988800
    # ntp server
    ntp_host = "ntp.nict.jp"
    
    NTP_QUERY = bytearray(48)
    NTP_QUERY[0] = 0x1b
    addr = socket.getaddrinfo(ntp_host, 123)[0][-1]
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    # s.settimeout(1)
    res = s.sendto(NTP_QUERY, addr)
    msg = s.recv(48)
    s.close()
    val = struct.unpack("!I", msg[40:44])[0]
    return val - NTP_DELTA

 

以下のように使用します。

import ntptime
import utime

# timeメソッドを呼び出すと経過秒数が取得できます
ntptime.time()
    ==> 1503019707
# その値をそのままutime.set_time()に渡せば時刻を設定できます。
utime.set_time(1503019707)
utime.localtime()
    ==> (2017, 8, 18, 10, 28, 27, 4, 230)

# 通常はこんな感じでまとめれば良いでしょう
utime.set_time(ntptime.time())
utime.localtime()
    ==>(2017, 8, 18, 10, 29, 4, 4, 230)

 

 

以下解説のようなものです。

 

必要なモジュールを読み込みます。

import usocket as socket
try:
    import ustruct as struct
except:
    import struct

 

timeメソッドを定義しています。

def time():
    ....

 

使用する定数です。
NTPサーバからは1900年1月1日00:00:00(UTC)からの経過秒数が送られてきますが、必要なのは1970年1月1日00:00:00(UTC)からの経過秒数なので、それを補正するための定数です。

    # (datetime.date(1970, 1, 1) - datetime.date(1900, 1, 1)).days * 24 * 60 * 60
    NTP_DELTA = 2208988800

 

接続するNTPサーバです。別のところに接続したければ変更してください。

    # ntp server
    ntp_host = "ntp.nict.jp"

 

NTPサーバに送信するqueryパケットを生成しています。

    NTP_QUERY = bytearray(48)
    NTP_QUERY[0] = 0x1b

 

NTPサーバと接続するためのアドレスとソケットを作成します。

    addr = socket.getaddrinfo(ntp_host, 123)[0][-1]
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

 

QUERYパケットを送信します。

    res = s.sendto(NTP_QUERY, addr)

 

サーバからのレスポンスを受信します。

    msg = s.recv(48)

 

ソケットをクローズします

    s.close()

 

受信データの40~44バイト目に経過秒数データ(正確に表現すると「送信タイムスタンプの整数部」)が入っているので、これを整数に変換しています(ネットワークバイトオーダの整数なので、unpackのフォーマットには「!I」が指定されています)。
変換結果を1970年1月1日00:00:00(UTC)からの経過秒数に変換した値を返します。

    val = struct.unpack("!I", msg[40:44])[0]
    return val - NTP_DELTA

 

 

 

 

 

 

 

micropython on ESP32 で時刻設定を可能にする

ESP32のmicropythonのutimeモジュールにはtimeという時刻(ある時点からの経過秒)を取得するメソッドがあるのですが、時刻を設定する方法が見当たりません。
「無いのなら作ってしまえ、ホトトギス」ということで作ってみました。
ついでに、経過秒から年月日時分秒を取得するlocaltimeメソッド、年月日時分秒から経過秒を取得するmktimeメソッドも作りました(というか、ESP8266からパクってきました)。

以下のパッチをあててmicropythonを再buildしてください。

diff --git a/esp32/modutime.c b/esp32/modutime.c
index f037faa..f1fd43f 100644
--- a/esp32/modutime.c
+++ b/esp32/modutime.c
@@ -31,7 +31,12 @@
 #include <sys/time.h>
 
 #include "extmod/utime_mphal.h"
+#include "lib/timeutils/timeutils.h"
+#include "py/nlr.h"
 
+/// \function time()
+/// get current time.
+/// It returns an integer which is the number of seconds since Jan 1, 1970.
 STATIC mp_obj_t time_time(void) {
     struct timeval tv;
     gettimeofday(&tv, NULL);
@@ -39,10 +44,98 @@ STATIC mp_obj_t time_time(void) {
 }
 MP_DEFINE_CONST_FUN_OBJ_0(time_time_obj, time_time);
 
+/// \function set_time(secs)
+/// set current time.
+/// argument is integer which is the number of seconds since Jan 1, 1970.
+STATIC mp_obj_t time_set_time(mp_obj_t arg) {
+    struct timeval tv;
+    mp_int_t sec = mp_obj_get_int(arg);
+
+    tv.tv_sec = sec;
+    tv.tv_usec = 0;
+    settimeofday(&tv, NULL);
+    return mp_const_none;
+}
+MP_DEFINE_CONST_FUN_OBJ_1(time_set_time_obj, time_set_time);
+
+/// \function localtime([secs])
+/// Convert a time expressed in seconds since Jan 1, 1970 into an 8-tuple which
+/// contains: (year, month, mday, hour, minute, second, weekday, yearday)
+/// If secs is not provided or None, then the current time from the RTC is used.
+/// year includes the century (for example 2014)
+/// month   is 1-12
+/// mday    is 1-31
+/// hour    is 0-23
+/// minute  is 0-59
+/// second  is 0-59
+/// weekday is 0-6 for Mon-Sun.
+/// yearday is 1-366
+STATIC mp_obj_t time_localtime(mp_uint_t n_args, const mp_obj_t *args) {
+    timeutils_struct_time_t tm;
+    mp_int_t seconds;
+    if (n_args == 0 || args[0] == mp_const_none) {
+        struct timeval tv;
+        gettimeofday(&tv, NULL);
+        seconds = tv.tv_sec;
+    } else {
+        seconds = mp_obj_get_int(args[0]);
+    }
+    seconds -= 946684800;        // 946684800 means seconds of 1st January 1970 00:00:00 to 1st January 2000 00:00:00
+    // reflect TimeZone
+    seconds += 9 * 60 * 60;                 // FIX to "JST"
+
+    timeutils_seconds_since_2000_to_struct_time(seconds, &tm);
+    mp_obj_t tuple[8] = {
+        tuple[0] = mp_obj_new_int(tm.tm_year),
+        tuple[1] = mp_obj_new_int(tm.tm_mon),
+        tuple[2] = mp_obj_new_int(tm.tm_mday),
+        tuple[3] = mp_obj_new_int(tm.tm_hour),
+        tuple[4] = mp_obj_new_int(tm.tm_min),
+        tuple[5] = mp_obj_new_int(tm.tm_sec),
+        tuple[6] = mp_obj_new_int(tm.tm_wday),
+        tuple[7] = mp_obj_new_int(tm.tm_yday),
+    };
+    return mp_obj_new_tuple(8, tuple);
+}
+MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(time_localtime_obj, 0, 1, time_localtime);
+
+/// \function mktime()
+/// This is inverse function of localtime. It's argument is a full 8-tuple
+/// which expresses a time as per localtime. It returns an integer which is
+/// the number of seconds since Jan 1, 2000.
+STATIC mp_obj_t time_mktime(mp_obj_t tuple) {
+
+    size_t len;
+    mp_obj_t *elem;
+
+    mp_obj_get_array(tuple, &len, &elem);
+
+    // localtime generates a tuple of len 8. CPython uses 9, so we accept both.
+    if (len < 8 || len > 9) {
+        nlr_raise(mp_obj_new_exception_msg_varg(&mp_type_TypeError, "mktime needs a tuple of length 8 or 9 (%d given)", len));
+    }
+
+    mp_int_t t = timeutils_mktime(  mp_obj_get_int(elem[0]),
+                                    mp_obj_get_int(elem[1]),
+                                    mp_obj_get_int(elem[2]),
+                                    mp_obj_get_int(elem[3]),
+                                    mp_obj_get_int(elem[4]), 
+                                    mp_obj_get_int(elem[5])    );
+    // reflect TimeZone
+    t -= 9 * 60 * 60;               // FIX to "JST"
+    t += 946684800;                 // 946684800 means seconds of 1st January 1970 00:00:00 to 1st January 2000 00:00:00
+
+    return mp_obj_new_int_from_uint(t);
+}
+MP_DEFINE_CONST_FUN_OBJ_1(time_mktime_obj, time_mktime);
+
 STATIC const mp_rom_map_elem_t time_module_globals_table[] = {
     { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_utime) },
 
     { MP_ROM_QSTR(MP_QSTR_time), MP_ROM_PTR(&time_time_obj) },
+    { MP_ROM_QSTR(MP_QSTR_set_time), MP_ROM_PTR(&time_set_time_obj) },
+    { MP_ROM_QSTR(MP_QSTR_localtime), MP_ROM_PTR(&time_localtime_obj) },
+    { MP_ROM_QSTR(MP_QSTR_mktime), MP_ROM_PTR(&time_mktime_obj) },
     { MP_ROM_QSTR(MP_QSTR_sleep), MP_ROM_PTR(&mp_utime_sleep_obj) },
     { MP_ROM_QSTR(MP_QSTR_sleep_ms), MP_ROM_PTR(&mp_utime_sleep_ms_obj) },
     { MP_ROM_QSTR(MP_QSTR_sleep_us), MP_ROM_PTR(&mp_utime_sleep_us_obj) },

 

ESP8266などではtime()で取得できる経過秒は2000年1月1日 00:00:00(UTC) からの経過秒となっていますが、ここでは他のシステム同様1970年1月1日00:00:00(UTC)からの経過秒にしてあります。
2000年1月1日 00:00:00(UTC) を基準にしたい場合は、946684800を加減している部分を削除すると変更できると思います(たぶん)。

また、localtime/mktimeのタイムゾーンは+9(JST)に固定してあります。
これも 9*60*60 を 加減している部分を変更すれば任意のタイムゾーンに変更できます(たぶん)。

 

以下のように使用します。

import utime

# 年月日時分秒とダミー2個のタプルをmktimeに入力するとその時刻の経過秒が取得できます。
# 2017年8月18日8時3分0秒の場合
# メソッド呼び出しのための括弧とタプル作成のための括弧で括弧が2重になっています。1重にするとエラーになりますので、注意してください
utime.mktime((2017, 8, 18, 8, 3, 0, 0, 0))
    ==> 1503010980

# 時刻の設定 mktimeで取得した値をset_timeで設定します。
utime.set_time(1503010980)

# この2つを組み合わせて、以下のようにすることも可能です。
utime.set_time(utime.mktime((2017, 8, 18, 8, 3, 0, 0, 0)))


# timeで現在時刻の経過秒が取得できます
utime.time()
    ==> 1503010982

# localtimeをパラメータなしで実行すると現在時刻を年月日時分秒形式のタプルで取得できます。
# 後ろから2個目は曜日で、0:月曜、1:火曜、・・・6:日曜 を表します。
# 最後はその年の1月1日からの経過日数です。
utime.localtime()
    ==> (2017, 8, 18, 8, 3, 2, 4, 230)



# localtimeをパラメータを指定して実行するとパラメータで指定した経過秒を年月日時分秒形式に変換できます。
utime.localtime(1503010980)
    ==> (2017, 8, 18, 8, 3, 0, 4, 230)

# パラメータなしのutime.localtime()は、utime.localtime(utime.time())と等価です。

 ここで設定した時刻はソフトリセットでは保持されますが、ハードリセットや電源OFFでは保持されませんので、再度時刻を設定してください。

ちなみに、 mktimeは1970年1月1日09:00:00 以前の値を与えると間違った値を返します。

 

micropython on ESP32 でモジュールをインストールする

micropythonにはPyPiサイトに用意されたモジュールをインストールするためのモジュール upip が用意されています。

import upip
upip.install("パッケージ名")

でインストールできます。
ちなみに、upipのソースはmicropython-esp32/esp32/modules/upip.pyにあり、micropython本体に組み込まれています。

しかし、サーバアドレスが固定されているため、自分で作ったモジュールをインストールするには使用できません。
また、PyPiサイトのモジュール管理形式を使用しているため、URLを自分のサーバに変えるだけではうまくいきません。

 

そこで、upipもどきなモジュール upipm (例によって「もどき」の「m」を追加)を作ってみました。
BaseURL で指定されたURLからget_file()の第1パラメータで指定したファイルを第2パラメータで指定したディレクトリに保存します。
ダウンロード元はHTTPサーバでなければなりません(Espruinoで作ったmnpmと同じです)。

import sys
import gc
import uos as os

gc.collect()

debug = False

# Base URL
BaseURL = "http://192.168.78.80/py_modules/"# 自分の環境に合わせて修正してくさだい

class NotFoundError(Exception):
    pass

import ussl
import usocket
warn_ussl = True
def url_open(url):
    global warn_ussl

    if debug:
        print(url)

    proto, _, host, urlpath = url.split('/', 3)
    if debug:
        print("proto:%s, host:%s, urlpath:%s" % (proto, host, urlpath))

    if proto == "https:":
        port = 443
    else:
        port = 80

    try:
        ai = usocket.getaddrinfo(host, port)
    except OSError as e:
        fatal("Unable to resolve %s (no Internet?)" % host, e)

    if debug:
        print("Address infos:", ai)
    addr = ai[0][4]

    s = usocket.socket(ai[0][0])
    try:
        if debug:
            print("Connect address:", addr)
        s.connect(addr)

        if proto == "https:":
            s = ussl.wrap_socket(s)
            if warn_ussl:
                print("Warning: %s SSL certificate is not validated" % host)
                warn_ussl = False

        # MicroPython rawsocket module supports file interface directly
        s.write("GET /%s HTTP/1.0\r\nHost: %s\r\n\r\n" % (urlpath, host))
        l = s.readline()
        if debug:
            print(l)
        protover, status, msg = l.split(None, 2)
        if status != b"200":
            if status == b"404" or status == b"301":
                raise NotFoundError("File not found")
            raise ValueError(status)
        while 1:
            l = s.readline()
            if debug:
                print(l)
            if not l:
                raise ValueError("Unexpected EOF in HTTP headers")
            if l == b'\r\n':
                break
    except Exception as e:
        s.close()
        raise e

    return s


def fatal(msg, exc=None):
    print("Error:", msg)
    if exc and debug:
        raise exc
    sys.exit(1)


def get_file(name, distdir=None):
    if distdir is None:
        distdir = sys.path[1]
    if distdir[-1] != "/":
        distdir += "/"

    f = url_open(BaseURL + name)
    print("==== START ====")
    with open(distdir + name, "wb") as outf:
        while True:
            l = f.readline()
            if not l:
                break
            outf.write(l, len(l))
            # print(len(l))
            print(l)
    print("==== DONE ====")

def help():
    print("""\
upipm - Simple PyPI package manager **MODOKI** for MicroPython
Usage: 
import upipm; upipm.get_file(name, [<path>])

If <path> is not given, packages will be installed into sys.path[1].
""")
    print("Current value of sys.path[1]:", sys.path[1])

if __name__ == "__main__":
    def main():
        get_file("upipm.py")

if __name__ == "__main__":
    main()

 

BaseURLは自分の環境に合わせて変更しておいてください。
また、オンボードストレージに/libディレクトリが存在しない場合は、以下のように実行してディレクトリを作成しておいてください。

import uos
uos.chdir("/")
uos.listdir()
     ==> ['boot.py']
uos.mkdir("/lib")
uos.listdir()
     ==> ['boot.py', 'lib']

 

このプログラムのファイルをupipm.pyという名前でBaseURLの場所に格納しておき、プログラム自体をコンソールからコピペして実行すると、upipm.pyがインストールされます。

以下のように実行すると正常にインストールできたか確認できます。

import uos
uos.listdir("/lib")
     ==> ['upipm.py']

 

インストール後は、以下のように実行すると任意のファイルをインストールできます。
モジュール単位でのインストールではなく、ファイル単位なので、ファイル拡張子(.py)も忘れずに指定してください。
デフォルトのインストール先はsys.path[1](ESP32の場合は/lib)です。

import upipm
upipm.get_file("ファイル名")

 

また、第2パラメータにはインストール先を指定することも可能です。
たとえば、こんな感じ。

upipm.get_file("boot.py", "/")

 

micropython on ESP32 でWi-Fiルータ(or AP)に接続する

ESP32をWi-Fiルータ または アクセスポイントに接続します。
以下のプログラムを実行すると、Wi-Fiルータに接続できます。

SSID_NAME = "SSID名"
SSID_PASS = "パスワード"

import utime
import network
# ==== connecti to wifi access point ============================================
def connect_wifi(ssid, passkey, timeout=10):
    wifi= network.WLAN(network.STA_IF)
    if wifi.isconnected() :
        print('already Connected.    connect skip')
        return wifi
    else :
        wifi.active(True)
        wifi.connect(ssid, passkey)
        while not wifi.isconnected() and timeout > 0:
            print('.')
            utime.sleep(1)
            timeout -= 1
    
    if wifi.isconnected():
        print('Connected')
        return wifi
    else:
        print('Connection failed!')
        return null

wifi = connect_wifi(SSID_NAME, SSID_PASS)
if not wifi :
    sys.exit(0)

 

Espruinoのときとは違い、micropythonでは次回起動時に自動的に再接続はしてくれません。
Wi-Fiに接続するには、このプログラムを起動の度に実行しなければなりません。
micropythonは起動時にオンボードストレージの /boot.py を自動的に実行してくれますので、このプログラムを /boot.py として保存しておけば起動のたびにWi-Fiに接続できます。
ただし、設定したSSIDが通信範囲内にない場合はタイムアウトするまで(10秒)起動が遅れます。

オンボードストレージには下記のプログラムで書き込めます。
プログラムが小さいので、プログラム全体を文字列として保存し、それを書き込んでいます。
プログラムの中に「'''」「\n」「\r」などがないことを確認し、あれば別の文字に置き換えてください。

script = '''
# <<ここにパラメータを設定した上のプログラムをコピペしてください>>
'''
f=open("boot.py","wt")
f.write(script)
f.close()

 

書き込んだ内容を確認するには以下のように実行すればOKです。

open("/boot.py","rt").read()

 

一時的にboot.pyを無効にしたい場合はリネームしておくと良いでしょう。
以下のように実行するとリネームできます。

import uos
uos.chdir("/")
uos.listdir()
     ==> ['boot.py']
uos.rename("boot.py", "boot.temp")
uos.listdir()
     ==> ['boot.temp']

 

元に戻す場合は、uos.renameのパラメータを入れ替えて元に戻してください。

 

/boot.pyでなく、/lib/MyWifi.pyとして保存しておき、必要なときに

import MyWifi

とすることもできます。
/lib に保存するときはあらかじめ/libディレクトリを作成しておいてください。

import uos
uos.chdir("/")
uos.listdir()
     ==> ['boot.py']
uos.mkdir("/lib")
uos.listdir()
     ==> ['boot.py', 'lib']

 

 

 

micropython on ESP32 でダウンローダを使う

プログラムを作成して、逐一コピペするのは面倒だなぁ、と思うでしょ?
micropythonにはダウンローダが用意されています。tools/pyboard.py がそれです。

/work2/esp/micropython-esp32/tools/pyboard.py --device /dev/ttyUSB0 file名

で実行できるはずなんですが、ちょっとうまく動きません(PATHが通ってないのでフルパスで指定)。
どうも、ESP32をリセットした後からシリアルを接続するとうまく接続できない現象が影響しているようです。
そこで、pyboard.pyが動作開始時にソフトリセットを行っている部分をハードリセットに変更する変更を行います。
ついでに、--device オプションのデフォルト値を /dev/ttyUSB0 に変更しておきます。
以下がそのパッチです。
あ、手動でリセットボタン押す修正をしたときの残骸がコメントアウトされて残ってる。。。(^^ゞ

diff --git a/tools/pyboard.py b/tools/pyboard.py
index d15f520..992b1ac 100755
--- a/tools/pyboard.py
+++ b/tools/pyboard.py
@@ -220,6 +220,12 @@ class ProcessPtyToTerminal:
     def inWaiting(self):
         return self.ser.inWaiting()
 
+    def setRTS(self, data):
+        return self.ser.setRTS(data)
+
+    def setDTR(self, data):
+        return self.ser.setDTR(data)
+
 
 class Pyboard:
     def __init__(self, device, baudrate=115200, user='micro', password='python', wait=0):
@@ -278,13 +284,47 @@ class Pyboard:
         return data
 
     def enter_raw_repl(self):
-        self.serial.write(b'\r\x03\x03') # ctrl-C twice: interrupt any running program
+        ## self.serial.write(b'\r\x03\x03') # ctrl-C twice: interrupt any running program
+        # print("Reset Target and Hit CR key!!")
+        # sys.stdin.readline()
+        # print("continue...")
+        # time.sleep(1)
+        # ====== haedware reset ! =======
+        """
+            == truth table ==
+            RTS DTR |RESET BOOT
+            --------+----------
+             0   0  | 1     1
+             0   1  | 0     1
+             1   0  | 1     0
+             1   1  | 1     1
+
+        """
+        print("Reseting Target ....")
+        self.serial.setDTR(False)  # DTR = HIGH  
+        self.serial.setRTS(True)   # RTS = LOW   IO0=HIGH, EN=LOW  reset assert
+        time.sleep(0.3)
+        self.serial.setRTS(False)  # RTS = HIGH  IO0=HIGH, EN=HIGH  reset deassert
+        time.sleep(0.8)
+        print("Done")
 
         # flush input (without relying on serial.flushInput())
         n = self.serial.inWaiting()
+        if n == 0 :
+            print("probably reset failure...")
+        """
         while n > 0:
-            self.serial.read(n)
+            a = self.serial.read(n)
+            print(a)
             n = self.serial.inWaiting()
+        print("^^^^^^^^ boot message end ^^^^^^^^")
+        """
+        # waiting prompt...
+        print("waiting prompt")
+        data = self.read_until(1, b'>>>')
+        if not data.endswith(b'>>>'):
+            print(data)
+            raise PyboardError('could not get prompt')
 
         self.serial.write(b'\r\x01') # ctrl-A: enter raw REPL
         data = self.read_until(1, b'raw REPL; CTRL-B to exit\r\n>')
@@ -295,7 +335,6 @@ class Pyboard:
         self.serial.write(b'\x04') # ctrl-D: soft reset
         data = self.read_until(1, b'soft reboot\r\n')
         if not data.endswith(b'soft reboot\r\n'):
-            print(data)
             raise PyboardError('could not enter raw repl')
         # By splitting this into 2 reads, it allows boot.py to print stuff,
         # which will show up after the soft reboot and before the raw REPL.
@@ -384,7 +423,7 @@ def execfile(filename, device='/dev/ttyACM0', baudrate=115200, user='micro', pas
 def main():
     import argparse
     cmd_parser = argparse.ArgumentParser(description='Run scripts on the pyboard.')
-    cmd_parser.add_argument('--device', default='/dev/ttyACM0', help='the serial device or the IP address of the pyboard')
+    cmd_parser.add_argument('--device', default='/dev/ttyUSB0', help='the serial device or the IP address of the pyboard')
     cmd_parser.add_argument('-b', '--baudrate', default=115200, help='the baud rate of the serial device')
     cmd_parser.add_argument('-u', '--user', default='micro', help='the telnet login username')
     cmd_parser.add_argument('-p', '--password', default='python', help='the telnet login password')

 

このパッチをあてて

/work2/esp/micropython-esp32/tools/pyboard.py file名

と実行するとfile名で指定したプログラムがダウンロードされ、実行されます。
プログラムの実行が完了し、プロンプトが返ってきたらダウンローダは終了します。
ただし、前回のプログラムのように、プロンプトが返ってきた後も割り込みが発生するようなプログラムではダウンローダが終了すると割り込み処理結果を得ることができません。
その場合は、--follow オプションを指定して実行してください。プロンプトが返ってきたあともダウンローダは終了せず、ESP32からのデータを表示し続けます。
終了するにはCTRL-Cを入力します。
--followオプション指定時、プロンプトが表示されてもキー入力はできませんので注意してください。

 

micropython on ESP32 でぽちっとな

またまた定番のボタンをぽちっと押してみます。

GPIO端子(下の例ではIO21)にpull-up抵抗とスイッチを接続し、Active Lowな入力にします。
以下のプログラムを実行すると、スイッチを押したり放したりすると「ぽちっとな~」とか「はなしたでー」とか表示されます。

。。。ん?そうです、micropythonは日本語が使えるんです。

ということで、シリアルコンソールの日本語モードは「UTF-8」に設定しておいてください。gtktermはUTF-8のみ対応のようなので、これを使う場合は特に設定不要です。

また、コンソール上で通常モード時は日本語入力ができないので、CTRL+Eでペーストモードにしてからコピペし、CTRL-Dで通常モードに戻してください。

 

import sys
platform = sys.platform
# platform :    ESP32 => 'esp32'
#               Linux => 'linux'

if platform == 'linux':
    raise ValueError("Linux not support GPIO")

import machine
import utime

# 前回の割り込み受付時刻
pin21_prev_time = utime.ticks_ms()

# PIN21の割り込みハンドラ
def int_handler_gpio21(p):
    global pin21_prev_time
    cur_time = utime.ticks_ms()
    if cur_time < pin21_prev_time + 100 :
        # 前回受付から100msec未満だったらチャタリングと判断してスキップ
        return

    # 前回の割り込み受付時刻を更新
    pin21_prev_time = cur_time

    # 現在のpinレベル
    cur_data = p.value()
    # 現在のpinレベルをスイッチ状態の文字列に変換(active LOWなのでTrueのとき"OFF")
    message = 'はなしたでー' if cur_data else 'ぽちっとな~'     # active LOW
    print(message)

# PIN21を入力モードに設定
pin21=machine.Pin(21, machine.Pin.IN)
# int_handler_gpio21 を PIN21の割り込みハンドラに設定(両エッジ)
pin21.irq(trigger=machine.Pin.IRQ_RISING | machine.Pin.IRQ_FALLING, handler=int_handler_gpio21)

 

 以下解説。

最初の方は前回と同じです。

時刻関連の処理を行うため、utimeモジュールをインポートします。今回は相対時間だけ欲しいので、時刻合わせは不要です。

import utime

# 前回の割り込み受付時刻
pin21_prev_time = utime.ticks_ms()

割り込み発生間隔を測るための時刻ですが、最初に1回読み込んでおきます。

 

端子の入力レベルが変化したときに実行されるcallback関数です。関数のパラメータpには対象の端子のインスタンスが入っています。これを使えば複数の端子に同じcallback関数を割り当てる、という使い方も可能です。

# PIN21の割り込みハンドラ
def int_handler_gpio21(p):
    ....

 

Espruinoではチャタリング除去時間を設定できましたが、micropythonではそのような機能がありません。そこで、現在の時刻とcallback関数が前回実行された時刻の差が100msec以下だったらチャタリングとみなして何も処理せずに終了します。100msecが妥当かどうかは検証する必要があるでしょう。

    global pin21_prev_time
    cur_time = utime.ticks_ms()
    if cur_time < pin21_prev_time + 100 :
        # 前回受付から100msec未満だったらチャタリングと判断してスキップ
        return

    # 前回の割り込み受付時刻を更新
    pin21_prev_time = cur_time

 

現在の端子レベルを読み込んで、Lowレベルだったら「ぽちっとな~」、Highレベルだったら「はなしたでー」を表示します。Active Lowのスイッチなので、こうなっています。Active Highのスイッチの場合は文字列を逆にしてください。(このコードだとチャタリングの発生タイミングによっては正常な値が読めるとは限らないんだよなぁーー。細かいことは目をつぶってください。)

    # 現在のpinレベル
    cur_data = p.value()
    # 現在のpinレベルをスイッチ状態の文字列に変換(active LOWなのでTrueのとき"OFF")
    message = 'はなしたでー' if cur_data else 'ぽちっとな~'     # active LOW
    print(message)

 

 messageを設定している部分はCで言う三項演算子です。ちょっとわかりにくいですね。
Cで書くとこんな感じかな。

message = cur_data ? "はなしたでー" : "ぽちっとな~"

 

 使用する端子を入力モードに設定します。

pin21=machine.Pin(21, machine.Pin.IN)

 

 端子の割り込み処理を登録します。
triggerでIRQ_RISING(立ち上がり)とIRQ_FALLING(立ち下り)を指定しているので両エッジでcallback関数が実行されます。

pin21.irq(trigger=machine.Pin.IRQ_RISING | machine.Pin.IRQ_FALLING, handler=int_handler_gpio21)

 

 

micropython on ESP32 でLチカ

ということで、定番のLチカ

GPIO端子(下の例ではIO22とIO23)にLEDと電流制限用抵抗を接続しておきます。
Active HighでもActive LowでもOK(点滅するので、どっちになってても分からない)。

 

以下のプログラムを実行すると、LEDがそれぞれの周期で点滅します。

import sys
platform = sys.platform
# platform :    ESP32 => 'esp32'
#               Linux => 'linux'

if platform == 'linux':
    raise ValueError("Linux not support GPIO")

import machine


# 現在のpinレベルを反転して出力
def pin_toggle(p) :
    cur_data = p.value()
    p.value(not cur_data)

# 端子を初期化(出力モード、Low出力)
pin22=machine.Pin(22, mode=machine.Pin.OUT, value=0)
pin23=machine.Pin(23, mode=machine.Pin.OUT, value=0)

# タイマ0用割り込みハンドラ
def int_handler_timer0(t):      # t はタイマのインスタンスが入っている
    pin_toggle(pin22)

# タイマ1用割り込みハンドラ
def int_handler_timer1(t):      # t はタイマのインスタンスが入っている
    pin_toggle(pin23)

# タイマ0の設定
t0 = machine.Timer(0)           # タイマは0~3が指定可能
t0.init(period=1300, mode=machine.Timer.PERIODIC, callback=int_handler_timer0)

# タイマ1の設定
t1 = machine.Timer(1)
t1.init(period=1000, mode=machine.Timer.PERIODIC, callback=int_handler_timer1)

 

 

プログラムを実行するには、ESP32を接続したシリアルコンソールで1文字ずつ入力するか、CTRL+Eでペーストモードにして(プロンプトが===に変わる)、ソースをコピペし、CTRL+Dで通常モードに戻すとプログラムが実行されます。

通常モードでコピペすると悲しいことになりますので注意してください。

 

 

 以下解説のようなものです。

 

現在の実行環境をチェックします。Linux版ではGPIOを使えないのでエラー終了しています。

import sys
platform = sys.platform
# platform :    ESP32 => 'esp32'
#               Linux => 'linux'

if platform == 'linux':
    raise ValueError("Linux not support GPIO")

 

GPIOやタイマを使用するため、machineモジュールをインポートします。

import machine

 

GPIO端子の出力を反転する関数です。Espruinoのようなtoggleメソッドは用意されていないようです。

valueメソッドはパラメータなしのとき、現在の端子の値を取得します。パラメータにFalseや0を指定するとLowが、Trueや1を指定するとHighが出力されます。

def pin_toggle(p) :
    cur_data = p.value()
    p.value(not cur_data)

 

端子の初期化です。IO22を出力モードで、Lowレベル出力に初期化しています。

以下、説明はIO22端子側だけですが、IO23端子も同様の処理を行っています。

pin22=machine.Pin(22, mode=machine.Pin.OUT, value=0)

 

周期的に実行するため、タイマのcallbackを使用します。そのcallback関数です。端子を反転する処理を行っています。
関数のパラメータtには対象のタイマのインスタンスが入っていますが、特に有用な情報が入っていないので参照していません。でも書いておかないと実行時にエラー(TypeError: function takes 0 positional arguments but 1 were given)になります。

def int_handler_timer0(t):      # t はタイマのインスタンスが入っている
    pin_toggle(pin22)

細かい話ですが、このcallback関数はハードウェアの割り込みハンドラで直接実行されるのではなく、ハードウェアの割り込みハンドラで仮想マシンの例外をアサートし、仮想マシンのコマンドループ内で例外として処理されるようです。

 

タイマの初期化です。
タイマは0~3が指定できます。それ以上を指定した場合は指定値を0x03でマスクした値が使用されます。つまり、4を指定すると0が、5を指定すると1が指定されたものとみなします。
modeにmachine.Timer.PERIODICを指定することでperiodicで指定した時間(msec)間隔でcallbackで指定した関数が実行されます。
modeにmachine.Timer.ONE_SHOTを指定すると、 periodicで指定した時間(msec)後に1回だけcallbackで指定した関数が実行されます。

t0 = machine.Timer(0)           # タイマは0~3が指定可能
t0.init(period=1300, mode=machine.Timer.PERIODIC, callback=int_handler_timer0)

タイマを停止するときは t0.deinit() と実行します。