分享

TelloPy-develop-0.7.0源码阅读.1

 云深无际 2021-11-03

    最近我在反思,为什么我看了那么多书,为什么还是写不出大型的程序?我也很苦恼,我想了下。应该还是看的源码少的过,古人曾经说过熟读唐诗三百首不会吟诗也会吟 。在读源码的选择上,我没有选择太复杂的开源库,而是选择了自己比较熟悉的TT无人机库,而且代码行数选择在1k行以内的库。就是下面这个库了~


首先我们在文章开头先简要的说明一下,什么是工厂函数

from setuptools import setup, find_packagesfrom codecs import openfrom os import path
here = path.abspath(path.dirname(__file__))
with open(path.join(here, 'README.md'), encoding='utf-8') as f: long_description = f.read()

在setup中,出现了这个,下面开始阅读

https://docs.python.org/zh-cn/3/library/codecs.html?highlight=codecs

这些函数最好去看官网的doc

常用的有获得当前文件的绝对路径

讲一个path加到环境变量中

获得目录名称

__file__这个变量不知道是什么意思

开了调试看了一下

'C:\\Users\\yunswj\\AppData\\Local\\Programs\\Python\\Python37\\lib\\ntpath.py'

接着向下跳,又看到了路径

延时函数

开始读主文件了

看到了这种导入的方式

搜索了一下,大概是同级导包的意思

然后试了一下指向包的地址,也成功了、确实是在同级的目录下跳转

继续看,这个utils经常可以看见

搜索了一下,就是小工具的意思

接着读主干,出现了一个日志类

import datetimeimport threading
LOG_ERROR = 0LOG_WARN = 1LOG_INFO = 2LOG_DEBUG = 3LOG_ALL = 99

找到源码,可以看到就使用了线程和时间库

下面是定义了日志的等级

Logger库的结构

class Logger(object): # Object 继承 for child 'RospyLogger' (Jordy) def __init__(self, header=''): self.log_level = LOG_INFO self.header_string = header self.lock = threading.Lock()
def header(self): now = datetime.datetime.now() ts = ("%02d:%02d:%02d.%03d" % (now.hour, now.minute, now.second, now.microsecond/1000)) return "%s: %s" % (self.header_string, ts)
def set_level(self, level): self.log_level = level
def output(self, msg): self.lock.acquire() print(msg) self.lock.release()
def error(self, str): if self.log_level < LOG_ERROR: return self.output("%s: Error: %s" % (self.header(), str))
def warn(self, str): if self.log_level < LOG_WARN: return self.output("%s: Warn: %s" % (self.header(), str))
def info(self, str): if self.log_level < LOG_INFO: return self.output("%s: Info: %s" % (self.header(), str))
def debug(self, str): if self.log_level < LOG_DEBUG: return self.output("%s: Debug: %s" % (self.header(), str))

先看初始化,日志等级

和日志头,以及一个线程锁

是一个类

这个头是一个个时间头,也好理解

我们平时的日志也是时间打头

return "%s: %s" % (self.header_string, ts)

这样的话就打印出一个自己喜欢的以一个输入的字符串开始的log,当然这里什么也没有写,就是一个时间头了

然后一个输出的函数必不可少

获得一下锁,打印,然后,释放

下面的函数都一样,就看一个,先判断一下

然后调用上面的输出函数

打印一个log,header+msg

用__main__修饰一下,模块也可以当,单独也能打

import datetimeimport threading
LOG_ERROR = 0LOG_WARN = 1LOG_INFO = 2LOG_DEBUG = 3LOG_ALL = 99

class Logger(object): # Object 继承 for child 'RospyLogger' (Jordy) def __init__(self, header=''): self.log_level = LOG_INFO self.header_string = header self.lock = threading.Lock()
def header(self): now = datetime.datetime.now() ts = ("%02d:%02d:%02d.%03d" x%(now.hour, now.minute, now.second, now.microsecond/1000)) return "%s: %s" % (self.header_string, ts)
def set_level(self, level): self.log_level = level
def output(self, msg): self.lock.acquire() print(msg) self.lock.release()
def error(self, str): if self.log_level < LOG_ERROR: return self.output("%s: Error: %s" % (self.header(), str))
def warn(self, str): if self.log_level < LOG_WARN: return self.output("%s: Warn: %s" % (self.header(), str))
def info(self, str): if self.log_level < LOG_INFO: return self.output("%s: Info: %s" % (self.header(), str))
def debug(self, str): if self.log_level < LOG_DEBUG: return self.output("%s: Debug: %s" % (self.header(), str))

if __name__ == '__main__': log = Logger('test') log.error('This is an error message') log.warn('This is a warning message') log.info('This is an info message') log.debug('This should ** NOT ** be displayed') log.set_level(LOG_ALL) log.debug('This is a debug message')

最后附上完整的日志代码,这段代码的移植性极好。因为依赖的库全是标准库,而且还完成了基础的日志分级功能,日后可以慢慢重构

那么我们知道了,调用之前传一个str进去当header

我们继续看,所有的地方都是用了event类的函数

就这么点,你能想到什么?

对!他是自己基于已有的数据模型,自己又做了一个数据模型

下面是两个必须要加的函数

一个给人看,友好的格式

一个机器用,丰富的info

所以你看出来了什么?到底是在干嘛?我觉得是python没有宏定义

这个类是用类本身的特性完成了宏定义的功能

你看这些大写的str

