0%

请注意:在本文中包含了一些非常危险的行为,在使用前请务必三思

事情起因

因为之前我做过一个处理误差的库的尝试,虽然构造了一个可以自动计算常见误差形式的类,然而这样一个类用在科学计算上的价值却不大,原因就在于这么做没法使用python中的常用数学计算库numpy,那么我就开始动起了歪脑筋:是不是可以通过修改np.ndarray一些函数支持来实现我所想要的功能

就以__add__为例,我可以创建一个新函数去替代__add__以实现我的功能.

于是尝试了一下np.ndarray.__add__=myadd,很不幸,解释器告诉我

1
TypeError: can't set attributes of built-in/extension type 'numpy.ndarray'

我考虑了一下,发现原因是numpy不是由python编写的,因此也无法直接使用python修改动态类型的手段.

解决:forbiddenfruit

正在我表示绝望准备放弃的时候(原本准备用类继承实现功能),突然看到了一篇文章

在这篇文章,作者介绍了一个神奇又危险的库forbiddenfruit(下载地址),在这个库里,你可以替换python内置的类型

在文章中也介绍了修改内置类的工作原理,但是仍然无法解决运算符重载的问题

__add__可以正常被替换,但是+就不行了

那么下面我可以来演示一下如何替换掉python中list的加法方法

1
2
3
4
5
6
7
from forbiddenfruit import curse
def change_list_add():
old_add=list.__add__#保存原来加法实现
def new_add(self,o):
return old_add(old_add(self,[" "]),o)#新的加法
curse(list,"__add__",new_add)#替换掉原有加法
change_list_add()

然后未来进行列表加法时大家就会惊奇地发现列表两个元素之间总是会有一个空格

在实际的生产中,这样的做法可能会导致暴力事件的发生

大家也可以试试在python大作业中用这个东西(吃C别怪我)

由于在开发3D位移平台时,需要使用到MPS010602,但是因为MPS010602是一个独立的项目(目前还在开发),因此正好学习一下如何打包为wheel库,以及其中遇到的一些问题

源代码整理与__init__.py

首先,我们要把源代码放置在一个文件夹中,并且需要有一个__init__.py

__init__.py应当如何编写,这篇文章写得很清楚,可以去参考一下

目前我就采用最为简易的方式

如果目录结构是这样

1
2
3
4
5
---MPS010602(import时所用的名称)
|-__init__.py
|-DriverOperator.py
|-MPSDataType.py

那么在__init.py__中,为了方便(不需要引用子文件名),可以这么写

1
2
from MPS010602.MPSDataType import *
from MPS010602.DriverOperator import *

需要注意在两个源文件中如果出现了交叉引用,需要修改import格式

setup.py

在放置代码的文件夹外,新建一个setup.py,然后填写以下内容

1
2
3
4
5
6
7
8
9
10
import setuptools
setuptools.setup(
name='mps010602',
version='0.0.0',
description='This is a program for MPS-010602',
author='songyuli',
author_email='',
packages=setuptools.find_packages(),
package_data={'':['*.dll'],}
)

其他部分都很简单,除了两个需要注意的地方:

  1. 包名称和import时使用的名称不同
  2. package_data包含了你希望包含的除了源代码以外的内容(例如说使用的动态链接库)

构建与安装

在这一文件夹下,输入python setup.py bdist_wheel,之后在dist文件夹内可以看见编译好的whl文件,然后文件用pip install可以直接安装

为了未来后续开发方便,建议一个环境开发,一个环境测试

其他的一些问题

我之前在引用动态链接库时遇到问题(因为工作目录发生改变)

因此获取自己当前的位置,可以通过os.path.dirname(os.path.abspath(__file__))访问当前所在文件夹

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment

之前在制作控制程序的时候,有一些不够优雅的地方,今天决定对这些问题进行修正

事件驱动

之前的工作是通过不断地询问实现的,我们要实现与串口交互的部分不断询问,而上层则是通过事件去驱动,这样可以极大程度简便工作流程,并且为未来处理碰撞等问题提供解决方案.

对于电机

电机输入一个MoveControlCallbacksBase,其中规定了三种可能的情况:状态更新,碰撞和坐标更新

这些函数只有在值改变的时候才会调用

