2024年7月

作者:来自 vivo 互联网前端团队- Su Ning

作为一个3D的项目,从用户打开页面到最终模型的渲染需要经过多个流程,加载的时间也会比普通的H5项目要更长一些,从而造成大量的用户流失。为了提升首屏加载的转化率,需要尽可能的降低loading的时间。这里就分享一些我们在模型加载优化方面的心得。

一、前言

近段时间,我们使用three.js完成了vivo拟我形象的开发工作,大家可以在vivo账号中拟制属于自己的3D形象,也可以保存作为自己的头像名片。

作为一个3D的项目,从用户打开页面到最终模型的渲染需要经过多个流程,加载的时间也会比普通的H5项目要更长一些。然而过长的等待时间会造成大量的用户流失,这部分用户没有体验到具体的功能就退出了页面非常的遗憾,为了提升首屏加载的转化率,需要尽可能的降低loading的时间。这里就分享一些我们在模型加载优化方面的心得。

二、模型加载的优化思路

想对加载进行优化,首先需要了解Three.js加载模型时的工作流程,并分析出其中耗时的部分进行针对性的处理。

在Three.js中,模型从加载到渲染需要经过模型下载、序列化模型、网格解析、写入缓存和渲染模型几个步骤,经过分析发现主要的瓶颈在网络请求和网格解析两个部分,所以整体的优化思路就是减少网络请求资源的体积和提升网格的解析速度。

图片

三、缩小模型的体积

3.1 常见的解决方案

目前主流的压缩方案是使用google的draco库对模型进行压缩。draco的原理类似降低图片的分辨率,通过减少模型的顶点数起到压缩体积的效果。

也就是说draco是一种有损的压缩方式,这样就会带来诸多的
问题

  • 可能在网格连接处存在画面模型撕裂。

  • 仅仅压缩顶点只能将50.7mb的人物模型压缩到49.5,压缩效果有限。

  • draco前台decoder在h5中的解算效率不理想,可能节省下来的网络请求时间还没有增加的数据解算的时间长。

基于以上几点,最终我们放弃了draco的压缩方案。

图片

使用draco压缩之后导致的模型撕裂

3.2 进阶方案

高端的食材,往往只需要采用最朴素的烹饪方式。经过一些尝试,我们发现将glb模型直接打成zip包可以明显的提升模型的压缩效率。50.7mb的人物模型可以压缩到11.6mb。

但是Three.js提供的gltfloader是不能直接加载zip文件的,于是我们需要对其进行功能扩展。

Three.js加载gltf模型是首先通过fetch请求获取到模型的arraybuffer,再对arraybuffer进行格式化。所以我们只需要在模型格式化之前拦截zip文件进行解压缩即可。

图片

于是我们使用jszip,资源加载完成后判断资源的后缀,如果是zip文件就使用jszip进行解压缩。

图片

看起来还不错,在保证视觉效果的同时又可以大幅压缩模型的体积,那么有没有可能做的再极致一些呢?

既然是针对性的场景,我们就可以从解压缩的解算开始入手,于是我们使用rust写了一个解压工具,将其转换成wasm包代替jszip,可以发现wasm的冷启动性能确实要比js好很多,可以将解压的时长从几十到100毫秒降低到1毫秒左右,适合体积比较小的解压缩场景。

图片

图片

四、文件的加解密

作为一个h5项目,获取到静态资源的链接并不困难,所以需要对模型文件进行一点点加密,让破解起来没有那么容易。同时解密的过程不能显著延长资源加载的时间,影响用户体验。

基于数据解密的效率,我们可以截取文件buffer的一部分进行加密,而不对全文进行加密,同时将数据解密的过程也放到wasm中,提升解算效率的同时也增强了安全性。采用对称加密的算法,同一个方法既可以用于加密,也可以用于解密。

按照模型加载的流程,解密的操作应该放在解压缩之后,序列化之前,那么如何判断数据是否进行了加密呢,可以通过判断解压数据decode以后是否有glTF的标记来确定。

图片

如下图,数据解密的耗时几乎可以忽略不计,可以放心使用。

图片

五、如何优化首帧的渲染体验

优化完模型的加载,继续来优化模型的渲染,在加载一个体积比较大的模型的时候经常会有页面的卡死的情况出现,需要从两方面治标也治本的进行优化:

  1. 通过减少页面的卡停来优化用户的感官体验。

  2. 通过缩短首屏渲染的时长来解决根本问题。

5.1 减少页面的卡停

在模型加载的时候通常会设置一个loading页面来展示当前的加载进度,同时loading页也可以播放一些动效或者互动来让用户等待的过程中不那么无聊。但是由于js单线程的特性,在进行首帧渲染的时候任何事件都不无法响应,会让用户误以为页面卡死,造成流失。

为了解决这个问题我们可以使用分步加载的方案,在模型加载的时候先遍历第一层网格,将所有的网格隐藏起来,然后循环这些网格,每展示一个就执行一次render方法,这样就可以把一个大的卡顿分散成多个小的,不至于影响前台的体验。

图片

但是这样的方法只能让用户感受起来没那么卡顿,该等的时间一点没少,过长的等待时间还是会让用户等的不耐烦,有没有其他解决卡顿的方式呢?这就要从Three.js的渲染逻辑来进行分析了。

5.2 缩短首帧渲染的时间

