TelloPy-develop-0.7.0源码阅读.1
最近我在反思,为什么我看了那么多书,为什么还是写不出大型的程序?我也很苦恼,我想了下。应该还是看的源码少的过,古人曾经说过熟读唐诗三百首,不会吟诗也会吟 。在读源码的选择上,我没有选择太复杂的开源库,而是选择了自己比较熟悉的TT无人机库,而且代码行数选择在1k行以内的库。就是下面这个库了~
首先我们在文章开头先简要的说明一下,什么是工厂函数
from setuptools import setup, find_packages
from codecs import open
from 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 datetime
import threading
LOG_ERROR = 0
LOG_WARN = 1
LOG_INFO = 2
LOG_DEBUG = 3
LOG_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 datetime
import threading
LOG_ERROR = 0
LOG_WARN = 1
LOG_INFO = 2
LOG_DEBUG = 3
LOG_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 = 0xcc
SSID_MSG = 0x0011
SSID_CMD = 0x0012
SSID_PASSWORD_MSG = 0x0013
SSID_PASSWORD_CMD = 0x0014
WIFI_REGION_MSG = 0x0015
WIFI_REGION_CMD = 0x0016
WIFI_MSG = 0x001a
VIDEO_ENCODER_RATE_CMD = 0x0020
VIDEO_DYN_ADJ_RATE_CMD = 0x0021
EIS_CMD = 0x0024
VIDEO_START_CMD = 0x0025
VIDEO_RATE_QUERY = 0x0028
TAKE_PICTURE_COMMAND = 0x0030
VIDEO_MODE_CMD = 0x0031
VIDEO_RECORD_CMD = 0x0032
EXPOSURE_CMD = 0x0034
LIGHT_MSG = 0x0035
JPEG_QUALITY_MSG = 0x0037
ERROR_1_MSG = 0x0043
ERROR_2_MSG = 0x0044
VERSION_MSG = 0x0045
TIME_CMD = 0x0046
ACTIVATION_TIME_MSG = 0x0047
LOADER_VERSION_MSG = 0x0049
STICK_CMD = 0x0050
TAKEOFF_CMD = 0x0054
LAND_CMD = 0x0055
FLIGHT_MSG = 0x0056
SET_ALT_LIMIT_CMD = 0x0058
FLIP_CMD = 0x005c
THROW_AND_GO_CMD = 0x005d
PALM_LAND_CMD = 0x005e
TELLO_CMD_FILE_SIZE = 0x0062 # pt50
TELLO_CMD_FILE_DATA = 0x0063 # pt50
TELLO_CMD_FILE_COMPLETE = 0x0064 # pt48
SMART_VIDEO_CMD = 0x0080
SMART_VIDEO_STATUS_MSG = 0x0081
LOG_HEADER_MSG = 0x1050
LOG_DATA_MSG = 0x1051
LOG_CONFIG_MSG = 0x1052
BOUNCE_CMD = 0x1053
CALIBRATE_CMD = 0x1054
LOW_BAT_THRESHOLD_CMD = 0x1055
ALT_LIMIT_MSG = 0x1056
LOW_BAT_THRESHOLD_MSG = 0x1057
ATT_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
翻转的版本
写了很久了,下篇文章继续读