2024-2-2 06:58:10 [显示全部楼层]
3908浏览
查看: 3908|回复: 5

[教程] 27-行空板遇上Klipper之三: 做一个基于Klipper的通用延时摄影机

[复制链接]
本帖最后由 pATAq 于 2024-2-2 07:12 编辑

前言

  • Klipper On Unihiker(一)
    • Klipper 全家桶
    • KlipperScreen,横屏,触摸,关闭原来的
    • USB 连接打印机
    • 远程摄像头
  • 优秀的虚拟打印机测试平台(二)
    • 打印
    • 断电续打 plr 演示

原创文章,转载引用请务必注明链接,水平有限,如有疏漏,欢迎交流指正。

文章如有更新请访问 DFRobot 社区cnblogs 博客园,前者内容较全,后者排版及阅读体验更佳。

本文是<u>《行空板遇上 Klipper》</u>系列第三篇,这次我们使用 DFRobot 的行空板做一个便携式延时摄影设备。

我们约定:上位机指运行 Linux 的 MPU 部分,也称为 host。下位机又称 MCU、控制板。

以前玩树莓派+树莓派 CSI 摄像头的时候拍过蓝天白云的的延时摄影,文章如下:

不过当时拍照、调整参数、渲染视频都是手动完成的,后来玩 Klipper 的时候发现它提供了一条龙的延时摄影组件:

  • crowsnest 负责抓取摄像头画面,设置摄像头参数
  • moonraker-timelapse 组件用于完成图像存储、自动渲染等功能
  • WebUI (Fluidd/Mainsail等) 可以直观地设置 timelapse 参数、查看下载渲染好的视频等
  • KlipperScreen 可以实时查看摄像头画面,并提供了一个触摸控制界面

因为之前和 Ash 聊到要写一篇介绍行空板安装使用 Klipper 的文章,一直没空整理,这次一起吧。效果嘛,大概就是下图这么个意思:

【图1】将延时摄影功能独立出来使用。来源:

本文涉及的内容:

  1. 修改配置文件使上位机从打印机中独立出来作为便携式延时摄影拍摄设备使用
  2. 如何在旧版系统上安装 crowsnest
  3. timelapse 两种工作模式介绍
  4. 如何裁剪 Klipper 组件减小生成的固件尺寸,从而在 Arduino Leonardo 等设备上使用
  5. 如何使用 USBTinyISP 编程器配合 AVRDUDESS 软件烧录 Arduino 引导及固件
  6. 对设备树文件的编译/反编译及基本修改
  7. 使用 PGobject 库写一个类似 KlipperScreen 的简单图形控制界面

0、行空板硬件规格

产品链接:行空板 Python编程学习主控板

  • CPU: RK3308 四核 1.2GHz

  • 内存: 512MB DDR3

  • 硬盘: 16GB eMMC

  • 内置操作系统:Debian Buster

  • Wi-Fi:  2.4G

  • 蓝牙:  4.0

  • 板载元件:

    • 实体按键:Home按键,A/B按键
    • 屏幕:2.8寸240*320 TFT彩屏
    • 麦克风传感器
    • 光线传感器
    • 加速度传感器
    • 蜂鸣器
  • 接口:

    • USB  Type-C *1
    • USB  TYPE-A *1
    • microSD卡接口 *1
    • 3Pin I/O *4  (其中支持3路PWM 2路ADC)
    • 4Pin I2C *2
    • 金手指: 19路无冲突I/O(支持I2C、UART、SPI、ADC、PWM)
  • 供电: Type-C 5V供电

我们看到它有非常多的外设/传感器/GPIO,但是查看原理图发现不是和 MPU 直连,通过 MCU 转接,使用 pingpong 库进行操作。

1、安装必要组件

第一篇文章已经介绍了如何在行空板上安装 Klipper 全家桶(Klipper + Moonraker + Fluidd),这里我们不再赘述。

1.1 crowsnest 摄像头串流软件

主要用于读取摄像头的图像。由于我们的系统是基于 Debian Buster,新版的 crowsnest v4 已经不支持了,所以这里我们安装 v3 版本。方法及注意点如下:

cd ~
git clone -b legacy/v3 https://github.com/mainsail-crew/crowsnest.git
cd ~/crowsnest
sudo sed -i 's|#!/usr/bin/python|#!/usr/bin/python2|g' /usr/bin/crudini
sudo make install

