RaspberryPi笔记[4]-使用esp01s模块获取NTP时间

摘要

在Raspberry Pi Pico(RP2040)上使用MicroPython和ESP01s模块获取NTP时间.

关键信息

  • MicroPython:MicroPython v1.22.1 on 2024-01-05; Raspberry Pi Pico with RP2040
  • Thonny:4.1.4

原理简介

ESP01s模块AT指令串口透传

[https://github.com/maysrp/wifiat_pico]
[https://www.cnblogs.com/milton/p/14718010.html]
[https://www.cnblogs.com/Sakura-Ji/p/17742668.html]
[https://zhuanlan.zhihu.com/p/343952644]
[http://www.arib.or.jp/english/html/overview/doc/STD-T63v9_30/5_Appendix/Rel10/27/27007-a70.pdf]
[https://docs.espressif.com/projects/esp-at/zh_CN/latest/esp32/AT_Command_Set/index.html]
[https://docs.espressif.com/projects/esp-at/en/release-v2.2.0.0_esp8266/AT_Binary_Lists/ESP8266_AT_binaries.html]
[https://www.espressif.com.cn/zh-hans/support/download/at]
EN 脚和 RST 脚必须上拉到 VCC
出厂默认固件是带有AT功能的
默认通信参数:115200 bps,ascii
ESP01s模块是一种基于ESP8266的WiFi模块,它可以通过串口进行数据传输,并能将串口数据转换为TCP/IP数据进行网络传输,这种功能被称为串口透传。
AT(Attention)指令是由 Dennis Hayes 发明的,所以也称为 Hayes command set。AT 指令最初是用来指导 modem 工作的。
随着技术的发展,目前 AT 指令发展过程中形成两个重要标准:

  • V.250:该标准于 1995 年建立,1998 年重命名为 V.250
  • ETSI GSM 07.07(3GPP TS 27.007):用于控制 GSM modem 的 AT 指令集
  • GSM 07.07 是基于 V.250 标准的。是目前最新的 AT 标准。

目前的 AT 指令着重应用在蜂窝模块、WiFi 模块、BLE 模块中,目的是为了简化嵌入式设备联网的复杂度。

AT 标准定义了 AT 命令的格式本身,比如命令以 AT 为前缀开头,以 或者 结尾,这被现有的 AT 模块所延用。

但是,由于每个厂家的模块不一样,实现的功能不一样,导致每个 AT 模块厂家有自己的一套私有的 AT 命令集,每一个 AT 模块厂家实现的 AT 指令集解析器也不一样(解析器实现的 AT 标准功能也参差不齐)。

既然是指令集,那么必然会有指令集的解析处理,通常,我们把 AT 模块端的解析处理程序称为 AT Server,而将控制 AT 模块的处理器端的解析处理程序称为 AT Client。由 AT Client 发起命令请求,AT Server 回应处理结果。另外 AT Server 通过 URC(Unsolicited result code) 来主动给 AT Client 发送数据。

  • 重新烧录esp01s AT固件的方式:
python3 -m pip install esptool
ls /dev/cu*
esptool.py --port /dev/cu.usbserial-1110 --baud 115200 write_flash --flash_size=detect 0 ESP8266_2M_Web_2.2.1.bin

某些AT固件还可以直接使用如下命令获取NTP时间:

AT+CIPSNTPCFG=1,8,"ntp1.aliyun.com"\r\n
AT+CIPSNTPTIME?\r\n

使用UDP方式获取NTP时间服务器时间

[https://tool.lu/timestamp/]
[https://zhuanlan.zhihu.com/p/474002660]
[http://www.beijing-time.org]
阿里云NTP服务器是http://ntp1.aliyun.com(IP为120.25.115.20)端口为123,因为ntp服务器是udp协议,ip:120.25.115.20端口号:123,格式是接收48个字节,第一个字节以0xa3(版本4)、0x1b(版本3)、0x13(版本2)、0x0b(版本1),返回的数据中带有时间。我们使用版本3获取阿里云NTP信息,阿里云服务器返回的数据我们取第40位到43位的十六进制,所以得到0xE2CFA73B十六进制,我们把该十六进制转成十进制变成3805259579秒再减去1900-1970的时间差(2208988800秒).

使用UDP测试从NTP获取时间

实现

安装MicroPython

[https://micropython.org/download/RPI_PICO/]
[https://micropython.org/resources/firmware/RPI_PICO-20240105-v1.22.1.uf2]

  1. 按住 Pico 开发板上的 BOOTSEL 按钮,然后将 Pico 插入树莓派或 PC 的 USB 接口,然后松开 BOOTSEL 按钮.
  2. Pico 会被识别为大容量存储设备.
  3. 将下载的 MicroPython UF2 文件放入 RPI-RP2 卷上。你的 Pico 将自动重启,然后 MicroPython 就开始运行了.
  4. 使用Thonny连接树莓派REPL终端.

核心代码

main.py放到MicroPython的虚拟磁盘中可实现上电自运行.
main.py

'''
备注:
- rp2040芯片
- micropython
硬件:
- dht11
- esp01s
- TikyAC1606(320x480)
- 4x4矩阵键盘
连接:
- dht11_dat_pin:io28
- esp01s_rx_pin:io4(uart1)
- esp01s_rst_pin:io3
- esp01s_en_pin:io2
- esp01s_tx_pin:io5
- keypad_col1_pin:io16
- keypad_col2_pin:io17
- keypad_col3_pin:io18
- keypad_col4_pin:io19
- keypad_row1_pin:io20
- keypad_row2_pin:io21
- keypad_row3_pin:io22
- keypad_row4_pin:io26
- tiky_cs_pin:io16
- tiky_wr_pin:io17
- tiky_rst_pin:io18
- tiky_rs_pin:io19
- tiky_rd_pin:io20
- tiky_mosi_pin:io27
- tiky_bl_pin:io2
- tiky_do0_pin:io21
- tiky_do1_pin:io22
- tiky_do2_pin:io26
- tiky_do3_pin:io4
- tiky_do4_pin:io28
- tiky_do5_pin:io15
- tiky_do6_pin:io14
- tiky_do7_pin:io13
- tiky_do8_pin:io12
- tiky_do9_pin:io11
- tiky_do10_pin:io10
- tiky_do11_pin:io9
- tiky_do12_pin:io8
- tiky_do13_pin:io7
- tiky_do14_pin:io6
- tiky_do15_pin:io3
库文件:
- dht11.py
'''

from machine import Pin, I2C,UART,RTC
import utime as time
import struct
from wifi_lib import Esp01sWifi

"""start 全局变量"""
dht11_dat_pin = 28
esp01s_rx_pin = 4
esp01s_rst_pin = 3
esp01s_en_pin = 2
esp01s_tx_pin = 5
keypad_col1_pin = 16
keypad_col2_pin = 17
keypad_col3_pin = 18
keypad_col4_pin = 19
keypad_row1_pin = 20
keypad_row2_pin = 21
keypad_row3_pin = 22
keypad_row4_pin = 26
tiky_cs_pin = 16
tiky_wr_pin = 17
tiky_rst_pin = 18
tiky_rs_pin = 19
tiky_rd_pin = 20
tiky_mosi_pin = 27
tiky_bl_pin = 2
tiky_do0_pin = 21
tiky_do1_pin = 22
tiky_do2_pin = 26
tiky_do3_pin = 4
tiky_do4_pin = 28
tiky_do5_pin = 15
tiky_do6_pin = 14
tiky_do7_pin = 13
tiky_do8_pin = 12
tiky_do9_pin = 11
tiky_do10_pin = 10
tiky_do11_pin = 9
tiky_do12_pin = 8
tiky_do13_pin = 7
tiky_do14_pin = 6
tiky_do15_pin = 3

g_keypad_pin_list = []
"""end 全局变量"""

"""start 自定义类"""
class NtpRtcSync(Esp01sWifi):
    NTP_PORT = 123
    NTP_PACKET_FORMAT = "!12I"
    NTP_TIMESTAMP_DELTA = 2208988800  # 1970-01-01 -> 1900-01-01
    NTP_RTC = RTC()
    def __init__(self, uart, ntp_server="ntp.aliyun.com"):
        super().__init__(uart)
        self.ntp_server = ntp_server
    
    def waitRespHex(self,timeout=2000):
        prvMills = time.ticks_ms()
        resp = b""
        while (time.ticks_ms()-prvMills)<timeout:
            if self.uart.any():
                resp = b"".join([resp, self.uart.read(1)])
        print("RESP:")
        print(f"CMD: {[hex(b) for b in resp]}")
        return resp

    def sendHexCMD_waitResp(self,cmd, timeout=2000):
        print(f"CMD: {[hex(b) for b in cmd]}")
        self.uart.write(cmd)
        return self.waitRespHex(timeout)
    
    def ntp_request(self):
        # 设置为单路链接模式
        self.sendCMD_waitResp('AT+CIPMUX=0\r\n')
        # 发送NTP请求
        self.sendCMD_waitResp('AT+CIPSTART="UDP","{}",{}\r\n'.format(self.ntp_server, self.NTP_PORT))
        # 发送48字节的NTP请求数据
        hex_string = "1b 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
        # 使用bytes.fromhex()将其转换为字节字符串
        ntp_packet = bytes.fromhex(hex_string.replace(" ", ""))
        # ntp_packet = b'\xe3\x00\x06\xec\x00\x00\x00\x00\x00\x00\x00\x00\x31\x4e\x31\x34\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
        self.sendCMD_waitResp('AT+CIPSEND=48\r\n')  # 发送数据长度
        response = self.sendHexCMD_waitResp(ntp_packet)# 发送NTP请求数据包(不要添加换行)
        # 等待响应
        # response = self.waitRespLine(5000)  # 超时设置为5秒
        self.sendCMD_waitResp('AT+CIPCLOSE\r\n')  # 关闭连接
        return response
    
    def parse_ntp_response(self, response):
        # 确保response是一个bytes类型
        if isinstance(response, bytes):
            # 尝试找到"+IPD,48:"这个序列
            ipd_index = response.find(b'+IPD,48:')
            if ipd_index != -1:
                # 从"+IPD,48:"后面开始提取48个字节的数据
                ntp_data = response[ipd_index + len(b'+IPD,48:'): ipd_index + len(b'+IPD,48:') + 48]
                # 解析NTP数据包
                try:
                    timestamp = struct.unpack(self.NTP_PACKET_FORMAT, ntp_data)[10]
                    # 转换为Unix时间戳
                    timestamp -= self.NTP_TIMESTAMP_DELTA
                    return timestamp
                except Exception as e:
                    print("Error parsing NTP response:", e)
            else:
                print("NTP response format is invalid")
        return None
    
    def init_rtc_from_ntp(self):
        # 初始化
        self.NTP_RTC.datetime((2024, 2, 5, 2, 10, 32, 36, 0))
        # 发送NTP请求并解析响应
        ntp_response = self.ntp_request()
        timestamp = None
        # 确保ntp_response是一个字符串
        if isinstance(ntp_response, bytes):
            try:
                # ntp_response = ntp_response[0]  # 尝试获取第一行响应
                timestamp = self.parse_ntp_response(ntp_response)
            except IndexError:
                print("NTP response list is empty or invalid")
        
        if timestamp is not None:
            # 打印时间秒数
            print(timestamp)
            # 修正时间
            timestamp += 8 * 60 * 60
            # 将Unix时间戳转换为时间元组
            time_tuple = time.localtime(timestamp)
            print(time_tuple)
            #time_tuple = time.gmtime(timestamp)
            # 初始化RTC  
            # 修正时间元组,移除星期和微秒,并将小时从本地时间转换为 RTC 使用的 UTC 时间
            s_formatted_time = (time_tuple[0], time_tuple[1], time_tuple[2], 0,time_tuple[3],time_tuple[4], time_tuple[5], 0)
            self.NTP_RTC.datetime(s_formatted_time)
            print("RTC set to:", self.NTP_RTC.datetime())
        else:
            print("NTP request failed")
"""end 自定义类"""

"""start 自定义函数"""

"""end 自定义函数"""

"""start 初始化"""
# 初始化ESP01s
g_esp01s_en_pin = Pin(esp01s_en_pin,Pin.OUT,Pin.PULL_UP)
g_esp01s_en_pin.value(1) # 高电平有效
g_esp01s_rst_pin = Pin(esp01s_rst_pin,Pin.OUT,Pin.PULL_UP)
g_esp01s_rst_pin.value(1) # 高电平有效
g_esp01s_uart = UART(1,baudrate = 115200,bits = 8,parity=None,stop=1,tx=Pin(esp01s_rx_pin),rx=Pin(esp01s_tx_pin))
g_esp01s = Esp01sWifi(g_esp01s_uart)
g_esp01s.connect("xiaomi.home","xiaomiwifi")
time.sleep(3)# 等待获取ip
g_esp01s.netinfo()

# 初始化NTP时间
g_esp01s_rtc = NtpRtcSync(uart=g_esp01s_uart)  # 使用默认的NTP服务器或指定一个
g_esp01s_rtc.init_rtc_from_ntp()
print(g_esp01s_rtc.NTP_RTC.datetime())

"""end 初始化"""


"""start 主循环"""
while True:
    print("start main loop")
    time.sleep(1) # 1s

    # 打印时间
    # 获取当前日期和时间
    s_rtc_datetime = g_esp01s_rtc.NTP_RTC.datetime()
    # 格式化日期和时间
    s_formatted_datetime = "{:04d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}".format(s_rtc_datetime[0], s_rtc_datetime[1], s_rtc_datetime[2], s_rtc_datetime[4], s_rtc_datetime[5], s_rtc_datetime[6])
    # 打印格式化的日期和时间
    print(s_formatted_datetime)
    
"""end 主循环"""

wifi_lib.py

import utime

class Esp01sWifi(object):
    def __init__(self,uart):
        self.uart=uart
    def sendCMD_waitResp(self,cmd, timeout=2000):
        print("CMD: " + cmd)
        self.uart.write(cmd.encode('utf-8'))
        return self.waitResp(timeout)
    def waitResp(self,timeout=2000):
        prvMills = utime.ticks_ms()
        resp = b""
        while (utime.ticks_ms()-prvMills)<timeout:
            if self.uart.any():
                resp = b"".join([resp, self.uart.read(1)])
        print("RESP:")
        print(resp)
        return resp
    def sendCMD_waitRespLine(self,cmd, timeout=2000):
        print("CMD: " + cmd)
        self.uart.write(cmd.encode('utf-8'))
        return self.waitRespLine(timeout)
    def waitRespLine(self,timeout=2000):
        cl=[]
        prvMills = utime.ticks_ms()
        while (utime.ticks_ms()-prvMills)<timeout:
            if self.uart.any():
                li=self.uart.readline()
                print(li)
                cl.append(li)
        return cl
    def restart(self):
        self.sendCMD_waitResp('+++')
        return self.sendCMD_waitResp("AT+RST\r\n")
    def connect(self,name,password):
        self.sendCMD_waitResp('AT+CWMODE=1\r\n')
        cmd='AT+CWJAP="%s","%s"\r\n' % (name,password)
        return self.sendCMD_waitResp(cmd)
    def netinfo(self):
        return self.sendCMD_waitResp("AT+CIFSR\r\n")
    def info(self):
        return self.sendCMD_waitResp("AT+GMR\r\n")
    def restore(self):
        return self.sendCMD_waitResp("AT+RESTORE\r\n")
    def ping(self,doip):
        return self.sendCMD_waitResp('AT+PING="%s"\r\n' % (doip,))
    def disconnect(self):
        return self.sendCMD_waitResp("AT+CWQAP\r\n")
    def http(self,url,method="GET",ua=1,content=1,data='',timeout=6):
        self.sendCMD_waitResp("+++")
        typ,edx,domain,rel=url.split("/",3)
        if "HTTP" == typ[0:-1].upper():
            cs='AT+CIPSTART="TCP","%s",80\r\n' % (domain,)
        else:
            self.sendCMD_waitResp("AT+CIPSSLSIZE=4096\r\n")
            cs='AT+CIPSTART="SSL","%s",443\r\n' % (domain,)
        self.sendCMD_waitResp(cs)
        self.sendCMD_waitResp('AT+CIPMODE=1\r\n')
        self.sendCMD_waitResp('AT+CIPSEND\r\n')
        if ua==1:
            ua="Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.163 Safari/535.1"
        elif ua==2:
            ua="Mozilla/5.0 (Windows NT 6.1; WOW64; rv:6.0) Gecko/20100101 Firefox/6.0"
        elif ua==3:
            ua="Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36"
        elif ua==4:
            ua="Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1"
        if content==1:
            content="text/html;charset=utf-8"
        elif content==2:
            content="text/xml"
        elif content==3:
            content="application/json"
        elif content==4:
            content="multipart/form-data"
        elif content==5:
            content="application/x-www-form-urlencoded"
        c="%s %s HTTP/1.1\r\nHOST:%s \r\nContent-Type: %s\r\nUser-Agent:%s \r\n  \n %s \r\n\r\n " % (method,url,domain,content,ua,data)
        return self.sendCMD_waitRespLine(c,1000*timeout)

效果

串口打印时间信息
posted @ 2024-02-06 00:50  qsBye  阅读(190)  评论(0编辑  收藏  举报