由于我们做的是一个捏脸的项目,通过形态键来实现不同的脸型,表情等表现。在Three.js中存储形态键信息的属性在geometry.morphAttributes中,形态键存放的顶点信息总数与网格的顶点数相同,这就意味着同一个模型有多少个形态键,就额外需要加载多少套网格的顶点信息。在首次渲染的时候Three.js会遍历每一个形态键的顶点信息,生成一个float32array,而这个巨量的遍历操作就是造成卡顿的根本原因。

图片

如何解决这个循环黑洞呢,我想到了steamdeck上的着色器预缓存,通过将着色器编译的结果进行持久化,缩短页面加载的时间。那么我们只要将每一个网格的形态键编译的结果储存起来就行了。

  • /three/src/renderers/webgl/WebGLMorphtargets.js

图片

通过这种方式成功的将首帧渲染的时间从7秒缩短到0.6秒,大幅的提升了用户的体验。

讲到这里,大家可能发现了,虽然首帧渲染的时长缩短了,但是形态键缓存的资源有80mb,压缩后也有15mb,这块的时长可不可以继续压榨呢,先看一下资源的处理流程,处理解压后的文件需要将文件解析成JSON字符串,然后在转换成float32array,这里耗时最大的点就是JSON.parse的操作,有没有更好的方式处理呢,可以将这部分内容丢到rust里面,平均可以减少0.5s的时间。

图片

图片

六、总结与规划

以上就是我们的优化流程,将glb模型文件压缩成zip包,配合前台wasm解压工具降低模型的加载时间。通过增加形态键缓存的方式来降低首帧渲染的时长。

图片

经过这一系列的操作,成功的将模型的体积从50mb压缩到11mb,增加了额外80mb的形态键缓存也可以使用zip压缩到15mb,处理后页面的首次加载时长从15秒缩短到5秒,算是一个不小的提升。

然而,我们也意识到还有进一步的优化空间,譬如目前虽然有了形态键缓存,但是原模型中的形态键信息还存储在模型中,这一部分的信息不需要被threejs读取,却很大的占用了模型的体积,后续可以开发一个gltf-pipeline类似的处理工具,将形态键缓存直接整合进gltf模型中,同时把整个模型的序列化工作放到wasm中处理,降低模型的尺寸的同时也可以减少模型解析的时长。期待为大家带来更好的使用体验。

现象:

最近遇到了WCF 服务无法调用的错误,异常如下。

System.ServiceModel.ProtocolException, System.ServiceModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 Error while reading message framing format at position 0 of stream (state: ReadingUpgradeRecord)

System.IO.InvalidDataException, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 More data was expected, but EOF was reached.

日志

WCF的问题一般需要对框架比较熟悉,有时难以定位。第一眼找不到原因的先
根据官方文档打开WCF日志

日志打开后发现服务端完全没有调用的记录。这时怀疑是不是调错端口了,打开资源管理器,服务正常跑着。只能再打开Wireshark记录日志,大概就是正常的3次握手,不正常的不知为何服务端主动FIN并RESET的连接。

在WCF的问题处理中,自己的日志/WCF的日志/抓包的日志都很重要。

原因

最后的原因发现是端口被罗技升级程序占用的问题, 考虑如下示例代码

// 设置要监听的端口号
  int port = 13000;
TcpListener listener = new TcpListener(IPAddress.Any, port);
TcpListener listener2 = new TcpListener(IPAddress.Loopback, port);
// 开始监听
listener.Start();
listener2.Start();
// 并不会抛出异常
Console.ReadLine();

工作久了过于相信经验,感觉一定会异常,但端口监听的是套接字,自然0.0.0.0:13000 和 127.0.0.1:13000不是一回事。

WCF异常原理

wcf的通信流程大概是这样的。

// 客户端发送服务端基地址,确认这是一个wcf的服务
client-> service: net.tcp://127.0.0.1:39100/Service

// 服务端回0b 代表确认
service-> client: 0x0b

// 客户端发送具体调用的内容
.....

由于服务端不回
0x0b
,自然会出现这样的异常。附一下异常的调用栈。事后看看也挺明显。