注意:

安装过程中调用的 crudini 为 python2 程序,系统默认为 python3,语法不同,需要修改一下才能正确运行。同样也需要系统安装有 python2。

1.2 moonraker-timelapse 延时摄影组件

安装没什么问题,后面的配置文件需要注意。

cd ~/
git clone https://github.com/mainsail-crew/moonraker-timelapse.git
cd ~/moonraker-timelapse
make install

注意:默认临时截取的图片存储于 /tmp 目录中,如果此文件夹挂载在内存中或者设置为独立分区且空间划分较小,可能会导致图像抓取/渲染失败。

同时可以选择抓取的图像保存到 SD 卡中,毕竟板载存储只有 16GB。

1.3 启用 klipper host mcu

由于我们独立使用,不需要外接一块 3D 打印机主板,这里启用上位机作为 mcu 使用。

## 编译并启用 host mcu
# 添加当前用户到必要用户组
sudo usermod -a -G tty $USER

# 启用服务
cd $HOME/klipper
sudo cp ./scripts/klipper-mcu.service /etc/systemd/system/
sudo systemctl enable klipper-mcu.service

# 编译固件
wget -q http://klipper.7130404.xyz:8001/klipper-mcu.config -O $HOME/klipper/.config
make olddefconfig
make clean
make

# 启用固件
sudo service klipper stop
make flash
sudo service klipper start

番外:

行空板板载一颗 MCU 控制大量引脚和传感器,本来想在其上面烧录 Klipper 固件,查阅型号为 GD32VF103C8T6,V代表 RISC-V 架构,这下懵了,目前 Klipper 最多只支持 OpenRISC 的 AR100 (全志的协处理器),还不支持此芯片,看来只能另辟蹊径了。这么说 LattePanda 是一个不错的 Klipper 开发板。

如果想动手移植到新的芯片,可以参考官方文档 Porting to a new micro-controller

2、修改配置文件以独立使用

2.1 单机使用 Klipper

默认情况下,timelapse 功能在一个完整的 3D 打印机上使用,拍摄模型打印的过程,由于我们想要制作一个便携延时摄影机器,就要摆脱打印机主板和打印机框架的束缚,创建一个最小配置文件,让 Klipper 可以正确运行起来无红色报错、无黄色警告。之前介绍过,配置如下:

## printer.cfg
# 以下为必要配置,避免红字报错
[mcu]
serial: /tmp/klipper_host_mcu

[printer]
kinematics: none
max_velocity: 1
max_accel: 1

# 以下为必备配置,避免黄字警告
[pause_resume]
[display_status]
[virtual_sdcard]
path: ~/printer_data/gcodes

[gcode_macro CANCEL_PRINT]
rename_existing: CANCEL_PRINT_BASE
gcode:

# 以下为可选配置
[respond]
[force_move]
enable_force_move: True

[temperature_sensor host]
sensor_type = temperature_host

连接后如图所示:

image-20240202054706577

2.2 配置摄像头串流

我们要正确设置摄像头以获取图像

## crowsnest.conf
[cam 1]
mode: mjpg
port: 8080
device: /dev/v4l/by-id/usb-DSJ_UC60_Video_200901010001-video-index0 # 改成你的设备号
resolution: 1920x1080 # 设置合适的分辨率
max_fps: 15
custom_flags: --host 0.0.0.0
v4l2ctl: focus_auto=1

# 获取摄像头参数
~/crowsnest/tools/dev-helper.sh -c

2.3 timelapse 组件

项目主页:moonraker-timelapse

模式选择:

  • layermacro

    This mode uses a macro to trigger frame grabbing, but needs the slicer to be setup to add such on layerchange (refer to the 'Slicer setup' below)

    使用 TIMELAPSE_TAKE_FRAME 宏触发抓取单张图像,一般用于切片软件中换层抓图,并可以配合挤出头移动到停留区,这样拍出来的效果更好看。

  • hyperlapse

    This mode takes a frame every x seconds configured by the hyperlapse_cycle setting

    开启后每隔 X 秒自动抓图,间隔时间由 hyperlapse_cycle 参数设置

显然我们这里选择 hyperlapse 模式。

禁用停留区

