ID.LODA 发表于 2023-6-30 23:59

【得捷电子Follow me第1期】+ 提交贴

## 项目描述

使用 Raspberry Pico W 控制板板载的无线模块连接 WIFI 进行时间同步,通过串口连接 Grove - GPS (Air530) 模块获取定位信息,并转化为具体的城市位置,然后再通过和风天气提供的天气接口 https://dev.qweather.com/docs/api/weather/weather-now/,获取城市当前的天气信息,并通过 I2C 接口连接的 OLED 进行显示。设备在运行过程中每一分钟进行一次位置更新,每一个小时进行一次时间同步和天气更新。

## 任务讲解
### 熟悉micropython的基本语法

1. 烧录固件
        点击 https://micropython.org/download/rp2-pico-w/rp2-pico-w-latest.uf2 链接下载UF2文件; 也可前往树莓派官网下载。
        按住板卡上的BOOTSEL按键,将树莓派Pico W通过Micro USB线接到电脑,然后松开按键。
        接入之后,电脑会自动识别到一个可移动盘(RPI-RP2)。
        将前面下载的固件文件,复制拖拽到RPi-RP2移动盘上。
        复制完成之后,Pico会自动重启, 自动重启之后,pico会被识别为一个虚拟串口
2. 安装Mu Editor
        Mu Editor的下载地址:https://codewith.mu/en/
        直接双击安装即可。模式选项中配置为 RP2040 芯片进行连接
       
3. 熟悉micropython语法
        MicroPython 是编程语言 Python3 的精简高效实现,语法和 Python3 保持一致,但只实现了 Python 标准库的一小部分,并且经过优化,可以在 MCU,WIFI SOC 上等资源受限的环境中使用。
简单控制一下板卡上的小灯,实验一下。
        ```python
        from machine import Pin, Timer

        led = Pin("LED", Pin.OUT)
        timer = Timer()

        def blink(timer):
                led.toggle()

        timer.init(freq=2.5, mode=Timer.PERIODIC, callback=blink)
        ```

### 驱动外设

1. 驱动OLED显示屏

        将 Grove-OLED-Display-0.96-SSD1315 模块连接至 IIC0 接口,对应的 PICO 引脚 SCL PIN9,SDA PIN8
        导入 ssd1306 的显示驱动库
        ```python
        # MicroPython SSD1306 OLED driver, I2C and SPI interfaces
        # https://github.com/micropython/micropython/blob/master/drivers/display/ssd1306.py
        from micropython import const
        import framebuf

        # register definitions
        SET_CONTRAST = const(0x81)
        SET_ENTIRE_ON = const(0xA4)
        SET_NORM_INV = const(0xA6)
        SET_DISP = const(0xAE)
        SET_MEM_ADDR = const(0x20)
        SET_COL_ADDR = const(0x21)
        SET_PAGE_ADDR = const(0x22)
        SET_DISP_START_LINE = const(0x40)
        SET_SEG_REMAP = const(0xA0)
        SET_MUX_RATIO = const(0xA8)
        SET_IREF_SELECT = const(0xAD)
        SET_COM_OUT_DIR = const(0xC0)
        SET_DISP_OFFSET = const(0xD3)
        SET_COM_PIN_CFG = const(0xDA)
        SET_DISP_CLK_DIV = const(0xD5)
        SET_PRECHARGE = const(0xD9)
        SET_VCOM_DESEL = const(0xDB)
        SET_CHARGE_PUMP = const(0x8D)

        # Subclassing FrameBuffer provides support for graphics primitives
        # http://docs.micropython.org/en/latest/pyboard/library/framebuf.html
        class SSD1306(framebuf.FrameBuffer):
                def __init__(self, width, height, external_vcc):
                        self.width = width
                        self.height = height
                        self.external_vcc = external_vcc
                        self.pages = self.height // 8
                        self.buffer = bytearray(self.pages * self.width)
                        super().__init__(self.buffer, self.width, self.height, framebuf.MONO_VLSB)
                        self.init_display()

                def init_display(self):
                        for cmd in (
                                        SET_DISP,# display off
                                        # address setting
                                        SET_MEM_ADDR,
                                        0x00,# horizontal
                                        # resolution and layout
                                        SET_DISP_START_LINE,# start at line 0
                                        SET_SEG_REMAP | 0x01,# column addr 127 mapped to SEG0
                                        SET_MUX_RATIO,
                                        self.height - 1,
                                        SET_COM_OUT_DIR | 0x08,# scan from COM to COM0
                                        SET_DISP_OFFSET,
                                        0x00,
                                        SET_COM_PIN_CFG,
                                        0x02 if self.width > 2 * self.height else 0x12,
                                        # timing and driving scheme
                                        SET_DISP_CLK_DIV,
                                        0x80,
                                        SET_PRECHARGE,
                                        0x22 if self.external_vcc else 0xF1,
                                        SET_VCOM_DESEL,
                                        0x30,# 0.83*Vcc
                                        # display
                                        SET_CONTRAST,
                                        0xFF,# maximum
                                        SET_ENTIRE_ON,# output follows RAM contents
                                        SET_NORM_INV,# not inverted
                                        SET_IREF_SELECT,
                                        0x30,# enable internal IREF during display on
                                        # charge pump
                                        SET_CHARGE_PUMP,
                                        0x10 if self.external_vcc else 0x14,
                                        SET_DISP | 0x01,# display on
                        ):# on
                                self.write_cmd(cmd)
                        self.fill(0)
                        self.show()

                def poweroff(self):
                        self.write_cmd(SET_DISP)

                def poweron(self):
                        self.write_cmd(SET_DISP | 0x01)

                def contrast(self, contrast):
                        self.write_cmd(SET_CONTRAST)
                        self.write_cmd(contrast)

                def invert(self, invert):
                        self.write_cmd(SET_NORM_INV | (invert & 1))

                def rotate(self, rotate):
                        self.write_cmd(SET_COM_OUT_DIR | ((rotate & 1) << 3))
                        self.write_cmd(SET_SEG_REMAP | (rotate & 1))

                def show(self):
                        x0 = 0
                        x1 = self.width - 1
                        if self.width != 128:
                                # narrow displays use centred columns
                                col_offset = (128 - self.width) // 2
                                x0 += col_offset
                                x1 += col_offset
                        self.write_cmd(SET_COL_ADDR)
                        self.write_cmd(x0)
                        self.write_cmd(x1)
                        self.write_cmd(SET_PAGE_ADDR)
                        self.write_cmd(0)
                        self.write_cmd(self.pages - 1)
                        self.write_data(self.buffer)

                def clear(self):
                        self.fill(0)

        class SSD1306_I2C(SSD1306):
                def __init__(self, width, height, i2c, addr=0x3C, external_vcc=False):
                        self.i2c = i2c
                        self.addr = addr
                        self.temp = bytearray(2)
                        self.write_list = # Co=0, D/C#=1
                        super().__init__(width, height, external_vcc)

                def write_cmd(self, cmd):
                        self.temp = 0x80# Co=1, D/C#=0
                        self.temp = cmd
                        self.i2c.writeto(self.addr, self.temp)

                def write_data(self, buf):
                        self.write_list = buf
                        self.i2c.writevto(self.addr, self.write_list)

        class SSD1306_SPI(SSD1306):
                def __init__(self, width, height, spi, dc, res, cs, external_vcc=False):
                        self.rate = 10 * 1024 * 1024
                        dc.init(dc.OUT, value=0)
                        res.init(res.OUT, value=0)
                        cs.init(cs.OUT, value=1)
                        self.spi = spi
                        self.dc = dc
                        self.res = res
                        self.cs = cs
                        import time

                        self.res(1)
                        time.sleep_ms(1)
                        self.res(0)
                        time.sleep_ms(10)
                        self.res(1)
                        super().__init__(width, height, external_vcc)

                def write_cmd(self, cmd):
                        self.spi.init(baudrate=self.rate, polarity=0, phase=0)
                        self.cs(1)
                        self.dc(0)
                        self.cs(0)
                        self.spi.write(bytearray())
                        self.cs(1)

                def write_data(self, buf):
                        self.spi.init(baudrate=self.rate, polarity=0, phase=0)
                        self.cs(1)
                        self.dc(1)
                        self.cs(0)
                        self.spi.write(buf)
                        self.cs(1)
        ```
        添加应用代码, 这边我为了能显示中文,又导入了 **ufont** 库及其字库文件 **unifont-14-12917-16.v3.bmf**
        ```python
        from machine import Pin, I2C, RTC, Timer, UART
        import time
        from ssd1306 import SSD1306_I2C
        import ufont
        import framebuf

        led = Pin('LED', Pin.OUT)
        timer = Timer()
        i2c_oled = I2C(0, scl=Pin(9), sda=Pin(8), freq=400000)
        display = SSD1306_I2C(128, 64, i2c_oled)
        display.clear()
        font = ufont.BMFont("unifont-14-12917-16.v3.bmf")

        buffer = bytearray(b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00|?\x00\x01\x86@\x80\x01\x01\x80\x80\x01\x11\x88\x80\x01\x05\xa0\x80\x00\x83\xc1\x00\x00C\xe3\x00\x00~\xfc\x00\x00L'\x00\x00\x9c\x11\x00\x00\xbf\xfd\x00\x00\xe1\x87\x00\x01\xc1\x83\x80\x02A\x82@\x02A\x82@\x02\xc1\xc2@\x02\xf6>\xc0\x01\xfc=\x80\x01\x18\x18\x80\x01\x88\x10\x80\x00\x8c!\x00\x00\x87\xf1\x00\x00\x7f\xf6\x00\x008\x1c\x00\x00\x0c \x00\x00\x03\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00")
        fb = framebuf.FrameBuffer(buffer, 32, 32, framebuf.MONO_HLSB)
        display.blit(fb, 96, 0)
        font.text(display, "Thanks", 0, 0, show=False)
        #display.text("eeworld!", 8, 16)
        #display.text("Digi-Key!", 16, 32)
        font.text(display, "eeworld!", 16, 16, show=False)
        font.text(display, "Digi-Key!", 16, 32, show=False)
        font.text(display, "Follow me 活动", 0, 48, show=False)
        display.show()
        ```
        显示效果
       