这样的话MoveControl类只留下了一个接受命令并阻塞运行的run方法(在阻塞运行时,坐标会更新),并且保留了两个公开可修改属性

  1. refreshtime:控制多久刷新一次坐标信息
  2. feed_rate:控制移动速度

对于控制器

计划将控制器的构造函数的参数不出现有关移动控制和测量控制的函数,这样可以保证更好的封装

然后和电机保持一样的回调控制方式

对于控制器,则保留四种情况的回调:

  1. status_refresh_call控制器状态刷新
  2. coordinate_refresh_call坐标状态刷新
  3. measure_refresh_call测量结果刷新
  4. no_command_call无后续命令

并且保留了两个公开方法add_cmdchange_motor_settings

属性中保留cmd_list进行读取待进行指令,cmd_now为当前执行指令

以及move_start控制是否开始测量

1.31更新

如果单纯的使用一个cmd来同时管理电机,测量和显示,这无疑是非常浪费的,而且这样操作如果未来需要修改命令格式以支持其他功能也是非常不妥当的,因此,我们需要对cmd进行预处理操作.

我们添加了一个_cmd_parser指令,这个指令可以独立地去处理数据,目前可以处理两类:

  1. 一次移动,一次测量
  2. 多次移动一起,一次测量

为了更好的适配2,我们在电机控制中添加了_send_cmd_sync这个命令,可以更高效处理曲折的路径

同时,我们在回调函数中添加了cmd_refresh_call,避免用户去访问cmd_now属性造成混乱

测量驱动修改

应用了MPS010602,将相关文件打包成一个weel再安装,这个留到另外一篇文章讲

VSCode作为一个常用的编辑器(有时可以作为集成开发环境),其拥有许多重要的”特殊功能”,在这里进行笔记,以便未来查阅

快捷键

正则表达式搜索替换

正则表达式速查表