由于我们脱离了打印机使用,所以不存在停留区。

可以在网页调整的选项我们不进行设定,最终的配置文件如下:

## printer.cfg
[include timelapse.cfg]

## moonraker.conf
[timelapse]
# 默认抓取图像临时存储目录,避免空间满
#frame_path: /tmp/timelapse/
enabled: True
mode: hyperlapse
# 每隔 60s 抓取图像
hyperlapse_cycle: 60
# 自动渲染,根据需求选择,默认建议关闭,因为 Unihiker 性能不够强,会占用较多时间
# autorender: False
# 是否将抓取的图像打包成 zip 文件
# saveFrames: False
snapshoturl: http://localhost:8080/?action=snapshot
gcode_verbose: False
parkhead: False
# 生成视频质量,取值范围0–51,0代表无损,默认23,17左右肉眼无损
# constant_rate_factor: 23
# 参数
pixelformat: yuv420p
time_format_code: %Y%m%d_%H%M
# 输出视频帧率
# output_framerate: 30
# flip_x: False
# flip_y: False
# duplicatelastframe: 2
# previewimage: True
wget_skip_cert_check: False
# 指定 POS 否则默认设置会报错"!! Error evaluating 'gcode_macro _SET_TIMELAPSE_SETUP:gcode': gcode.CommandError: CUSTOM_POS_X=10.0 must be within [0.0 - 0.0]"
park_custom_pos_x: 0.0
park_custom_pos_y: 0.0
park_custom_pos_dz: 0.0

手动开始和停止拍摄的命令如下:

HYPERLAPSE ACTION=START CYCLE=10
HYPERLAPSE ACTION=STOP

至此,我们可以实现摄像头显示,根据指令开始、停止拍摄,设置参数,手动渲染,下载渲染好的视频。

image-20240202055757065

3、拍摄控制系统

摄像我们需要 3 个,最少 2 个按钮进行控制,分别为:开始、停止、关机。可选 渲染 按钮,其中开始和停止按钮可以使用一个按钮进行切换,但是为了避免误触,也可以分开控制。

3.1 使用行空板 A/B/Home 按键【失败】

本着外设尽量少,多使用板载资源的想法,我们首先想到使用 A/B 和 Home 按键,查看上面的原理图可知三者都连接的 MPU,其中 AB 也和 MCU 连接。首先看下目前 GPIO 的使用情况:

pi@unihiker:~$ sudo cat /sys/kernel/debug/gpio
GPIOs 0-31, platform/pinctrl, gpio0:
 gpio-2   (                    |reset               ) out hi
 gpio-5   (                    |?                   ) out lo
 gpio-6   (                    |?                   ) out hi
 gpio-17  (                    |dfrobot-gpio-a      ) in  hi
 gpio-18  (                    |dfrobot-gpio-b      ) in  hi
 gpio-21  (                    |vcc_otg_vbus        ) out hi

GPIOs 32-63, platform/pinctrl, gpio1:
 gpio-44  (                    |fb_ili9341          ) out hi
 gpio-46  (                    |fb_ili9341          ) out hi

GPIOs 64-95, platform/pinctrl, gpio2:

GPIOs 96-127, platform/pinctrl, gpio3:

GPIOs 128-159, platform/pinctrl, gpio4:
 gpio-135 (                    |bt_default_rts      ) out lo
 gpio-139 (                    |bt_default_poweron  ) out hi
 gpio-140 (                    |bt_default_wake_host) in  hi
 gpio-158 (                    |vcc_sd              ) out hi

可见 A/B 名为 dfrobot-gpio-[a|b],Home 键没找到 。我们修改设备树文件释放这两者:

# 查看设备树文件, 可知使用的设备树文件为 fdtfile=rockchip/pythonboard.dtb
cat /boot/uEnv.txt

## 反编译设备树,查找对应名称并禁用
sudo apt-get install device-tree-compiler
dtc -I dtb -O dts /boot/dtbs/$(uname -r)/rockchip/pythonboard.dtb -o ~/pythonboard.dts
# 将如下 okay 改成 disabled
        keyboard-ablf {
                compatible = "dfrobot,keyboard-ablf";
                status = "okay";
                dfrobot-gpio-a = < 0x78 0x11 0x02 >; 
                dfrobot-gpio-b = < 0x78 0x12 0x02 >;
                dfrobot-gpio-lf = < 0x78 0x20 0x02 >;
        };