2. 驱动蜂鸣器

        使用的 Grove - Buzzer 模块为无源蜂鸣器,可以选择拉高电平或者输出 pwm 信号进行控制,这里使用了PICO W的 26 脚。
        ```python
        from machine import I2C, Pin, PWM
        import utime

        tones = {"1": 262, "2": 294, "3": 330, "4": 349, "5": 392, "6": 440, "7": 494, "-": 0}
        melody = "1155665-4433221-5544332-5544332-1155665-4433221"
        beeper = PWM(Pin(16))
        beeper.duty_u16(1000)
        for tone in melody:
                freq = tones
                if freq:
                        beeper.freq(freq)
                        beeper.duty_u16(1000)
                        #beeper = PWM(Pin(16), freq=freq, duty_u16=1000)
                else:
                        beeper.duty_u16(0)
                        utime.sleep_ms(400)
                        beeper.duty_u16(0)
                        utime.sleep_ms(100)
        beeper.deinit()
        ```
        运行之后可以听到有声音的变化

### 同步网络时间
通过板载的 WIFI 模块和自带的 NTP 库进行网络同步系统时间
        ```python
        from machine import Pin, Timer, RTC
        import time
        import network
        import ntptime

        WIFI_SSID = "your-ssid"
        WIFI_PASSWORD = "your-password"
        UTC_OFFSET = 8 * 60 * 60
        led = Pin("LED", Pin.OUT)
        rtc = RTC()

        # Connect to WiFi
        wlan = network.WLAN(network.STA_IF)
        wlan.active(True)
        access_points = wlan.scan()
        for AP in access_points:
        print(AP)
        wlan.connect(WIFI_SSID, WIFI_PASSWORD)
        while not wlan.isconnected() and wlan.status() >= 0:
        print("Waiting to connect:")
        led.toggle()
        time.sleep(0.25)
        print("Connected to WiFi!")
        print(wlan.ifconfig())
        # Get the current network time and set the system clock
        print("Local time before synchronization:%s" % str(time.localtime()))
        ntptime.timeout = 3
        ntptime.settime()
        utctime = ntptime.time() + UTC_OFFSET
        print("Local time after synchronization:%s" % str(time.localtime(utctime)))
        ```
运行之后的 log 信息如下:
        ```python
        Connected to WiFi!
        ('192.168.0.3', '255.255.255.0', '192.168.0.1', '211.136.150.66')
        Local time before synchronization:(2023, 6, 7, 14, 19, 21, 2, 158)
        Local time after synchronization:(2023, 6, 7, 22, 19, 21, 2, 158)
        ```