转义字符 匹配内容
\t tab
\r 回车符号\r
\n 换行符号\n
\ 特殊符号转义,如”“ ,转义后匹配的是字符”“, “(” 匹配的是括号”(“
[字符序列] 匹配[ ]中的任意字符,如[ae],字符a和字符e均匹配
[^字符序列] 匹配不在[ ]中的任意字符,如[^ae]除了a和e,其他字符都匹配
[字符1-字符2] 匹配在[ ]之间的任意字符,如[a-x],就是匹配a和x之间的所有字符(包括a和x)
. 匹配任意单个字符(除了\n)
\w 匹配所有单词字符(如”a”,“3”,“E”,但不匹配”?”,”.”等)
\W 和\w相反,匹配所有非单词字符
\s 匹配空格
\S 和\s相反,匹配非空格
\d 匹配数字字符,如”1”,“4”,”9”等
\D 和\d相反,匹配除了数字字符外的其他字符
* 将前面的元素匹配0到多次,如”\d*.\d”,可以匹配”19.9”,”.0”,“129.9”
+ 将前面的元素匹配1到多次,如”be+”,可以匹配”be”, “beeeeee”
将前面的元素匹配0次或者一次,如”rai?n” 可以且只可以匹配 “ran” 或者 “rain”
{n} n是个数字,将前面的元素匹配n次,如”be{3}“可以且只可以匹配 ”beee”
{n, m} 将前面的元素匹配至少n次,最多m次,如”be{1,3}” 可以且只可以匹配”be”,“bee”, “beee”
| 相当于”或”,表示匹配由

而正则表达式中的”子模式”(),在替换时可以使用$1来相应的替换.

例如对于anaconda导出的environment.yml文件,其一部分格式如下:

1
- packagename=0.0.0=aaaaa

由于在跨操作系统迁移时要将后面那个详细版本给去掉因此可以使用替换功能

查找(.=.)=.

替换为$1

VSC分屏显示

由于在写代码时希望可以做到两个显示屏共同显示project的代码,因此需要用分屏显示

首先ctrl+shift+P,然后输入Workspace: Duplicate As Workspace in New Window

像环境配置这样的问题虽然如果想非常快弄完,也不难(你甚至可以去Mircosoft Store里面下载一个Python环境),然而,这么做*后患无穷,尤其是等到要安装一些复杂的包的时候.因此,在这篇文章中,我将介绍conda的使用和Python中最常见的两个环境IPythonJupyter Notebook的相关配置,以及使用VScode编辑器.

conda的使用

为什么要用conda

Package, dependency and environment management for any language—Python, R, Ruby, Lua, Scala, Java, JavaScript, C/ C++, FORTRAN

conda作为一个包管理器,可以保证你的电脑上同时存在多个不同的Python版本,并且每一个版本之间都相互独立,互不干扰(为何同时要有多个版本,因为许多包不是向下兼容的.简单的说,就是对于一个依赖,安装其最新的版本并不总是一个好的选择)

这是其优点的一方面,而另外一个方面则更为重要,在conda中,你可以打包,发布你的程序,这样可以非常方便的实现代码的共享(这可比直接拷贝源文件来得方便)

而我们这次使用的是conda的一个发行版(?有可能是别的名称)anaconda,并且以我个人电脑为例,讨论其安装与使用.

anaconda的安装

首先在官网上下载anaconda(这个时候要养成在官网下载的习惯,不要下一堆杂七杂八的东西)

然后安装anaconda,首先它会问你是给本用户安装还是给所有用户安装,我个人建议是前者(但实际上就我所知,99%家里的Windows电脑只有一个用户)

之后就会有这样一个提示框

image-20230117094427619

这里有两个选项,我刚好要和推荐配置唱一波反调:

  1. 第一个选项有关于环境变量,虽然上面并不建议添加到环境变量,但是如果不这么做,很多时候会非常麻烦
  2. 第二个选项是覆盖原有Python成为默认环境,我个人倾向于如果已经有Python环境了,就不要覆盖,免得影响现有依赖

然后就是点击Install等待其安装.

anaconda的使用

安装完anaconda之后,你可以打开你电脑上的Anaconda Powershell Prompt,然后可以看见一个命令行界面

image-20230117095543879

不过与Powershell不同的是,在anaconda里面有一个(base),这意味着你目前处于一个基础环境.

然后如果你在其中输入IPython,那么就会有IPython的界面跳出来,表明其已经被安装了.

Jupyter环境则可以通过输入jupyter notebook尝试,你们会得到这样的界面:

image-20230117100215561

表明Jupyter内核已经启动,然后你需要使用浏览器去连接这个内核

怎么连接希望大家自己想想,毕竟大家都懂英文

但是实际上,我更加推荐使用VSCode作为Jupyter的编辑器,不过jupyter命令有别的功能,比如转换笔记本,大家可以去查找一下.

创建新环境

好,现在假设我需要创造一个环境用来给我做机器学习,那么我们可以用

conda create -n "machine-learning" python=3.8来创建,这里我配置了一个名字叫”machine-learning”的环境,使用的python版本为3.8

安装好之后,在anaconda里面输入conda info -e(展示所有的环境),就可以看到这样的结果

image-20230117101138010

这表明新的环境已经被成功安装,然后输一下ipython试一试

image-20230117134918357

淦,出错了,还是原来的python环境没变

经过分析,我发现这个是我电脑原本安装的python,由于我的conda环境里面并没有ipython,因此在寻找的时候按照先后顺序就找到了自己电脑的ipython

因此,在使用conda的时候,务必注意环境里面你打开的究竟是哪个内核.

安装包

比如说,我现在要安装sklearn库,那么我就在网上搜索sklearn anaconda,然后就搜到了安装指令conda install -c anaconda scikit-learn网址

把这输进去(注意检查当前的环境),然后安装即可

其会自动安装依赖,比如:

image-20230117141643636

转移环境

假如你千辛万苦配好一个环境并且成功运行了一套程序,然后你需要移到别的电脑上运行(这台电脑可能是别的系统)

你可以采用conda env export > environment.yml来创建一个配置文件,然后把这个文件复制到另外一台电脑(可能是集群),然后conda env create -f environment.yml,就可以在不同的操作系统平台上复现环境

这种情况并不少见,你可能希望在自己电脑上(Windows或Mac)编程,然后到集群上(Linux)运行

可以参考一下这个网页

需要注意删除一些不必要的库以提升可移植性

源配置

由于一些众所周知的原因,大家需要使用国内镜像站,可以给一个网址

VSCode的安装与使用

VSCode作为一个常用编辑器,其安装并不复杂,这个大家自己网上搜就可以了.

VSCode的常用插件

  1. Better Comments
  2. Jupyter(注意,在使用前,需要安装好Jupyter内核)
  3. Python(为python程序设计提供IDE环境)

最后想要提醒的

在每次打开程序的时候,务必要注意打开的python版本

比如:

image-20230117140557615

image-20230117140625609

image-20230117141531520

事情起因

今天我刷到了一个视频了解到EasyConnect似乎有滥用权限,泄露用户数据的可能,因此希望找到一种方法,在正常访问内网资源的同时实现对EasyConnect的控制.

解决手段

我采用的是视频中提供的方案,是docker-easyconnect,[此为源代码地址][https://github.com/Hagb/docker-easyconnect]

在操作的时候,需要使用Docker环境进行隔离,刚好也对Docker环境进行学习

需要注意的,在使用socks5代理的时候,把端口从1080改为其他类型的端口

结果

算是实现了部分的功能,在Docker上顺利登陆了,并且使用Firefox上的SwichyOmega实现对于流量的分流(基于socks5原理)(不过目前只代理了`.fudan.edu.cn网页),这么做可以保障我在使用内网资源的时候同时可以访问外网资料

不过,这样的连接存在较为严重的稳定性问题,问题比较像一个issue,如何解决目前不明.

同时,非常遗憾的,使用这种手段无法解决我使用ssh的问题,而网上的资料似乎主要针对Linux系统,因此还是无法摆脱EasyConnect的困扰.

为了实现通过用户给的3D模型实现避障功能的路径规划,我们采取首先对模型进行切片,然后再通过修改路径长度和两点之间路径获取的函数完成

对于meshcut库的使用

可以实现3D建模处理的库有很多,但如果仅仅是想要一个切面就将vtk搬出来未免就有一点大材小用了,通过寻找,我找到了一个meshcut库,其可以将stl文件和定义的平面求得交线.

meshcut库的开发主页

下面是使用meshcut库实现对一个3D模型切割的效果图

42

而其所使用的源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import stl
import matplotlib.pyplot as plt
import numpy as np
import meshcut

def create_mesh(m:stl.mesh.Mesh):#从stl文件中创建mesh
# Flatten our vert array to Nx3 and generate corresponding faces array
verts = m.vectors.reshape(-1, 3)
faces = np.arange(len(verts)).reshape(-1, 3)

verts, faces = meshcut.merge_close_vertices(verts, faces)
return meshcut.TriangleMesh(verts,faces)

if __name__=="__main__":
m=stl.mesh.Mesh.from_file("up.stl")
mesh=create_mesh(m)

i=1
z=1#z坐标
while(i!=0):#在没有交点是会报错,程序自动终止
plane_origin=np.array([0,0,z])
plane_n=np.array([0,0,1])
plane=meshcut.Plane(plane_origin,plane_n)#创建平面

p=meshcut.cross_section_mesh(mesh,plane)#求解相交部分多边形
x=p[0][:,0]
x=np.hstack([x,np.array(x[0])])
y=p[0][:,1]
y=np.hstack([y,np.array(y[0])])
plt.plot(x,y)
plt.savefig("{}.png".format(z))
z=z+1

判断一条线是否与多边形相交

算法

接下来我们来讨论一下关于碰撞检测这一部分的内容.

多边形是由有限个点首尾相连而成的,那么假如一条线段与多边形相交,那么其必然与多边形的至少一条边相交.因此问题就转化为判断一条线段是否与一个线段集中线段相交.

而对于判断两线段是否相交的方法,记第一条线段两点为$A_1,A_2$,第二条线段两点为$B_1,B_2$,如果从线段一指向线段二某点(通常使用某个端点)的两个矢量与线段二的叉乘异号,表明$A_1,A_2$分别位于第二条线段两侧.同理,可以判断$B_1,B_2$是否位于第一条线段两侧.如果两个条件同时满足,那么就可以说明线段相交.

这一手段参考资料

特殊情况,判断点是否在多边形内,可以用相似的方法,通过过该点做一条射线,然后统计相交点数,如果相交点数为偶数,则点在多边形内,否则在多边形外

实现

在实现这个问题的同时,需要考虑到Python在处理大量的for循环时效率非常低下,因此需要对操作进行向量化处理.

假定$A_x,A_y,B_x,B_y$为$n,n,m,m$维列向量,定义$C_{ij}$的值等于$A_{i}\times B_{j}$为行列式的值,根据线性代数相关知识,我们可以得到$C=A_xB^T_y-A_yB_x^T$

如此操作时需要注意对array进行reshape

下面展示了实现矢量集叉乘和判断线段集是否相交的代码

1
2
3
4
5
6
7
def calc_cross(a: np.array, b: np.array):
# 计算两个二维向量集的叉乘
ax = a[:, 0].reshape(-1, 1)
ay = a[:, 1].reshape(-1, 1)
bx = b[:, 0].reshape(1, -1)
by = b[:, 1].reshape(1, -1)
return np.dot(ax, by)-np.dot(ay, bx)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def check_crossing(lines1: np.array, lines2: np.array):
begs1 = lines1[:, 0, :]
begs2 = lines2[:, 0, :]
ends1 = lines1[:, 1, :]
ends2 = lines2[:, 1, :]
l1 = ends1-begs1
l2 = ends2-begs2

# 检查线2是否分隔线1
v1 = begs2-begs1
v2 = begs2-ends1
c1 = calc_cross(v1, l2)
c2 = calc_cross(v2, l2)
c = c1*c2 # 计算两矩阵各个元素相乘(比较同号异号)
bca = c < 0 # 分割线1

# 检查线1是否分隔线2
v1 = begs1-begs2
v2 = begs1-ends2
c1 = calc_cross(v1, l1)
c2 = calc_cross(v2, l1)
c = c1*c2 # 计算两矩阵各个元素相乘(比较同号异号)
bcb = c < 0 # 分割线2

bc = np.all([bca, bcb], axis=0) # 同时分割
return bc # 第i行第j列元素判断lines1的第i根线与lines2的第j根线是否相交

这个时候我意识到了一个问题:在实际的移动过程中,z轴的移动无法控制,因此需要在路径规划的过程中对z轴进行单独处理

因此:在每一次移动时,首先移动z轴,然后就可以理解为平面内运动

完成路径规划的RoutePlanner类,实现从点集(datapoints)到路径命令(routecmd)的转换

使用算法

使用的算法参考自参考资料,我们的问题是一个旅行商问题,因此没有一个多项式时间内可以实现的最优算法

但是我们需要注意到,我们所面临的图是一个完全图,并且近似满足三角不等式.因此可以使用最小生成树的方法得到近似解,其结果不会大于最优解的两倍

实现

由于未来需要保留避障措施,但是避障之后的路径可以用一个新的路径代替,因此是通过修改路径获取函数实现(为未来预留接口)

这样做可能导致三角不等式不成立,但是影响应该不大

datapoints属性在修改时会重新计算route,因此不要频繁修改点集

程序功能

预想中的程序应该实现以下几个功能:

  1. 对路径的规划:获得带测量点,避免和已有待测物体发生撞车,智能更新测量点(类似于绘制函数的时候梯度变化大的点多,梯度变化小的点少)
  2. 移动命令的执行:与grbl进行通讯实现移动(通过传输GCode的方式),实时获知位置,获知出现撞车或者达到极限等状况,自定义原点
  3. 数据的获取与处理:原始数据获取(厂家有提供库),数据不确定度估算与定标(需要结合技术手册以及探头设计进一步研究)
  4. 中央控制程序:将电机等的状态显示出来,3D显示路径以及测量结果

为何不使用如kliment这样的程序

因为kliment是设计给3D打印机而非我们所使用的测量装置的

其优点有:

  1. 具有较为完善的各种错误的处理功能(但是grbl不支持)
  2. 界面比较漂亮
  3. 支持stl文件的导入等

但是我目前已经实现了gcode的传输以及实时的位置获取,klimenr可以为未来改进自定义原点或检查异常情况提供灵感,但是没有必要依赖其实现

主要原因在于klimenr缺乏开发文档,研究其源码会非常困难,并且其完全依赖于下位机的gcode功能(例如温度获取),但是对于我们现在的目标这么做并不切合实际.那么这样会导致可能存在解析gcode的”中间商”,将问题弄得过于复杂.

我的观点在于目前几个部分的模块分别开发(目前已经实现了一个可以控制grbl移动以及控制AD卡读取数据的一个控制程序),剩余的部分自行开发(当然像处理3D模型这样的问题究竟是使用像VTK这样的工具还是像使用slicer这样的小型工具还有待研究)

而最后的图形界面(虽然现在有一个基于CUI的”伪”图形界面),这个的制作属于比较细枝末节的问题(甚至最后可以扔给Labview解决),我们现在就不考虑了