# 编译设备树,拷贝到对应目录,修改 uEnv.txt 使用新的设备树文件
dtc -I dts -O dtb -o pythonboard_nokey.dtb pythonboard.dts
sudo mv pythonboard_nokey.dtb /boot/dtbs/$(uname -r)/rockchip/
# /boot/uEnv.txt
fdtfile=rockchip/pythonboard_nokey.dtb

# 重启,确认修改成功
sudo reboot

失败

重启后发现 ab 按键被释放为 GPIO,但是 moonraker 无法控制,查询可知是内核版本低, 需要 4.8 以上才支持 libgpiod。

Since Linux version 4.8 the GPIO sysfs interface is deprecated, and now we have a new API based on character devices to access GPIO lines from user space. 来源

解决思路

  1. 修改 mrk/klipper 源码来支持旧版 GPIO sysfs 的操作方式
  2. 升级为新版内核的操作系统,可以参考 Armbian RK3088 (RockPi S) 进行移植 6.x Bookworm 系统,可能需要修改设备树文件

3.2 使用外置控制板按钮(Sparrow 限时返场)

image-20240201145515606

从收纳盒里找到 Sparrow 控制板,其使用 ATmega32u4 微控制器,类似 Arduino Leonardo,我们设想使用其连接三颗按钮模块,以及使用板载的彩色灯珠显示状态。

3.2.1 编译烧录 Klipper 固件【失败】

image-20240201145914023

由于我们使用 Buster 版本,不存在 AVR 编译工具链需要降级的问题,所以直接烧录即可,但实际上遇到各种问题,归结为两个问题。

现象
  1. 使用 Arduino IDE 烧录 blink,正常
  2. 使用 Linux avrdude 和 Windows AVRDUDESS 烧录 klipper 和 blink 都提示编程器响应超时,无法烧录
  3. 使用 USBTinyISP 可以烧录 klipper 但是提示 mismatch,blink 没问题
原因

2 的原因是 32u4 的板子烧写前先按一下板子上的Reset按钮 1-2s。

3 的原因是编译出来的 klipper 固件大于 32u4 的存储空间

3.2.2 探索过程

编程器通讯超时无响应:

由于 Arduino IDE 不支持自定义 hex 烧录,我们使用 AVRDUDESS 烧录固件(avrdude)。

先测试 blink hex,ide 找到已编译目录,blink with BootLoader,烧录参数可以打开 ide 的上传参数显示选项,可知:

System wide configuration file is "C:\Users\xin\AppData\Local\Arduino15\packages\arduino\tools\avrdude\6.3.0-arduino17/etc/avrdude.conf"

         Using Port                    : COM21
         Using Programmer              : avr109
         Overriding Baud Rate          : 57600
         AVR Part                      : ATmega32U4
         Chip Erase delay              : 9000 us
         PAGEL                         : PD7
         BS2                           : PA0
         RESET disposition             : dedicated
         RETRY pulse                   : SCK
         serial program mode           : yes
         parallel program mode         : yes
         Timeout                       : 200
         StabDelay                     : 100
         CmdexeDelay                   : 25
         SyncLoops                     : 32
         ByteDelay                     : 0
         PollIndex                     : 3
         PollValue                     : 0x53
         Memory Detail                 :

                                  Block Poll               Page                       Polled
           Memory Type Mode Delay Size  Indx Paged  Size   Size #Pages MinW  MaxW   ReadBack
           ----------- ---- ----- ----- ---- ------ ------ ---- ------ ----- ----- ---------
           eeprom        65    20     4    0 no       1024    4      0  9000  9000 0x00 0x00
           flash         65     6   128    0 yes     32768  128    256  4500  4500 0x00 0x00
           lfuse          0     0     0    0 no          1    0      0  9000  9000 0x00 0x00
           hfuse          0     0     0    0 no          1    0      0  9000  9000 0x00 0x00
           efuse          0     0     0    0 no          1    0      0  9000  9000 0x00 0x00
           lock           0     0     0    0 no          1    0      0  9000  9000 0x00 0x00
           calibration    0     0     0    0 no          1    0      0     0     0 0x00 0x00
           signature      0     0     0    0 no          3    0      0     0     0 0x00 0x00

         Programmer Type : butterfly
         Description     : Atmel AppNote AVR109 Boot Loader