### 实现定位功能

使用 UART0 接口连接 Grove - GPS (Air530)模块,通讯采用的是标准的 NMEA-0183 协议,这边我导入了开源的 micropyGPS 进行解析
```python
"""
# MicropyGPS - a GPS NMEA sentence parser for Micropython/Python 3.X
# Copyright (c) 2017 Michael Calvin McCoy (calvin.mccoy@protonmail.com)
# The MIT License (MIT) - see LICENSE file
"""

# TODO:
# Time Since First Fix
# Distance/Time to Target
# More Helper Functions
# Dynamically limit sentences types to parse

from math import floor, modf

# Import utime or time for fix time handling
try:
    # Assume running on MicroPython
    import utime
except ImportError:
    # Otherwise default to time module for non-embedded implementations
    # Should still support millisecond resolution.
    import time


class MicropyGPS(object):
    """GPS NMEA Sentence Parser. Creates object that stores all relevant GPS data and statistics.
    Parses sentences one character at a time using update(). """

    # Max Number of Characters a valid sentence can be (based on GGA sentence)
    SENTENCE_LIMIT = 90
    __HEMISPHERES = ('N', 'S', 'E', 'W')
    __NO_FIX = 1
    __FIX_2D = 2
    __FIX_3D = 3
    __DIRECTIONS = ('N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W',
                  'WNW', 'NW', 'NNW')
    __MONTHS = ('January', 'February', 'March', 'April', 'May',
                'June', 'July', 'August', 'September', 'October',
                'November', 'December')

    def __init__(self, local_offset=0, location_formatting='ddm'):
      """
      Setup GPS Object Status Flags, Internal Data Registers, etc
            local_offset (int): Timzone Difference to UTC
            location_formatting (str): Style For Presenting Longitude/Latitude:
                                       Decimal Degree Minute (ddm) - 40° 26.767′ N
                                       Degrees Minutes Seconds (dms) - 40° 26′ 46″ N
                                       Decimal Degrees (dd) - 40.446° N
      """

      #####################
      # Object Status Flags
      self.sentence_active = False
      self.active_segment = 0
      self.process_crc = False
      self.gps_segments = []
      self.crc_xor = 0
      self.char_count = 0
      self.fix_time = 0

      #####################
      # Sentence Statistics
      self.crc_fails = 0
      self.clean_sentences = 0
      self.parsed_sentences = 0

      #####################
      # Logging Related
      self.log_handle = None
      self.log_en = False

      #####################
      # Data From Sentences
      # Time
      self.timestamp =
      self.date =
      self.local_offset = local_offset

      # Position/Motion
      self._latitude =
      self._longitude =
      self.coord_format = location_formatting
      self.speed =
      self.course = 0.0
      self.altitude = 0.0
      self.geoid_height = 0.0

      # GPS Info
      self.satellites_in_view = 0
      self.satellites_in_use = 0
      self.satellites_used = []
      self.last_sv_sentence = 0
      self.total_sv_sentences = 0
      self.satellite_data = dict()
      self.hdop = 0.0
      self.pdop = 0.0
      self.vdop = 0.0
      self.valid = False
      self.fix_stat = 0
      self.fix_type = 1

    ########################################
    # Coordinates Translation Functions
    ########################################
    @property
    def latitude(self):
      """Format Latitude Data Correctly"""
      if self.coord_format == 'dd':
            decimal_degrees = self._latitude + (self._latitude / 60)
            return ]
      elif self.coord_format == 'dms':
            minute_parts = modf(self._latitude)
            seconds = round(minute_parts * 60)
            return , int(minute_parts), seconds, self._latitude]
      else:
            return self._latitude

    @property
    def longitude(self):
      """Format Longitude Data Correctly"""
      if self.coord_format == 'dd':
            decimal_degrees = self._longitude + (self._longitude / 60)
            return ]
      elif self.coord_format == 'dms':
            minute_parts = modf(self._longitude)
            seconds = round(minute_parts * 60)
            return , int(minute_parts), seconds, self._longitude]
      else:
            return self._longitude

    ########################################
    # Logging Related Functions
    ########################################
    def start_logging(self, target_file, mode="append"):
      """
      Create GPS data log object
      """
      # Set Write Mode Overwrite or Append
      mode_code = 'w' if mode == 'new' else 'a'

      try:
            self.log_handle = open(target_file, mode_code)
      except AttributeError:
            print("Invalid FileName")
            return False

      self.log_en = True
      return True

    def stop_logging(self):
      """
      Closes the log file handler and disables further logging
      """
      try:
            self.log_handle.close()
      except AttributeError:
            print("Invalid Handle")
            return False

      self.log_en = False
      return True

    def write_log(self, log_string):
      """Attempts to write the last valid NMEA sentence character to the active file handler
      """
      try:
            self.log_handle.write(log_string)
      except TypeError:
            return False
      return True

    ########################################
    # Sentence Parsers
    ########################################
    def gprmc(self):
      """Parse Recommended Minimum Specific GPS/Transit data (RMC)Sentence.
      Updates UTC timestamp, latitude, longitude, Course, Speed, Date, and fix status
      """

      # UTC Timestamp
      try:
            utc_string = self.gps_segments

            if utc_string:# Possible timestamp found
                hours = (int(utc_string) + self.local_offset) % 24
                minutes = int(utc_string)
                seconds = float(utc_string)
                self.timestamp =
            else:# No Time stamp yet
                self.timestamp =

      except ValueError:# Bad Timestamp value present
            return False

      # Date stamp
      try:
            date_string = self.gps_segments

            # Date string printer function assumes to be year >=2000,
            # date_string() must be supplied with the correct century argument to display correctly
            if date_string:# Possible date stamp found
                day = int(date_string)
                month = int(date_string)
                year = int(date_string)
                self.date = (day, month, year)
            else:# No Date stamp yet
                self.date = (0, 0, 0)

      except ValueError:# Bad Date stamp value present
            return False

      # Check Receiver Data Valid Flag
      if self.gps_segments == 'A':# Data from Receiver is Valid/Has Fix

            # Longitude / Latitude
            try:
                # Latitude
                l_string = self.gps_segments
                lat_degs = int(l_string)
                lat_mins = float(l_string)
                lat_hemi = self.gps_segments

                # Longitude
                l_string = self.gps_segments
                lon_degs = int(l_string)
                lon_mins = float(l_string)
                lon_hemi = self.gps_segments
            except ValueError:
                return False

            if lat_hemi not in self.__HEMISPHERES:
                return False

            if lon_hemi not in self.__HEMISPHERES:
                return False

            # Speed
            try:
                spd_knt = float(self.gps_segments)
            except ValueError:
                return False

            # Course
            try:
                if self.gps_segments:
                  course = float(self.gps_segments)
                else:
                  course = 0.0
            except ValueError:
                return False

            # TODO - Add Magnetic Variation

            # Update Object Data
            self._latitude =
            self._longitude =
            # Include mph and hm/h
            self.speed =
            self.course = course
            self.valid = True

            # Update Last Fix Time
            self.new_fix_time()

      else:# Clear Position Data if Sentence is 'Invalid'
            self._latitude =
            self._longitude =
            self.speed =
            self.course = 0.0
            self.valid = False

      return True

    def gpgll(self):
      """Parse Geographic Latitude and Longitude (GLL)Sentence. Updates UTC timestamp, latitude,
      longitude, and fix status"""

      # UTC Timestamp
      try:
            utc_string = self.gps_segments

            if utc_string:# Possible timestamp found
                hours = (int(utc_string) + self.local_offset) % 24
                minutes = int(utc_string)
                seconds = float(utc_string)
                self.timestamp =
            else:# No Time stamp yet
                self.timestamp =

      except ValueError:# Bad Timestamp value present
            return False

      # Check Receiver Data Valid Flag
      if self.gps_segments == 'A':# Data from Receiver is Valid/Has Fix

            # Longitude / Latitude
            try:
                # Latitude
                l_string = self.gps_segments
                lat_degs = int(l_string)
                lat_mins = float(l_string)
                lat_hemi = self.gps_segments

                # Longitude
                l_string = self.gps_segments
                lon_degs = int(l_string)
                lon_mins = float(l_string)
                lon_hemi = self.gps_segments
            except ValueError:
                return False

            if lat_hemi not in self.__HEMISPHERES:
                return False

            if lon_hemi not in self.__HEMISPHERES:
                return False

            # Update Object Data
            self._latitude =
            self._longitude =
            self.valid = True

            # Update Last Fix Time
            self.new_fix_time()

      else:# Clear Position Data if Sentence is 'Invalid'
            self._latitude =
            self._longitude =
            self.valid = False

      return True

    def gpvtg(self):
      """Parse Track Made Good and Ground Speed (VTG) Sentence. Updates speed and course"""
      try:
            course = float(self.gps_segments) if self.gps_segments else 0.0
            spd_knt = float(self.gps_segments) if self.gps_segments else 0.0
      except ValueError:
            return False

      # Include mph and km/h
      self.speed = (spd_knt, spd_knt * 1.151, spd_knt * 1.852)
      self.course = course
      return True

    def gpgga(self):
      """Parse Global Positioning System Fix Data (GGA) Sentence. Updates UTC timestamp, latitude, longitude,
      fix status, satellites in use, Horizontal Dilution of Precision (HDOP), altitude, geoid height and fix status"""

      try:
            # UTC Timestamp
            utc_string = self.gps_segments

            # Skip timestamp if receiver doesn't have on yet
            if utc_string:
                hours = (int(utc_string) + self.local_offset) % 24
                minutes = int(utc_string)
                seconds = float(utc_string)
            else:
                hours = 0
                minutes = 0
                seconds = 0.0

            # Number of Satellites in Use
            satellites_in_use = int(self.gps_segments)

            # Get Fix Status
            fix_stat = int(self.gps_segments)

      except (ValueError, IndexError):
            return False

      try:
            # Horizontal Dilution of Precision
            hdop = float(self.gps_segments)
      except (ValueError, IndexError):
            hdop = 0.0

      # Process Location and Speed Data if Fix is GOOD
      if fix_stat:

            # Longitude / Latitude
            try:
                # Latitude
                l_string = self.gps_segments
                lat_degs = int(l_string)
                lat_mins = float(l_string)
                lat_hemi = self.gps_segments

                # Longitude
                l_string = self.gps_segments
                lon_degs = int(l_string)
                lon_mins = float(l_string)
                lon_hemi = self.gps_segments
            except ValueError:
                return False

            if lat_hemi not in self.__HEMISPHERES:
                return False

            if lon_hemi not in self.__HEMISPHERES:
                return False

            # Altitude / Height Above Geoid
            try:
                altitude = float(self.gps_segments)
                geoid_height = float(self.gps_segments)
            except ValueError:
                altitude = 0
                geoid_height = 0

            # Update Object Data
            self._latitude =
            self._longitude =
            self.altitude = altitude
            self.geoid_height = geoid_height

      # Update Object Data
      self.timestamp =
      self.satellites_in_use = satellites_in_use
      self.hdop = hdop
      self.fix_stat = fix_stat

      # If Fix is GOOD, update fix timestamp
      if fix_stat:
            self.new_fix_time()

      return True

    def gpgsa(self):
      """Parse GNSS DOP and Active Satellites (GSA) sentence. Updates GPS fix type, list of satellites used in
      fix calculation, Position Dilution of Precision (PDOP), Horizontal Dilution of Precision (HDOP), Vertical
      Dilution of Precision, and fix status"""

      # Fix Type (None,2D or 3D)
      try:
            fix_type = int(self.gps_segments)
      except ValueError:
            return False

      # Read All (up to 12) Available PRN Satellite Numbers
      sats_used = []
      for sats in range(12):
            sat_number_str = self.gps_segments
            if sat_number_str:
                try:
                  sat_number = int(sat_number_str)
                  sats_used.append(sat_number)
                except ValueError:
                  return False
            else:
                break

      # PDOP,HDOP,VDOP
      try:
            pdop = float(self.gps_segments)
            hdop = float(self.gps_segments)
            vdop = float(self.gps_segments)
      except ValueError:
            return False

      # Update Object Data
      self.fix_type = fix_type

      # If Fix is GOOD, update fix timestamp
      if fix_type > self.__NO_FIX:
            self.new_fix_time()

      self.satellites_used = sats_used
      self.hdop = hdop
      self.vdop = vdop
      self.pdop = pdop

      return True

    def gpgsv(self):
      """Parse Satellites in View (GSV) sentence. Updates number of SV Sentences,the number of the last SV sentence
      parsed, and data on each satellite present in the sentence"""
      try:
            num_sv_sentences = int(self.gps_segments)
            current_sv_sentence = int(self.gps_segments)
            sats_in_view = int(self.gps_segments)
      except ValueError:
            return False

      # Create a blank dict to store all the satellite data from this sentence in:
      # satellite PRN is key, tuple containing telemetry is value
      satellite_dict = dict()

      # CalculateNumber of Satelites to pull data for and thus how many segment positions to read
      if num_sv_sentences == current_sv_sentence:
            # Last sentence may have 1-4 satellites; 5 - 20 positions
            sat_segment_limit = (sats_in_view - ((num_sv_sentences - 1) * 4)) * 5
      else:
            sat_segment_limit = 20# Non-last sentences have 4 satellites and thus read up to position 20

      # Try to recover data for up to 4 satellites in sentence
      for sats in range(4, sat_segment_limit, 4):

            # If a PRN is present, grab satellite data
            if self.gps_segments:
                try:
                  sat_id = int(self.gps_segments)
                except (ValueError,IndexError):
                  return False

                try:# elevation can be null (no value) when not tracking
                  elevation = int(self.gps_segments)
                except (ValueError,IndexError):
                  elevation = None

                try:# azimuth can be null (no value) when not tracking
                  azimuth = int(self.gps_segments)
                except (ValueError,IndexError):
                  azimuth = None

                try:# SNR can be null (no value) when not tracking
                  snr = int(self.gps_segments)
                except (ValueError,IndexError):
                  snr = None
            # If no PRN is found, then the sentence has no more satellites to read
            else:
                break

            # Add Satellite Data to Sentence Dict
            satellite_dict = (elevation, azimuth, snr)

      # Update Object Data
      self.total_sv_sentences = num_sv_sentences
      self.last_sv_sentence = current_sv_sentence
      self.satellites_in_view = sats_in_view

      # For a new set of sentences, we either clear out the existing sat data or
      # update it as additional SV sentences are parsed
      if current_sv_sentence == 1:
            self.satellite_data = satellite_dict
      else:
            self.satellite_data.update(satellite_dict)

      return True

    ##########################################
    # Data Stream Handler Functions
    ##########################################

    def new_sentence(self):
      """Adjust Object Flags in Preparation for a New Sentence"""
      self.gps_segments = ['']
      self.active_segment = 0
      self.crc_xor = 0
      self.sentence_active = True
      self.process_crc = True
      self.char_count = 0

    def update(self, new_char):
      """Process a new input char and updates GPS object if necessary based on special characters ('$', ',', '*')
      Function builds a list of received string that are validate by CRC prior to parsing by theappropriate
      sentence function. Returns sentence type on successful parse, None otherwise"""

      valid_sentence = False

      # Validate new_char is a printable char
      ascii_char = ord(new_char)

      if 10 <= ascii_char <= 126:
            self.char_count += 1

            # Write Character to log file if enabled
            if self.log_en:
                self.write_log(new_char)

            # Check if a new string is starting ($)
            if new_char == '$':
                self.new_sentence()
                return None

            elif self.sentence_active:

                # Check if sentence is ending (*)
                if new_char == '*':
                  self.process_crc = False
                  self.active_segment += 1
                  self.gps_segments.append('')
                  return None

                # Check if a section is ended (,), Create a new substring to feed
                # characters to
                elif new_char == ',':
                  self.active_segment += 1
                  self.gps_segments.append('')

                # Store All Other printable character and check CRC when ready
                else:
                  self.gps_segments += new_char

                  # When CRC input is disabled, sentence is nearly complete
                  if not self.process_crc:

                        if len(self.gps_segments) == 2:
                            try:
                              final_crc = int(self.gps_segments, 16)
                              if self.crc_xor == final_crc:
                                    valid_sentence = True
                              else:
                                    self.crc_fails += 1
                            except ValueError:
                              pass# CRC Value was deformed and could not have been correct

                # Update CRC
                if self.process_crc:
                  self.crc_xor ^= ascii_char

                # If a Valid Sentence Was received and it's a supported sentence, then parse it!!
                if valid_sentence:
                  self.clean_sentences += 1# Increment clean sentences received
                  self.sentence_active = False# Clear Active Processing Flag

                  if self.gps_segments in self.supported_sentences:

                        # parse the Sentence Based on the message type, return True if parse is clean
                        if self.supported_sentences](self):

                            # Let host know that the GPS object was updated by returning parsed sentence type
                            self.parsed_sentences += 1
                            return self.gps_segments

                # Check that the sentence buffer isn't filling up with Garage waiting for the sentence to complete
                if self.char_count > self.SENTENCE_LIMIT:
                  self.sentence_active = False

      # Tell Host no new sentence was parsed
      return None

    def new_fix_time(self):
      """Updates a high resolution counter with current time when fix is updated. Currently only triggered from
      GGA, GSA and RMC sentences"""
      try:
            self.fix_time = utime.ticks_ms()
      except NameError:
            self.fix_time = time.time()

    #########################################
    # User Helper Functions
    # These functions make working with the GPS object data easier
    #########################################

    def satellite_data_updated(self):
      """
      Checks if the all the GSV sentences in a group have been read, making satellite data complete
      :return: boolean
      """
      if self.total_sv_sentences > 0 and self.total_sv_sentences == self.last_sv_sentence:
            return True
      else:
            return False

    def unset_satellite_data_updated(self):
      """
      Mark GSV sentences as read indicating the data has been used and future updates are fresh
      """
      self.last_sv_sentence = 0

    def satellites_visible(self):
      """
      Returns a list of of the satellite PRNs currently visible to the receiver
      :return: list
      """
      return list(self.satellite_data.keys())

    def time_since_fix(self):
      """Returns number of millisecond since the last sentence with a valid fix was parsed. Returns 0 if
      no fix has been found"""

      # Test if a Fix has been found
      if self.fix_time == 0:
            return -1

      # Try calculating fix time using utime; if not running MicroPython
      # time.time() returns a floating point value in secs
      try:
            current = utime.ticks_diff(utime.ticks_ms(), self.fix_time)
      except NameError:
            current = (time.time() - self.fix_time) * 1000# ms

      return current

    def compass_direction(self):
      """
      Determine a cardinal or inter-cardinal direction based on current course.
      :return: string
      """
      # Calculate the offset for a rotated compass
      if self.course >= 348.75:
            offset_course = 360 - self.course
      else:
            offset_course = self.course + 11.25

      # Each compass point is separated by 22.5 degrees, divide to find lookup value
      dir_index = floor(offset_course / 22.5)

      final_dir = self.__DIRECTIONS

      return final_dir

    def latitude_string(self):
      """
      Create a readable string of the current latitude data
      :return: string
      """
      if self.coord_format == 'dd':
            formatted_latitude = self.latitude
            lat_string = str(formatted_latitude) + '° ' + str(self._latitude)
      elif self.coord_format == 'dms':
            formatted_latitude = self.latitude
            lat_string = str(formatted_latitude) + '° ' + str(formatted_latitude) + "' " + str(formatted_latitude) + '" ' + str(formatted_latitude)
      else:
            lat_string = str(self._latitude) + '° ' + str(self._latitude) + "' " + str(self._latitude)
      return lat_string

    def longitude_string(self):
      """
      Create a readable string of the current longitude data
      :return: string
      """
      if self.coord_format == 'dd':
            formatted_longitude = self.longitude
            lon_string = str(formatted_longitude) + '° ' + str(self._longitude)
      elif self.coord_format == 'dms':
            formatted_longitude = self.longitude
            lon_string = str(formatted_longitude) + '° ' + str(formatted_longitude) + "' " + str(formatted_longitude) + '" ' + str(formatted_longitude)
      else:
            lon_string = str(self._longitude) + '° ' + str(self._longitude) + "' " + str(self._longitude)
      return lon_string

    def speed_string(self, unit='kph'):
      """
      Creates a readable string of the current speed data in one of three units
      :param unit: string of 'kph','mph, or 'knot'
      :return:
      """
      if unit == 'mph':
            speed_string = str(self.speed) + ' mph'

      elif unit == 'knot':
            if self.speed == 1:
                unit_str = ' knot'
            else:
                unit_str = ' knots'
            speed_string = str(self.speed) + unit_str

      else:
            speed_string = str(self.speed) + ' km/h'

      return speed_string

    def date_string(self, formatting='s_mdy', century='20'):
      """
      Creates a readable string of the current date.
      Can select between long format: Januray 1st, 2014
      or two short formats:
      11/01/2014 (MM/DD/YYYY)
      01/11/2014 (DD/MM/YYYY)
      :param formatting: string 's_mdy', 's_dmy', or 'long'
      :param century: int delineating the century the GPS data is from (19 for 19XX, 20 for 20XX)
      :return: date_stringstring with long or short format date
      """

      # Long Format Januray 1st, 2014
      if formatting == 'long':
            # Retrieve Month string from private set
            month = self.__MONTHS - 1]

            # Determine Date Suffix
            if self.date in (1, 21, 31):
                suffix = 'st'
            elif self.date in (2, 22):
                suffix = 'nd'
            elif self.date == (3, 23):
                suffix = 'rd'
            else:
                suffix = 'th'

            day = str(self.date) + suffix# Create Day String

            year = century + str(self.date)# Create Year String

            date_string = month + ' ' + day + ', ' + year# Put it all together

      else:
            # Add leading zeros to day string if necessary
            if self.date < 10:
                day = '0' + str(self.date)
            else:
                day = str(self.date)

            # Add leading zeros to month string if necessary
            if self.date < 10:
                month = '0' + str(self.date)
            else:
                month = str(self.date)

            # Add leading zeros to year string if necessary
            if self.date < 10:
                year = '0' + str(self.date)
            else:
                year = str(self.date)

            # Build final string based on desired formatting
            if formatting == 's_dmy':
                date_string = day + '/' + month + '/' + year

            else:# Default date format
                date_string = month + '/' + day + '/' + year

      return date_string

    # All the currently supported NMEA sentences
    supported_sentences = {'GPRMC': gprmc, 'GLRMC': gprmc,
                           'GPGGA': gpgga, 'GLGGA': gpgga,
                           'GPVTG': gpvtg, 'GLVTG': gpvtg,
                           'GPGSA': gpgsa, 'GLGSA': gpgsa,
                           'GPGSV': gpgsv, 'GLGSV': gpgsv,
                           'GPGLL': gpgll, 'GLGLL': gpgll,
                           'GNGGA': gpgga, 'GNRMC': gprmc,
                           'GNVTG': gpvtg, 'GNGLL': gpgll,
                           'GNGSA': gpgsa,
                        }

if __name__ == "__main__":
    pass
```
编写测试代码如下
```python
from machine import UART, Pin
import time
from micropyGPS import MicropyGPS


uart0 = UART(0, baudrate=9600, tx=Pin(0), rx=Pin(1))
my_gps = MicropyGPS()

while True:
    if uart0.any():
      if my_gps.update(uart0.read(1).decode("ascii")):
            print("Latitude:", my_gps.latitude_string())
            print("Longitude:", my_gps.longitude_string())
            print("Speed:", my_gps.speed_string("kph"), "or", my_gps.speed_string("mph"), "or", my_gps.speed_string("knot"))
            print("Date (Long Format):", my_gps.date_string("long"))
            print("Date (Short D/M/Y Format):", my_gps.date_string("s_dmy"))
            print("Date (Short M/D/Y Format):", my_gps.date_string("s_mdy"))
            time.sleep(5)
```
等待运行一段时间后日志如下显示
```python
Latitude: 0° 0.0’ N
Longitude: 0° 0.0’ W
Speed: 0.0 km/h or 0.0 mph or 0.0 knots
Date (Long Format): June 8th, 2023
Date (Short D/M/Y Format): 08/06/23
Date (Short M/D/Y Format): 06/08/23
Latitude: 31° 10.23957’ N
Longitude: 121° 31.71391’ E
Speed: 0.0 km/h or 0.0 mph or 0.0 knots
Date (Long Format): June 8th, 2023
Date (Short D/M/Y Format): 08/06/23
Date (Short M/D/Y Format): 06/08/23
```
### 综合示例
设备运行之前,需要先将 **WIFI_SSID**、**WIFI_PASSWORD** 设置为你的无线账户,并注册和风天气创建自己的项目获取 KEY填充 **QWEATHER_KEY**
设备运行时会先连接网络,同步时间,然后进行 gps 定位,通过百度接口转换为城市信息,并通过和风天气接口获取实时天气
```python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from machine import Pin, I2C, RTC, Timer, UART
import time
import utime
import network
import ntptime
import urequests
import ujson
import uzlib
from micropyGPS import MicropyGPS
from ssd1306 import SSD1306_I2C
import ufont
import weather_ico
import framebuf


# gps status
GPS_STATUS_NONE = 0
GPS_STATUS_INIT = 1
GPS_STATUS_IDLE = 2
GPS_STATUS_SYNC = 3
GPS_STATUS_UPDATE = 4

WIFI_SSID = "your-ssid"
WIFI_PASSWORD = "your-password"
QWEATHER_KEY = ""
UTC_OFFSET = 8 * 60 * 60
WEEK = ['一', '二', '三', '四', '五', '六', '日']

g_tick_counter = 1
g_gps_status = GPS_STATUS_NONE
g_longi = "0.0"
g_lati = "0.0"


led = Pin("LED", Pin.OUT)
beep = Pin(Pin(16), Pin.OUT)
timer = Timer()
rtc = RTC()
i2c_oled = I2C(0, scl=Pin(9), sda=Pin(8), freq=400000)
display = SSD1306_I2C(128, 64, i2c_oled)
display.clear()
font = ufont.BMFont("unifont-14-12917-16.v3.bmf")
uart_gps = UART(0, baudrate=9600, tx=Pin(0), rx=Pin(1))
gps = MicropyGPS(location_formatting='dd')
wlan = network.WLAN(network.STA_IF)


def gps_process():
    if uart_gps.any():
      if gps.update(uart_gps.read(1).decode("ascii")):
            # print("Latitude:", gps.latitude_string())
            # print("Longitude:", gps.longitude_string())
            # print("Speed:", gps.speed_string("kph"), "or", gps.speed_string("mph"), "or", gps.speed_string("knot"))
            # # print("Date (Long Format):", gps.date_string("long"))
            # # print("Date (Short D/M/Y Format):", gps.date_string("s_dmy"))
            # print("Date (Short M/D/Y Format):", gps.date_string("s_mdy"))
            global g_gps_status, g_longi, g_lati
            if gps.longitude != 0.0:
                if g_gps_status == GPS_STATUS_NONE:
                  g_gps_status = GPS_STATUS_INIT
                  g_longi = str(round(gps.longitude, 2))
                  g_lati = str(round(gps.latitude, 2))
                  #print("Latitude:", g_longi)
                  #print("Longitude:", g_lati)
                elif g_gps_status == GPS_STATUS_SYNC:
                  g_gps_status = GPS_STATUS_UPDATE
                  g_longi = str(round(gps.longitude, 2))
                  g_lati = str(round(gps.latitude, 2))
                  #print("Latitude:", g_longi)
                  #print("Longitude:", g_lati)


def connect_to_network():
    # Connect to WiFi
    wlan.active(True)
    # wlan.config(pm = 0xa11140) # Disable power-save mode
    wlan.connect(WIFI_SSID, WIFI_PASSWORD)

    max_wait = 10
    while max_wait > 0:
      if wlan.status() < 0 or wlan.status() >= 3:
            break
      max_wait -= 1
      print("waiting for connection..." + str(wlan.status()))
      time.sleep(1)
    if wlan.status() != 3:
      raise RuntimeError("network connection failed")
    else:
      print("connected")
      status = wlan.ifconfig()
      print("ip = " + status)


def ntp_sync_time():
    # Get the current network time and set the system clock
    # print("Local time before synchronization:%s" % str(time.localtime()))
    # ntptime.settime()
    # utctime = ntptime.time() + UTC_OFFSET
    # print("Local time after synchronization:%s" % str(time.localtime(utctime)))
    print("Local time before synchronization:%s" % str(time.localtime()))
    ntptime.host = 'ntp1.aliyun.com'
    ntptime.timeout = 5
    t = ntptime.time() + UTC_OFFSET
    tm = utime.gmtime(t)
    rtc.datetime((tm, tm, tm, tm + 1, tm, tm, tm, 0))
    print("Local time after synchronization:%s" % str(time.localtime()))


def baidu_get_location():
    url = "http://api.map.baidu.com/geocoder?location=" + g_lati + "," + g_longi + "&output=json"
    location = {}
    # print('old url = ' + url)
    # new_url = url.replace("***", g_lati_longi)
    print("request url = " + url)
    res = urequests.get(url=url).json()
    print(res)
    if res["status"] == "OK":
      location['city'] = res["result"]["addressComponent"]["city"]
      location['district'] = res["result"]["addressComponent"]["district"]
      print("location at " + location['city'] + " " + location['district'])
    return location

def qweather_decompress(data):
    FTEXT = 1
    FHCRC = 2
    FEXTRA = 4
    FNAME = 8
    FCOMMENT = 16
    assert data == 0x1f and data == 0x8b
    assert data == 8
    flg = data
    assert flg & 0xe0 == 0
    i = 10
    if flg & FEXTRA:
      i += data << 8 + data + 2
    if flg & FNAME:
      while data:
            i += 1
      i += 1
    if flg & FCOMMENT:
      while data:
            i += 1
      i += 1
    if flg & FHCRC:
      i += 2
    return uzlib.decompress(memoryview(data), -15)


def qweather_city_search():
    url = "https://geoapi.qweather.com/v2/city/lookup?key=" + QWEATHER_KEY + "&location=" + g_longi + "," + g_lati
    print("request url = " + url)
    res = urequests.get(url=url)
    data = qweather_decompress(res.content).decode()
    dic_res = ujson.loads(data)

    if dic_res['code'] == '200':
      name = dic_res["location"]["name"]
      name_id = dic_res["location"]["id"]
      city = dic_res["location"]["adm2"]
      province = dic_res["location"]["adm1"]
      print('locationa at ' + name_id + ' ' + name + ' ' + city + ' ' + province)


def qweather_weather_now():
    url = "https://devapi.qweather.com/v7/weather/now?key=" + QWEATHER_KEY + "&location=" + g_longi + "," + g_lati
    print("request url = " + url)
    res = urequests.get(url=url)
    data = qweather_decompress(res.content).decode()
    dic_res = ujson.loads(data)
    weather = {}

    if dic_res['code'] == '200':
      weather['temp'] = dic_res["now"]["temp"]
      weather['humi'] = dic_res["now"]["humidity"]
      weather['text'] = dic_res["now"]["text"]
      weather['icon'] = dic_res["now"]["icon"]
      print('weather: temp=' + weather['temp'] + ' hump=' + weather['humi'] + ' text=' + weather['text'])
    return weather

def systick_handle(timer):
    global g_tick_counter
    g_tick_counter += 1
    # print("g_tick_counter = " + str(g_tick_counter))
    if wlan.status() != 3 and wlan.status() != 1:
      wlan.connect(WIFI_SSID, WIFI_PASSWORD)

    if not wlan.isconnected():
      led.toggle()
    else:
      if g_tick_counter % 5 == 0:
            led.toggle()
    display.rect(0, 48, 128, 16, 0, True)
    date = time.localtime()
    nyr = '{}/{}/{}'.format(date, date, date)
    display.text(nyr, 0, 48)
    sfm = '{}/{}/{}'.format(date, date, date)
    display.text(sfm, 0, 56)
    font.text(display, '星期' + WEEK], 112-32, 48, show=True)

font.text(display, "WIFI 接入中。。。", 0, 0, show=True)
connect_to_network()
time.sleep_ms(250)
timer.init(freq=5, mode=Timer.PERIODIC, callback=systick_handle)
if wlan.isconnected():
    display.rect(0, 0, 128, 16, 0, True)
    font.text(display, "WIFI 接入成功", 0, 0, show=True)
    ntp_sync_time()
    display.rect(0, 48, 128, 16, 0, True)
    date = time.localtime()
    nyr = '{}/{}/{}'.format(date, date, date)
    display.text(nyr, 0, 48)
    sfm = '{}/{}/{}'.format(date, date, date)
    display.text(sfm, 0, 56)
    font.text(display, '星期' + WEEK], 112-32, 48, show=True)
    '''weather = qweather_weather_now()
    if weather:
      display.rect(0, 16, 112, 32, 0, True)
      font.text(display, '温度 ' + weather['temp'], 0, 16, show=False)
      font.text(display, '湿度 ' + weather['humi'], 0, 32, show=False)
      # font.text(display, weather['text'], 0, 48, show=True)
      fb = framebuf.FrameBuffer(weather_ico.ico], 32, 32, framebuf.MONO_HLSB)
      display.blit(fb, 80, 16)
      display.show()'''
time.sleep_ms(250)
uart_gps.read()
display.rect(0, 0, 128, 16, 0, True)
font.text(display, "定位中。。。", 0, 0, show=True)
time.sleep_ms(250)
sync_start = gps_start = time.ticks_ms()
while True:
    gps_process()

    if wlan.isconnected():
      if time.ticks_diff(time.ticks_ms(), sync_start) > (1000 * 3600):
            sync_start = time.ticks_ms()
            ntp_sync_time()
            weather = qweather_weather_now()
            if weather:
                display.rect(0, 16, 112, 32, 0, True)
                font.text(display, '温度 ' + weather['temp'], 0, 16, show=False)
                font.text(display, '湿度 ' + weather['humi'], 0, 32, show=False)
                # font.text(display, weather['text'], 0, 48, show=True)
                fb = framebuf.FrameBuffer(weather_ico.ico], 32, 32, framebuf.MONO_HLSB)
                display.blit(fb, 80, 16)
                display.show()

      if time.ticks_diff(time.ticks_ms(), gps_start) > (1000 * 60):
            g_gps_status = GPS_STATUS_SYNC

      if g_gps_status == GPS_STATUS_INIT:
            g_gps_status = GPS_STATUS_IDLE
            display.rect(0, 0, 128, 16, 0, True)
            location = baidu_get_location()
            if location:
                display.rect(0, 0, 128, 16, 0, True)
                display.text('E' + g_longi, 0, 0)
                display.text('N' + g_lati, 0, 8)
                #display.text(gps.longitude + str(round(gps.longitude, 2)), 0, 0)
                #display.text(gps.latitude + str(round(gps.latitude, 2)), 0, 8)
                font.text(display, location['district'], 63, 0, show=True)
            weather = qweather_weather_now()
            if weather:
                display.rect(0, 16, 112, 32, 0, True)
                font.text(display, '温度 ' + weather['temp'], 0, 16, show=False)
                font.text(display, '湿度 ' + weather['humi'], 0, 32, show=False)
                # font.text(display, weather['text'], 0, 48, show=True)
                fb = framebuf.FrameBuffer(weather_ico.ico], 32, 32, framebuf.MONO_HLSB)
                display.blit(fb, 80, 16)
                display.show()
      elif g_gps_status == GPS_STATUS_UPDATE:
            g_gps_status = GPS_STATUS_IDLE
            location = baidu_get_location()
            if location:
                display.rect(63, 0, 64, 16, 0, True)
                font.text(display, loca['district'], 63, 0, show=True)
```
显示效果如下