class Event: def __init__(self, name='annoymous'): # 恼人的,烦人的 self.name = name
def __repr__(self): return self.__str__()
def __str__(self): return '%s::%s' % (self.__class__.__name__, self.name)
def getname(self): return self.name

if __name__ == '__main__': ev = Event() print(ev) ev = Event('test event') print(ev)

这段实现宏的代码也是移植性极好的

同理

分别是tello ip+port 

调试开关

UDP发包大小

连接状态

线程锁状态

视频预览时间

视频大小

视频解码速率

日志长度等,蛮丰富的

视频变焦,还有就是要不要解锁快速飞行状态

把类型安排了

网络编程基操了

设置为阻塞模式,可不是那得堵住呗

上面得类得名字是调度

下面是两个线程

接收+视频流


连接

断开

发送,这里先等一下

这段调度得代码我没有看太懂

这个是状态机的函数,我们看看

放了四个参数

获得以一个锁

事件连接假

断开连接假

def byte(c): if isinstance(c, str): return ord(c) return c

返回是一个Unicode码

def le16(val): return (val & 0xff), ((val >> 8) & 0xff)

def int16(val0, val1): if (val1 & 0xff) is not 0: return ((val0 & 0xff) | ((val1 & 0xff) << 8)) - 0x10000 else: return (val0 & 0xff) | ((val1 & 0xff) << 8)
def byte_to_hexstring(buf): if isinstance(buf, str): return ''.join(["%02x " % ord(x) for x in buf]).strip()
return ''.join(["%02x " % ord(chr(x)) for x in buf]).strip()

def float_to_hex(f): return hex(struct.unpack('<I', struct.pack('<f', f))[0])
def show_exception(ex): exc_type, exc_value, exc_traceback = sys.exc_info() traceback.print_exception(exc_type, exc_value, exc_traceback

给出异常信息

然后打印错误的堆栈信息

以上的函数都是一些数据类型转换函数,在协议的处理的时候,频繁的用到所以这里提起封装。

https://tellopilots.com/wiki/protocol/#PacketTypeInfo

这个库牛逼的地方在于,不是简单的封装SDK那么简单,而是逆向出了低级的协议:

这个是逆向出来的低级的UDP结构包,大部分也是这个样子的

如果英文较好,可以看这个

数据包大小采用奇怪的小端格式,其中低位(第一个)字节是正常的,但高位(第二个)字节向左移动 3 位。所以解码大小看起来像这样:size = buffer[1] + ((buffer[2]<<8)>>3)

信息包类型信息格式

数据包的类型

Tello 消息 ID 和含义

https://bitbucket.org/PingguSoft/pytello/src/master/

以上包的获得都是通过这个wireshark抓到的

START_OF_PACKET = 0xccSSID_MSG = 0x0011SSID_CMD = 0x0012SSID_PASSWORD_MSG = 0x0013SSID_PASSWORD_CMD = 0x0014WIFI_REGION_MSG = 0x0015WIFI_REGION_CMD = 0x0016WIFI_MSG = 0x001aVIDEO_ENCODER_RATE_CMD = 0x0020VIDEO_DYN_ADJ_RATE_CMD = 0x0021EIS_CMD = 0x0024VIDEO_START_CMD = 0x0025VIDEO_RATE_QUERY = 0x0028TAKE_PICTURE_COMMAND = 0x0030VIDEO_MODE_CMD = 0x0031VIDEO_RECORD_CMD = 0x0032EXPOSURE_CMD = 0x0034LIGHT_MSG = 0x0035JPEG_QUALITY_MSG = 0x0037ERROR_1_MSG = 0x0043ERROR_2_MSG = 0x0044VERSION_MSG = 0x0045TIME_CMD = 0x0046ACTIVATION_TIME_MSG = 0x0047LOADER_VERSION_MSG = 0x0049STICK_CMD = 0x0050TAKEOFF_CMD = 0x0054LAND_CMD = 0x0055FLIGHT_MSG = 0x0056SET_ALT_LIMIT_CMD = 0x0058FLIP_CMD = 0x005cTHROW_AND_GO_CMD = 0x005dPALM_LAND_CMD = 0x005eTELLO_CMD_FILE_SIZE = 0x0062 # pt50TELLO_CMD_FILE_DATA = 0x0063 # pt50TELLO_CMD_FILE_COMPLETE = 0x0064 # pt48SMART_VIDEO_CMD = 0x0080SMART_VIDEO_STATUS_MSG = 0x0081LOG_HEADER_MSG = 0x1050LOG_DATA_MSG = 0x1051LOG_CONFIG_MSG = 0x1052BOUNCE_CMD = 0x1053CALIBRATE_CMD = 0x1054LOW_BAT_THRESHOLD_CMD = 0x1055ALT_LIMIT_MSG = 0x1056LOW_BAT_THRESHOLD_MSG = 0x1057ATT_LIMIT_CMD = 0x1058 #wiki是错误的ATT_LIMIT_MSG = 0x1059
EMERGENCY_CMD = 'emergency'

低级协议的编码

# Flip命令取自Go版本的代码# FlipFront向前翻转。FlipFront = 0# FlipLeft向左翻转。FlipLeft = 1# FlipBack flips backwards.FlipBack = 2# FlipRight flips to the right.FlipRight = 3# FlipForwardLeft flips forwards and to the left.FlipForwardLeft = 4# FlipBackLeft flips backwards and to the left.FlipBackLeft = 5# FlipBackRight flips backwards and to the right.FlipBackRight = 6# FlipForwardRight向前和向右翻转。FlipForwardRight = 7

翻转的版本

写了很久了,下篇文章继续读

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约