Connecting to programmer: .
Found programmer: Id = "CATERIN"; type = S
    Software Version = 1.0; No Hardware Version given.

故编程器选择 Atmel AppNote AVR109 Boot Loader,波特率选择 57600,由于AVRDUDESS 内置的 avdude 版本和 IDE 不同,我们可以在选项中使用 IDE 的 avrdude.exe、avrdude.conf 和 avr-size.exe。

image-20240201150639752

但无论是何种工具烧录,都报错超时,最终发现:

对于32u4芯片的Arduino板子,必须在烧写前先按一下板子上的Reset按钮,等1-2秒再执行烧写命令。来源:用命令行给Arduino烧写软件程序

可以烧录但是报错 mismatch:

使用 USBTinyUSB 烧录,提示 mismatch,驱动可以从 adafruit 下载,注意它的槽口方向一致

怀疑 bootloader 故障,重新烧录后依旧。

找到一块 UNO (328p) 烧录 Klipper 正常。

最后发现是编译出的固件超出 32u4 的存储空间了。Arduino UNO 因为有 16u2 用于 USB 通讯,而 32u4 需要在固件中包括额外的 USB 通讯功能,所以固件尺寸较大。

3.2.3 裁剪 Klipper 功能

参看 Fimware too large for microcontroller (atmega32u4)

解决方法:

  1. 使用更小的BootLoader:kp_boot_32u4,仅有 1KB 大小
# 使用 ISP 编程器通过 ICSP 引脚烧录 bootloader
git clone https://github.com/ahtn/kp_boot_32u4
git submodule update --init --recursive
make
make program-fuses
make program-hard
make program-lock
# 也可以自烧录 bootloader

# 查询设备并烧录固件
./kp_boot_32u4_cli.py -l
./kp_boot_32u4_cli.py -f program.hex
# 也可以使用图形界面烧录
  1. 裁剪 klipper 功能组件(与 F103C6 不同)

打开 klipper/src/Makefile,注释用不到的 tmcuart.c 及加速度传感器等,然后重新编译固件。也可以修改源码,增加 Klipper 功能组件选单。

# Main code build rules

src-y += sched.c command.c basecmd.c debugcmds.c
src-$(CONFIG_HAVE_GPIO) += initial_pins.c gpiocmds.c stepper.c endstop.c \
    trsync.c
src-$(CONFIG_HAVE_GPIO_ADC) += adccmds.c
src-$(CONFIG_HAVE_GPIO_SPI) += spicmds.c
src-$(CONFIG_HAVE_GPIO_SDIO) += sdiocmds.c
src-$(CONFIG_HAVE_GPIO_I2C) += i2ccmds.c
src-$(CONFIG_HAVE_GPIO_HARD_PWM) += pwmcmds.c

src-$(CONFIG_WANT_GPIO_BITBANGING) += buttons.c tmcuart.c neopixel.c \
    pulse_counter.c
src-$(CONFIG_WANT_DISPLAYS) += lcd_st7920.c lcd_hd44780.c
src-$(CONFIG_WANT_SOFTWARE_SPI) += spi_software.c
src-$(CONFIG_WANT_SOFTWARE_I2C) += i2c_software.c
sensors-src-$(CONFIG_HAVE_GPIO_SPI) := thermocouple.c sensor_adxl345.c \
    sensor_angle.c
sensors-src-$(CONFIG_HAVE_GPIO_I2C) += sensor_mpu9250.c
src-$(CONFIG_WANT_SENSORS) += $(sensors-src-y)
src-$(CONFIG_WANT_LIS2DW) += sensor_lis2dw.c
src-$(CONFIG_NEED_SENSOR_BULK) += sensor_bulk.c

3.2.4 使用板载 LED 和按钮

我们定义两个按钮用于触发开始和停止,同时使用板载 LED 灯珠显示当前状态。

# sparrow 上的 D6-D9 数字引脚分别对应 32u4 的 PD7 PB4 PB5
[gcode_button start_capture]
pin: PD7
press_gcode:
release_gcode:
  HYPERLAPSE ACTION=START CYCLE=10

[gcode_button stop_capture]
pin: PD7
press_gcode:
release_gcode:
  HYPERLAPSE ACTION=STOP