System.ServiceModel.Channels.ConnectionUpgradeHelper.ValidatePreambleResponse(Byte[] buffer, Int32 count, ClientFramingDecoder decoder, Uri via)
System.ServiceModel.Channels.ClientFramingDuplexSessionChannel.SendPreamble(IConnection connection, ArraySegment`1 preamble, TimeoutHelper& timeoutHelper)
System.ServiceModel.Channels.ClientFramingDuplexSessionChannel.DuplexConnectionPoolHelper.AcceptPooledConnection(IConnection connection, TimeoutHelper& timeoutHelper)
System.ServiceModel.Channels.ConnectionPoolHelper.EstablishConnection(TimeSpan timeout)
System.ServiceModel.Channels.ClientFramingDuplexSessionChannel.OnOpen(TimeSpan timeout)
System.ServiceModel.Channels.CommunicationObject.Open(TimeSpan timeout)
System.ServiceModel.Channels.ServiceChannel.OnOpen(TimeSpan timeout)
System.ServiceModel.Channels.CommunicationObject.Open(TimeSpan timeout)
System.ServiceModel.Channels.ServiceChannel.CallOpenOnce.System.ServiceModel.Channels.ServiceChannel.ICallOnce.Call(ServiceChannel channel, TimeSpan timeout)
System.ServiceModel.Channels.ServiceChannel.CallOnceManager.CallOnce(TimeSpan timeout, CallOnceManager cascade)
System.ServiceModel.Channels.ServiceChannel.EnsureOpened(TimeSpan timeout)
System.ServiceModel.Channels.ServiceChannel.Call(String action, Boolean oneway, ProxyOperationRuntime operation, Object[] ins, Object[] outs, TimeSpan timeout)
System.ServiceModel.Channels.ServiceChannelProxy.InvokeService(IMethodCallMessage methodCall, ProxyOperationRuntime operation)
System.ServiceModel.Channels.ServiceChannelProxy.Invoke(IMessage message)
System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(MessageData& msgData, Int32 type)
...

前言

出息了,这回0元玩玄机了,因为只是日志分析,赶紧导出来就关掉(五分钟内不扣金币)
日志分析只要会点正则然后配合Linux的命令很快就完成这题目了,非应急响应.

简介

账号密码 root apacherizhi
ssh root@IP
1、提交当天访问次数最多的IP,即黑客IP:
2、黑客使用的浏览器指纹是什么,提交指纹的md5:
3、查看index.php页面被访问的次数,提交次数:
4、查看黑客IP访问了多少次,提交次数:
5、查看2023年8月03日8时这一个小时内有多少IP访问,提交次数:

应急开始

准备工作

  • 找到apache的日志文件:
    /var/log/apache2/access.log (在题目靶机中日志文件为/var/log/apache2/access.log.1)
  • 迅速将文件导出来,还是心疼我的金币,不要浪费了:scp -r root@ip:/var/log/apache2/ /tmp/
    或者你可以使用xftp等等攻击直接导出来。
  • 我这里其实准备了360星图的工具,在真实的日志分析中会比较有用,我们这里根据题目要求来找flag的话其实Linux指令已经完全够用完成我们的任务了。
    下面还是附上我星图扫完后的html结果吧,不得不说这界面做的真好看啊。

在这里插入图片描述
在这里插入图片描述

步骤 1

1、提交当天访问次数最多的IP,即黑客IP:

  • 找到apache的日志文件

    cd /var/log/apache2 中的 access.log.1文件就是题目保存下来的日志文件
    
  • 使用Linux命令直接梭哈,直接就知道那个是访问次数多的ip了

    awk '{print $1}' /var/log/apache2/access.log.1 | uniq -c | sort -n
    

    在这里插入图片描述

  • flag为:
    flag{192.168.200.2}

步骤 2

2、黑客使用的浏览器指纹是什么,提交指纹的md5:

  • 已知黑客ip了,grep根据ip筛选日志记录即可知道,但是这里我在观察的时候发现这个黑客ip还使用了Firefox,你直接grep Firefox能够筛选出来黑客ip的,所以如果不确定就两个ip都提交一下即可。
    grep -Ea "192.168.200.2" /var/log/apache2/access.log.1
    

    在这里插入图片描述
  • 指纹为:
    Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
    flag为指纹的md5值
    flag{2d6330f380f44ac20f3a02eed0958f66}

步骤 3

3、查看index.php页面被访问的次数,提交次数:

  • 坑点:注意筛选的时候不要直接 grep 'index.php',因为黑客可能在访问的时候访问的index.php前缀还有,比如xxxindex.php这种也能筛选出来,这样的话你的index.php访问次数就不准确了,所以我们应该加一个字符限制就是反斜杠
    /
    grep -Ea '/index.php' /var/log/apache2/access.log.1 | wc -l #wc是计算行数的
    

    在这里插入图片描述
  • flag为:
    flag{27}

步骤 4

4、查看黑客IP访问了多少次,提交次数:

  • 由于已知黑客ip,所以直接过滤日志信息即可
    (这里有一个坑,我们在过滤的时候记得精准一下,ip后面的两个横杠记得加上去筛选- -,否则一些日志记录中也会命中你的规则,那就可能统计的数量不精准)
    grep -Ea "^192.168.200.2 - -" /var/log/apache2/access.log.1 | wc -l
    

    在这里插入图片描述
  • flag为:
    flag{6555}

步骤 5

5、查看2023年8月03日8时这一个小时内有多少IP访问,提交次数:

  • apache的日志记录时间格式是日月年时分秒
    在这里插入图片描述
  • 照样直接上Linux命令即可筛选出来
    grep -Ea "^[0-9]+.*+03/Aug/2023:[08|09]" /var/log/apache2/access.log.1 | awk '{print $1}' | uniq -c | wc -l
    

    	在这里插入图片描述
  • flag为:
    flag{5}

总结


成果:
flag{192.168.200.2}
flag{2d6330f380f44ac20f3a02eed0958f66}
flag{27}
flag{6555}
flag{5}


本次日志分析相对来说还是比较简单和快速,没啥可以说的,对Linux的一些指令更加熟悉了,同时也发现Linux里面的一些命令工具其实就能够直接充当日志分析了。

全网最适合入门的面向对象编程教程:20 类和对象的 Python 实现-组合关系的实现与 CSV 文件保存

摘要:

本文主要介绍了在使用 Python 面向对象编程时,如何实现组合关系,同时对比了组合关系和继承关系的优缺点,并讲解了如何通过 csv 模块来保存 Python 接收/生成的数据。

原文链接:

FreakStudio的博客

往期推荐:

学嵌入式的你,还不会面向对象??!

全网最适合入门的面向对象编程教程:00 面向对象设计方法导论

全网最适合入门的面向对象编程教程:01 面向对象编程的基本概念

全网最适合入门的面向对象编程教程:02 类和对象的 Python 实现-使用 Python 创建类

全网最适合入门的面向对象编程教程:03 类和对象的 Python 实现-为自定义类添加属性

全网最适合入门的面向对象编程教程:04 类和对象的Python实现-为自定义类添加方法

全网最适合入门的面向对象编程教程:05 类和对象的Python实现-PyCharm代码标签

全网最适合入门的面向对象编程教程:06 类和对象的Python实现-自定义类的数据封装

全网最适合入门的面向对象编程教程:07 类和对象的Python实现-类型注解

全网最适合入门的面向对象编程教程:08 类和对象的Python实现-@property装饰器

全网最适合入门的面向对象编程教程:09 类和对象的Python实现-类之间的关系

全网最适合入门的面向对象编程教程:10 类和对象的Python实现-类的继承和里氏替换原则

全网最适合入门的面向对象编程教程:11 类和对象的Python实现-子类调用父类方法

全网最适合入门的面向对象编程教程:12 类和对象的Python实现-Python使用logging模块输出程序运行日志

全网最适合入门的面向对象编程教程:13 类和对象的Python实现-可视化阅读代码神器Sourcetrail的安装使用

全网最适合入门的面向对象编程教程:全网最适合入门的面向对象编程教程:14 类和对象的Python实现-类的静态方法和类方法

全网最适合入门的面向对象编程教程:15 类和对象的 Python 实现-__slots__魔法方法

全网最适合入门的面向对象编程教程:16 类和对象的Python实现-多态、方法重写与开闭原则

全网最适合入门的面向对象编程教程:17 类和对象的Python实现-鸭子类型与“file-like object“

全网最适合入门的面向对象编程教程:18 类和对象的Python实现-多重继承与PyQtGraph串口数据绘制曲线图

全网最适合入门的面向对象编程教程:19 类和对象的 Python 实现-使用 PyCharm 自动生成文件注释和函数注释

更多精彩内容可看:

给你的 Python 加加速:一文速通 Python 并行计算

一文搞懂 CM3 单片机调试原理

肝了半个月,嵌入式技术栈大汇总出炉

电子计算机类比赛的“武林秘籍”

一个MicroPython的开源项目集锦:awesome-micropython,包含各个方面的Micropython工具库

文档和代码获取:

可访问如下链接进行对文档下载:

https://github.com/leezisheng/Doc

image

本文档主要介绍如何使用 Python 进行面向对象编程,需要读者对 Python 语法和单片机开发具有基本了解。相比其他讲解 Python 面向对象编程的博客或书籍而言,本文档更加详细、侧重于嵌入式上位机应用,以上位机和下位机的常见串口数据收发、数据处理、动态图绘制等为应用实例,同时使用 Sourcetrail 代码软件对代码进行可视化阅读便于读者理解。

相关示例代码获取链接如下:
https://github.com/leezisheng/Python-OOP-Demo

正文

前面讲了面向类与对象的继承,知道了继承是一种什么“是”什么的关系。然而类与类之间还有另一种关系,这就是组合。
组合是将几个对象收集在一起生成一个新对象的行为。当一个对象是另外一个对象的一部分时,组合通常是不错的选择。

例如,汽车是由发动机、传动装置、启动装置、车前灯、挡风玻璃以及其他部件组成的,发动机又是由活塞、曲柄轴和阀门等组合而成的。
汽车是发动机等多个元器件的抽象,而发动机是活塞等元器件的抽象,二者处于不同的层次而又有彼此交互的接口,组合是提供不同抽象层的好办法。
汽车对象可以提供司机所需要的接口,同时也能够获取内在组成部分,从而为机械师提供适合操作的深层抽象。当然,如果机械师需要更多信息来诊断问题或调整发动机,这些组成部分也可以进一步被细分。

总的来说,组合就是
让不同的类混合并且加入其他类中来增加功能和代码重用性
,这种适用于
由多个小类组成一个大类的情况
,并且不需要对小类进行太多修改。在前面示例中,我们实现了主机的串口收发和绘图功能,在实际应用中,我们往往需要将传感器数据存储到文件中,以便后续的查看和处理,很明显前面的传感器数据为一维的时间序列数据,
适合存储为表格类型(即列标题为索引和值),我们通常将该类数据保存为 csv 格式文件,csv 是一种字符串文件的格式,它组织数据的语法就是在字符串之间加分隔符(行与行之间是加换行符,同行字符之间是加逗号分隔)
,可以用任意的文本编辑器打开(如记事本),也可以用 Excel 打开,还可以通过 Excel 把文件另存为 csv 格式。
用 csv 格式存储数据,读写比较方便,易于实现,文件也会比 Excel 文件小。
但 csv 文件缺少 Excel 文件本身的很多功能,如不能嵌入图像和图表,不能生成公式等等。

操作 csv 文件我们需要借助 csv 模块,python 自带 csv 模块,不需要我们使用 pip 安装,我们可以点击如下链接查看 csv 模块使用方法:

https://docs.python.org/zh-cn/3.13/library/csv.html#csv.writer

image

这里,我们首先定义一个 FileIOClass 类,其中具有初始化方法、写入传感器数据到文件方法和关闭文件方法,示例代码如下:

import csv
_# 使用typing模块提供的复合注解功能_
from typing import List

class FileIOClass:
    def __init__(self,path:str="G:\\Python面向对象编程\\Demo\\file.csv"):
        '''
        初始化csv文件和列标题
        :param path: 文件路径和文件名
        '''
        self.path   = path
        _# path为输出路径和文件名,newline=''是为了不出现空行_
        self.csvFile = open(path, "w+", newline='')
        _# rowname为列名,index-索引,data-数据_
        self.rowname = ['index', 'data']
        _# 返回一个writer对象,将用户的数据在给定的文件型对象上转换为带分隔符的字符串_
        self.writer = csv.writer(self.csvFile)
        _# 写入csv文件的列标题_
        self.writer.writerow( self.rowname)

    def WriteFile(self,index:List[int],data:List[int])->None:
        '''
        :param index: 传感器索引列表
        :param data:  传感器数据列表
        :return:
        '''
        writedatalist = []
        for i in range(len(data)):
            writedatalist.append([index[i],data[i]])
            _# 将列表中的每个元素将被写入CSV文件的一列中_
            self.writer.writerow(writedatalist[i])

    def CloseFile(self)->None:
        '''
        关闭文件
        :return: None
        '''
        self.csvFile.close()

这里,在初始化方法中,我们需要传入文件保存路径。之后创建一个 writer 对象,将用户的数据在给定的文件型对象上转换为带分隔符的字符串,同时写入 csv 文件的列标题。在 WriteFile 方法中传入数据的索引列表用于表示数据的先后顺序,之后是数据列表(这里的类型注解需要使用 typing 模块提供的复合注解功能),并循环将每个元素将被写入 CSV 文件的一列中,最后定义了文件的关闭方法。

image

在主函数中,我们创建 FileIOClass 对象,写入模拟传感器数据后关闭文件,以下为示例代码和运行效果:

if __name__ == '__main__':
    path  = "G:\\Python面向对象编程\\Demo\\file.csv"
    data  = [11,42,307,46,55,61,78,80,19,11]
    index = [count for count in range(len(data))]

    file = FileIOClass(path)
    file.WriteFile(index,data)
    file.CloseFile()

image

这里,我们可以直接在 MasterClass 类的初始化中创建 FileIOClass 类的实例化对象来实现组合。代码如下:

_# 文件保存路径_
        self.savepath = "G:\\Python面向对象编程\\Demo\\file.csv"
        _# 创建FileIOClass类的实例化对象_
        self.fileio = FileIOClass(self.savepath)

通过 sourcetrail,我们可以清晰看到类之间的组合与继承关系:

image

image

在主程序中,我们在主机接收 10 次数据后,将数据保存到 file.csv 中:

if __name__ == "__main__":
_    # 创建数据列表_
    datalist = []
    m = MasterClass(state = MasterClass.IDLE_STATE,
                    port = "COM17",
                    wintitle = "Basic plotting examples",
                    plottitle = "Updating plot",
                    width = 1000,
                    height = 600)
    m.StartMaster()
    m.SendSensorCMD(MasterClass.SENDID_CMD)
    m.RecvSensorID()

    # 循环10次接收数据
    for i in range(10):
        m.SendSensorCMD(MasterClass.SENDVALUE_CMD)
        value = m.RecvSensorValue()
        datalist.append(value)
    indexlist = [count for count in range(len(datalist))]

    # 写入数据
    m.fileio.WriteFile(indexlist,datalist)
    m.fileio.CloseFile()

如下为运行效果:

image

目前,整个文件的完整代码如下,可以看到单单是这么一个简单程序就有了三百多行,对于代码查找修改来讲,非常不便。同时我们注意到,几个不同类之间似乎功能并不相同,不应该放到一个文件中。下一节我们将会说如何利用 Python 中的模块和包来组织我们的代码。

完整代码如下:

_# 串口相关库_
import serial
import serial.tools.list_ports
_# 队列相关_
import queue
import random
_# 日志输出相关库_
import logging
_# 曲线作图相关库_
import pyqtgraph as pg
import numpy as np
from pyqtgraph.Qt import QtCore
_# 文件读写相关库_
import csv
_# 使用typing模块提供的复合注解功能_
from typing import List
import time


_# # 设置日志输出级别_
_# logging.basicConfig(level=logging.DEBUG)_
_# 在配置下日志输出目标文件和日志格式_
LOG_FORMAT="%(asctime)s-%(levelname)s-%(message)s"
logging.basicConfig(filename='my.log',level=logging.DEBUG,format=LOG_FORMAT)

class SerialClass:
    _# 限定SerialClass对象只能绑定以下属性_
    __slots__ = ('dev','_SerialClass__devstate')
    _# 初始化_
    _# 使用默认参数_
    def __init__(self,
                 devport:str     = "COM17",
                 devbaudrate:int = 115200,
                 devbytesize:int = serial.EIGHTBITS,
                 devparity  :str = serial.PARITY_NONE,
                 devstopbits:int = serial.STOPBITS_ONE):
        _# 直接传入serial.Serial()类_
        self.dev             = serial.Serial()
        self.dev.port        = devport
        self.dev.baudrate    = devbaudrate
        self.dev.bytesize    = devbytesize
        self.dev.parity      = devparity
        self.dev.stopbits    = devstopbits
        _# 表示串口设备的状态-打开或者关闭_
        _# 初始化时为关闭_
        self.__devstate      = False

        print("SerialClass init")
        logging.info("SerialClass init")

    _# 取值方法_
    @property
    def devstate(self):
        return self.__devstate

    _# 打开串口_
    def OpenSerial(self):
        print("SerialClass-OpenSerial")
        logging.info("SerialClass-OpenSerial")
        self.dev.open()
        self.__devstate = True

    _# 关闭串口_
    def CloseSerial(self):
        print("SerialClass-CloseSerial")
        logging.info("SerialClass-CloseSerial")
        self.dev.close()
        self.__devstate = False

    _# 串口读取_
    def ReadSerial(self):
        print("SerialClass-ReadSerial")
        logging.info("SerialClass-ReadSerial")
        if self.__devstate:
            _# 阻塞方式读取_
            _# 按行读取_
            data = self.dev.readline()
            _# 收到为二进制数据_
            _# 用utf-8编码将二进制数据解码为unicode字符串_
            _# 字符串转为int类型_
            data = int(data.decode('utf-8', 'replace'))
            return data

    _# 串口写入_
    def WriteSerial(self,write_data):
        print("SerialClass-WriteSerial")
        logging.info("SerialClass-WriteSerial")
        if self.__devstate:
            _# 非阻塞方式写入_
            self.dev.write(write_data.encode())
            _# 输出换行符_
            _# write的输入参数必须是bytes 格式_
            _# 字符串数据需要encode()函数将其编码为二进制数据,然后才可以顺利发送_
            _# \r\n表示换行回车_
            self.dev.write('\r\n'.encode())

    def RetSerialState(self):
        if self.dev.isOpen():
            self.__devstate = True
            return True
        else:
            self.__devstate = False
            return False

class PlotClass:
    _# 绘图类初始化_
    def __init__(self,wintitle:str="Basic plotting examples",plottitle:str="Updating plot",width:int=1000,height:int=600):
        '''
        用于初始化Plot类
        :param wintitle:  窗口标题
        :param plottitle: 图层标题
        :param width:     窗口宽度
        :param height:    窗口高度
        '''
        _# Qt应用实例对象_
        self.app        = None
        _# 窗口对象_
        self.win        = None
        _# 设置窗口标题_
        self.title      = wintitle
        _# 设置窗口尺寸_
        self.width      = width
        self.height     = height
        _# 传感器数据_
        self.value      = 0
        _# 计数变量_
        self.__count    = 0
        _# 传感器数据缓存列表_
        self.valuelist  = []
        _# 绘图曲线_
        self.curve      = None
        _# 图层对象_
        self.plotob     = None
        _# 图层标题_
        self.plottitle  = plottitle
        _# 定时器对象_
        self.timer = QtCore.QTimer()
        _# 定时时间_
        self.time  = 0
        _# Qt应用和窗口初始化_
        self.appinit()

        print("PLOT INIT SUCCESS")
        logging.info("PLOT INIT SUCCESS")

    _# 应用程序初始化_
    def appinit(self):
        '''
        用于qt应用程序初始化,添加窗口、曲线和图层
        :return: None
        '''
        _# 创建一个Qt应用,并返回该应用的实例对象_
        self.app = pg.mkQApp("Plotting Example")
        _# 生成多面板图形_
        _# show:(bool) 如果为 True,则在创建小部件后立即显示小部件。_
        _# title:(str 或 None)如果指定,则为此小部件设置窗口标题。_
        self.win = pg.GraphicsLayoutWidget(show=True, title=self.title)
        _# 设置窗口尺寸_
        self.win.resize(self.width, self.height)
        _# 进行窗口全局设置,setConfigOptions一次性配置多项参数_
        _# antialias启用抗锯齿,useNumba对图像进行加速_
        pg.setConfigOptions(antialias=True, useNumba=True)

        _# 添加图层_
        self.plotob = self.win.addPlot(title=self.plottitle)
        _# 添加曲线_
        self.curve = self.plotob.plot(pen='y')

    _# 接收数据_
    def GetValue(self,value):
        '''
        用于接收传感器数据,加入缓存列表
        :param value: 传感器数据
        :return: None
        '''
        self.value = value
        _# 加入数据缓存列表_
        self.valuelist.append(value)
        print("PLOT RECV DATA : "+str(self.value))
        logging.info("PLOT RECV DATA : "+str(self.value))

    _# 更新曲线数据_
    def DataUpdate(self):
        '''
        用于定时进行曲线更新,这里模拟绘制正弦曲线
        :return: None
        '''
        _# 模拟绘制正弦曲线_
        _# 计数变量更新_
        self.__count = self.__count + 0.1
        self.value = np.sin(self.__count)
        self.GetValue(self.value)
        _# 将数据转化为图形_
        self.curve.setData(self.valuelist)

    _# 设置定时更新_
    def SetUpdate(self,time:int = 100):
        '''
        设置定时更新任务
        :param time: 定时的时间
        :return: None
        '''
        _# 定时器结束,触发DataUpdate方法_
        self.timer.timeout.connect(self.DataUpdate)
        _# 启动定时器_
        self.timer.start(time)
        _# 定时时间_
        self.time = time
        print("PLOT SET UPDATA")
        logging.info("PLOT SET UPDATA")
        _# 进入主事件循环并等待_
        pg.exec()

class FileIOClass:
    def __init__(self,path:str="G:\\Python面向对象编程\\Demo\\file.csv"):
        '''
        初始化csv文件和列标题
        :param path: 文件路径和文件名
        '''
        self.path   = path
        _# path为输出路径和文件名,newline=''是为了不出现空行_
        self.csvFile = open(path, "w+", newline='')
        _# rowname为列名,index-索引,data-数据_
        self.rowname = ['index', 'data']
        _# 返回一个writer对象,将用户的数据在给定的文件型对象上转换为带分隔符的字符串_
        self.writer = csv.writer(self.csvFile)
        _# 写入csv文件的列标题_
        self.writer.writerow(self.rowname)

    def WriteFile(self,index:List[int],data:List[int])->None:
        '''
        :param index: 传感器索引列表
        :param data:  传感器数据列表
        :return:
        '''
        writedatalist = []
        for i in range(len(data)):
            writedatalist.append([index[i],data[i]])
            _# 将列表中的每个元素将被写入CSV文件的一列中_
            self.writer.writerow(writedatalist[i])

    def CloseFile(self)->None:
        '''
        关闭文件
        :return: None
        '''
        self.csvFile.close()

class SensorClass(SerialClass):
    _# 类变量:_
    _#   RESPOND_MODE -响应模式-0_
    _#   LOOP_MODE    -循环模式-1_
    RESPOND_MODE,LOOP_MODE = (0,1)
    _# 类变量:_
    _#   START_CMD       - 开启命令      -0_
    _#   STOP_CMD        - 关闭命令      -1_
    _#   SENDID_CMD      - 发送ID命令    -2_
    _#   SENDVALUE_CMD   - 发送数据命令   -3_
    START_CMD,STOP_CMD,SENDID_CMD,SENDVALUE_CMD = (0,1,2,3)

    _# 类的初始化_
    def __init__(self,port:str = "COM11",id:int = 0,state:int = RESPOND_MODE):
        _# 调用父类的初始化方法,super() 函数将父类和子类连接_
        super().__init__(port)
        self.sensorvalue = 0
        self.sensorid    = id
        self.sensorstate = state
        print("Sensor Init")
        logging.info("Sensor Init")

    @staticmethod
    _# 判断传感器ID号是否正确:这里判断ID号是否在0到99之间_
    def IsTrueID(id:int = 0):
        if id >= 0 and id <= 99:
            print("Sensor ID True")
            return True
        else:
            print("Sensor ID False")
            return False

    _# 传感器上电初始化_
    def InitSensor(self):
        _# 传感器上电初始化工作_
        _# 同时输出ID号以及状态_
        print("Sensor %d Init complete : %d"%(self.sensorid,self.sensorstate))
        logging.info("Sensor %d Init complete : %d"%(self.sensorid,self.sensorstate))

    _# 开启传感器_
    def StartSensor(self):
        super().OpenSerial()
        print("Sensor %d start serial %s "%(self.sensorid,self.dev.port))
        logging.info("Sensor %d start serial %s "%(self.sensorid,self.dev.port))

    _# 停止传感器_
    def StopSensor(self):
        super().CloseSerial()
        print("Sensor %d close serial %s " % (self.sensorid, self.dev.port))
        logging.info("Sensor %d close serial %s " % (self.sensorid, self.dev.port))

    _# 发送传感器ID号_
    def SendSensorID(self):
        super().WriteSerial(str(self.sensorid))
        print("Sensor %d send id "%self.sensorid)
        logging.info("Sensor %d send id "%self.sensorid)

    _# 发送传感器数据_
    def SendSensorValue(self):
        _# 生成[1, 10]内的随机整数_
        data = random.randint(1, 10)
        super().WriteSerial(str(data))
        print("Sensor %d send data  %d" % (self.sensorid,data))
        logging.info("Sensor %d send data  %d" % (self.sensorid,data))

    _# 接收主机指令_
    def RecvMasterCMD(self):
        cmd = super().ReadSerial()
        print("Sensor %d recv cmd %d " % (self.sensorid,cmd))
        logging.info("Sensor %d recv cmd %d " % (self.sensorid,cmd))
        return cmd

class MasterClass(SerialClass,PlotClass):
    _# 类变量:_
    _#   BUSY_STATE  -忙碌状态-0_
    _#   IDLE_STATE  -空闲状态-1_
    BUSY_STATE, IDLE_STATE = (0, 1)
    _# 类变量:_
    _#   START_CMD       - 开启命令      -0_
    _#   STOP_CMD        - 关闭命令      -1_
    _#   SENDID_CMD      - 发送ID命令    -2_
    _#   SENDVALUE_CMD   - 发送数据命令   -3_
    START_CMD, STOP_CMD, SENDID_CMD, SENDVALUE_CMD = (0, 1, 2, 3)

    _# 类的初始化_
    def __init__(self,state:int = IDLE_STATE,port:str = "COM17",wintitle:str="Basic plotting examples",plottitle:str="Updating plot",width:int=1000,height:int=600):
        _# 分别调用不同父类的__init__方法_
        SerialClass.__init__(self,port)
        PlotClass.__init__(self,wintitle,plottitle,width,height)
        self.valuequeue   = queue.Queue(10)
        self.__masterstatue = state
        _# 初始化完成的标志量_
        self.INIT_FLAG = False
        _# 文件保存路径_
        self.savepath = "G:\\Python面向对象编程\\Demo\\file.csv"
        _# 创建FileIOClass类的实例化对象_
        self.fileio = FileIOClass(self.savepath)
        print("MASTER INIT SUCCESSS")
        logging.info("MASTER INIT SUCCESSS")

    @classmethod
    def MasterInfo(cls):
        print("Info : "+str(cls))

    _# 开启主机_
    def StartMaster(self):
        super().OpenSerial()
        print("START MASTER :"+self.dev.port)
        logging.info("START MASTER :"+self.dev.port)

    _# 停止主机_
    def StopMaster(self):
        super().CloseSerial()
        print("CLOSE MASTER :" + self.dev.port)
        logging.info("CLOSE MASTER :" + self.dev.port)

    _# 接收传感器ID号_
    def RecvSensorID(self):
        sensorid = super().ReadSerial()
        print("MASTER RECIEVE ID : " + str(sensorid))
        logging.info("MASTER RECIEVE ID : " + str(sensorid))
        return sensorid

    _# 接收传感器数据_
    def RecvSensorValue(self):
        data = super().ReadSerial()
        print("MASTER RECIEVE DATA : " + str(data))
        logging.info("MASTER RECIEVE DATA : " + str(data))
        self.valuequeue.put(data)
        return data

    _# 主机发送命令_
    def SendSensorCMD(self,cmd):
        super().WriteSerial(str(cmd))
        print("MASTER SEND CMD : " + str(cmd))
        logging.info("MASTER SEND CMD : " + str(cmd))

    _# 主机返回工作状态-_
    def RetMasterStatue(self):
        return self.__masterstatue

    _# 重写父类的DataUpdate方法_
    def DataUpdate(self):
        self.SendSensorCMD(self.SENDVALUE_CMD)
        self.value = self.RecvSensorValue()
        self.WriteSerial("Recv:"+str(self.value))
        self.GetValue(self.value)
        self.curve.setData(self.valuelist)
        print("PLOT UPDATA : " + str(self.value))
        logging.info("PLOT UPDATA : " + str(self.value))

class DevClass(SerialClass):
    def __init__(self,port:str = "COM1"):
        super().__init__(port)

    _# 开启设备_
    def StartDev(self):
        super().OpenSerial()
        print("START Dev :" + self.dev.port)

    def ReadSerial(self,byte_size):
        if super().RetSerialState():
            data = self.dev.read(byte_size)
            data = int(data.decode('utf-8', 'replace'))
            return data

_# 判断串口类对象的串口是否开启_
def IsSerialConnected(serialclass):
    return serialclass.RetSerialState()

if __name__ == "__main__":
    _# 创建数据列表_
    datalist = []
    m = MasterClass(state = MasterClass.IDLE_STATE,
                    port = "COM17",
                    wintitle = "Basic plotting examples",
                    plottitle = "Updating plot",
                    width = 1000,
                    height = 600)
    m.StartMaster()
    m.SendSensorCMD(MasterClass.SENDID_CMD)
    m.RecvSensorID()

    _# 循环10次接收数据_
    for i in range(10):
        m.SendSensorCMD(MasterClass.SENDVALUE_CMD)
        value = m.RecvSensorValue()
        datalist.append(value)
    indexlist = [count for count in range(len(datalist))]

    _# 写入数据_
    m.fileio.WriteFile(indexlist,datalist)
    m.fileio.CloseFile()

image

任何 Web、桌面或移动开发人员都经常使用图像。你可以从 C#、HTML、XAML、CSS、C++、VB、TypeScript 甚至代码注释中引用它们。有些图像是本地的,有些存在于线上或网络共享中,而其他图像可能仅以 base64 编码字符串的形式存在。我们在代码中以多种方式引用它们,但总是作为字符串值,不能显示图像的样子。直到现在。

当光标悬停时,预览图像的功能请求最近得到了很多关注,所以我们决定研究一下。不同的编辑器已经存在各种各样的图像预览器,但它们的工作方式都略有不同。如果我们要构建它,我们必须以一种对 Visual Studio 来说原生和自然的方式来做。因此,我们发起了一个社区实验来解决这个问题。

结果是代码编辑器中的编辑器 tooltip。该 tooltip 以原始大小显示图像,但宽度和高度上限为500像素。在预览图的下面,你会看到像素大小和字节大小。

实验参与者对这一功能非常兴奋。以下是他们中的一些人的留言:

我们确定了一些需要回答的重要问题:

- 必须支持哪些图像文件类型?

- 哪些图像引用语法和格式最常用?

- 哪些附加功能可能很重要?

这个实验帮助我们回答了这些问题。我们来复习一下。

支持的文件类型

实验参与者最常用的图像文件有(按顺序排列):

- PNG – 53%

- JPG – 21%

- SVG – 15%

- ICO – 4%

- WebP – 4%

- GIF – 2%

- 其他文件类型,如 BMP, TIFF, DDS

WPF 唯一不直接支持的格式是 SVG,因此我们必须将其转换为 WPF 可以呈现的位图格式。这就是为什么 SVG 支持没有进入第一个版本,但我们希望能尽快准备好。

引用语法

有多种方法可以引用图像,主要取决于语言和应用程序模型。我们从实验中看到的最常见的是:

- 相对 URL (./, ../, /)

- 文件路径 (c:\, c:/, \, /)

- 数据 URI (data:image/png;base64,…)

- 包 URI (pack://application:,,,/Images/MyImage.png)

- 图像昵称 (KnownMonikers.StatusWarning)

附加功能

除了简单地显示图像预览外,社区还提出了两个功能。

缩放是一个很酷的功能,在未来的更新中可以进一步研究。然而,目前还不在讨论范围之内。

另一个想法是,用户应该能够单击预览图像,使其在默认的图像查看器应用程序中打开。这个功能很简单,所以我们就增加了。

这就是图像悬停预览功能如何进入 Visual Studio 的故事。要自己尝试,请安装最新版本的 Visual Studio 2022 (v17.10或更新版本)并尝试一下。

原文链接:https://devblogs.microsoft.com/visualstudio/making-visual-studio-a-bit-more-visual/