##心得体会
首先非常开心参加这次活动,在此期间让我对 micropython 进一步认识和熟悉。当然也因为个人时间的问题,没能更深入的学习比较遗憾。
最后,感谢社区,感谢得捷!

##相应的代码,演示视频

[演示视频](https://training.eeworld.com.cn/video/36917 "演示视频")

lugl4313820 发表于 2023-7-1 09:11

这个项目感觉非常有个性呀,汉字的大小可以调节的吗?

ID.LODA 发表于 2023-7-3 14:07

lugl4313820 发表于 2023-7-1 09:11
这个项目感觉非常有个性呀,汉字的大小可以调节的吗?

<p>可以调节,显示的时候有参数可选,但是这个字库支持的汉字有点少的,你可以github 上搜一下这个库 ufont</p>

damiaa 发表于 2024-11-6 08:46

<p>不知道这个室内效果如何。</p>

ID.LODA 发表于 2024-11-7 09:31

damiaa 发表于 2024-11-6 08:46
不知道这个室内效果如何。

<p>不太行,至少得放窗边</p>

damiaa 发表于 2024-11-7 10:11

ID.LODA 发表于 2024-11-7 09:31
不太行,至少得放窗边

<p><img height="48" src="https://bbs.eeworld.com.cn/static/editor/plugins/hkemoji/sticker/facebook/wanwan41.gif" width="48" />知道了。谢谢。</p>
页: [1]
查看完整版本: 【得捷电子Follow me第1期】+ 提交贴