# sparrow 的 ws2812 接在 D10 引脚,也就是 PB6
# 这里我们使用 LED Effects 插件

3.3 使用屏幕按钮控制

既然我们有屏幕,就直接使用。两种方案:使用 KlipperScreen,或者使用行空板的 UI 库。我们先试试前者。复杂的功能也不想写了,篇幅有限,而且整理文档特别费时费神。

3.3.1 KlipperScreen

和 KlipperScreen 一样使用 PyGObject库,这里设计一个简单的界面,包括三个按钮:开始/停止/关机。标题设置为 Klipper 延时摄影控制系统

由于行空板配备有 2.8 寸分辨率为 240*320 的 TFT 彩屏。由于我们在上文已经设置横向显示。所以设计界面如下:

40 title

10 |100,100,100 | 10

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
import subprocess

class MyWindow(Gtk.Window):

    def __init__(self):
        Gtk.Window.__init__(self)

        # 设置窗口标题
        header_bar = Gtk.HeaderBar(title="Klipper 延时摄影控制系统")
        header_bar.set_show_close_button(False)
        self.set_titlebar(header_bar)

        # 设置默认大小
        self.set_default_size(300, 200)

        # 设置窗口位置(左上角坐标)
        self.move(10, 0)

        def on_button1_clicked(button):
            subprocess.call(["curl", "-X", "POST", "http://127.0.0.1/printer/gcode/script?script=START"])

        def on_button2_clicked(button):
            subprocess.call(["curl", "-X", "POST", "http://127.0.0.1/printer/gcode/script?script=STOP"])

        def on_button3_clicked(button):
            subprocess.call(["curl", "-X", "POST", "http://127.0.0.1/machine/shutdown"])

        # 添加三个按钮到水平布局
        button1 = Gtk.Button(label="开始")
        button1.connect("clicked", on_button1_clicked)

        button2 = Gtk.Button(label="停止")
        button2.connect("clicked", on_button2_clicked)

        button3 = Gtk.Button(label="关机")
        button3.connect("clicked", on_button3_clicked)

        # 创建一个水平布局
        hbox = Gtk.HBox()
        hbox.pack_start(button1, True, True, 0)
        hbox.pack_start(button2, True, True, 0)
        hbox.pack_start(button3, True, True, 0)

        self.add(hbox)

win = MyWindow()
win.connect("destroy", Gtk.main_quit)
win.show_all()
Gtk.main()
  • 本次使用最简单的 Http POST 来和 moonraker 交互,实现开始拍摄/停止拍摄/关闭设备的功能
  • 进一步可以使用 websocket 通讯
  • 未来可以集成进 KlipperScreen 界面中

image-20240202070820832

3.3.2 行空板界面

教程,支持图形化编程,类似下图的效果,太累了,今天就不具体介绍了:

注意需要关闭 KlipperScreen,最好恢复竖屏和触摸。

4、优化

  • 3D 打印固定支架

  • 添加电池供电

  • 显示屏幕拍摄记录等

  • 导出图像后使用高性能主机进行视频渲染

  • 配合 虾米拓展板 实现更多功能

ASH腻  管理员

发表于 2024-2-2 10:16:01

哇 期待已久的大作
回复

使用道具 举报

木子呢  管理员

发表于 2024-2-2 10:34:35

大佬新作!膜拜
回复

使用道具 举报

RRoy  超级版主

发表于 2024-2-4 16:07:04

学习一下
回复

使用道具 举报

glwz007  初级技匠

发表于 2024-2-6 19:24:45

谢谢分享!
这是第三,请问哪里看第一和第二?
谢谢!
回复

使用道具 举报

pATAq  版主
 楼主|

发表于 2024-2-6 20:20:29

glwz007 发表于 2024-2-6 19:24
谢谢分享!
这是第三,请问哪里看第一和第二?
谢谢!

第1和第2实验已经完成,还没整理,哈哈
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

为本项目制作心愿单
购买心愿单
心愿单 编辑
[[wsData.name]]

硬件清单

  • [[d.name]]
btnicon
我也要做!
点击进入购买页面
上海智位机器人股份有限公司 沪ICP备09038501号-4

© 2013-2024 Comsenz Inc. Powered by Discuz! X3.4 Licensed

mail