本章的主题为一个嵌入式软件开发的助手,由于嵌入式软件开发的特殊性,这里的嵌入式软件开发助手能够提高嵌入式软件开发者的效率。
20.1 要解决什么问题
嵌入式软件开发跟传统软件开发不一样,嵌入式软件开发是跨平台的。传统的软件开发就像我们的Python编程一样,在电脑上进行开发,在电脑上运行。而嵌入式软件开发不一样,在电脑上开发,但不在电脑上运行,而是运行在目标机(嵌入式设备)。凡是此类开发和运行不在同一个环境中的软件开发,我们基本都可以称为嵌入式软件开发。
而正如上面的描述的,嵌入式软件开发比较特殊,因此我们在嵌入式软件开发中会遇到下面的一些问题。下面将以作者本人的嵌入式开发过程为例,来实现这样的一个工具帮助自己提高效率,仅作参考。因为每个公司或者项目的实际情况都会有不同差异,我们这里的开发过程大致如下:
- · 在本地电脑上修改代码,然后将被修改的代码上传到远端编译服务器上;
- · 登陆到编译服务器进行编译;
- · 将编译后得到的bin或img文件下载到本地电脑上;
- · 将下载到本地电脑的bin或img文件上传到测试环境电脑上;
大致流程如上所示,我们每次修改代码后都需要重复上面的过程。尤其是将本地修改的代码同步到编译服务器上是一件比较麻烦的工作,如果修改的文件比较多那就更耗时间。所以如果我们能够将这些重复的操作用程序来完成,是不是能大大提高我们的工作效率,那么接下来我们就用Python来实现这样一个程序,来帮助我们完成这些重复性的工作。
20.2 实现思路
接下来我们看看如何实现该程序。
首先,我们在本地电脑上安装git bash,将代码同步到本地。这样我们就可以在本地电脑上阅读和修改源码,其次将编译服务器上的代码checkout到同一个节点,也就是保证编译服务器上的代码跟本地代码完全一样。接下来,我们就可以用source insight来阅读和修改源码。通过git status命令,我们可以找出所有被修改了的源码文件,这样就可以利用该结果去同步被修改的了文件。下面给出大致的实现思路:
- · 利用git status命令找出本地被修改了的源码文件;
- · 通过scp命令或Python模块将上面找出的源码文件上传到远端编译服务器;
- · 上传到编译服务器后,我们可能需要进行必要的格式转换,因为windows和linux的换行符有区别,此外可能也需要执行一些代码格式转换或检查的工作;
- · 通过ssh登陆到远端编译服务器进行代码编译,同时将编译结果回显到我们的工具界面上,便于解决编译错误;
- · 编译成功后,将相关的bin文件或img文件下载到本地电脑;
- · 在访问实验室测试电脑之前,需要做特定的登陆鉴权,然后才能访问实验室电脑;
- · 将下载到本地电脑的bin文件上传到实验室测试电脑;
这里的实现只是针对作者个人的使用场景和习惯,读者如果从事嵌入式开发,可能相应的使用场景比这里简单。但没关系,代码都是可裁剪的,可以很方便删除或注释不需要的步骤。
20.3 相关模块的安装及介绍
本节将会涉及很多模块,这里只做简单的介绍。
20.3.1 configparser模块
configparser模块可以用于解析ini格式的文件,利用该模块可以很方便的读写配置文件。我们将使用模块进行解析配置文件ini,程序的配置参数都存储在ini配置文件中,通过下面的例子可以进行了解基本的操作接口。
cf = configparser.ConfigParser()cf.read(file)# Parser config fileparser_arg[LOCAL_CODE_DIR] = cf.get('LOCAL', 'local_code_directory')parser_arg[SSH_IP] = cf.get('COMPILE_SERVER', 'ip')parser_arg[SSH_PORT] = cf.get('COMPILE_SERVER', 'port')
我们这里不需要修改,只需要读取其中的对应的ip地址和port号即可,配置文件booking-testline.ini如下面的内容。
[LOCAL]local_code_directory = C:local_repocode_dir[COMPILE_SERVER]ip = 10.10.10.10port = 22
20.3.2 logging模块
logging模块正如其名,是用来记录日志的,可以灵活设置帮助记录程序运行过程或辅助定位问题。支持不同的日志等级,可以很方便的设置级别,使调试版本和发布给别人的版本使用不同的日志级别。可以将日志打印在串口,也可以保存在日志文件中。还可以自定义日志信息的格式,使其更易于阅读。
我们的程序中将定义如下日志信息格式,及具体使用的方法。通过logging.debug(),logging.info()和logging.error()等不同接口来记录日志信息。我们这里设置的level为logging.INFO级别,因此DEBUG级别的信息不会在日志中出现。当需要调试定位问题时,可以将日志级别修改为DEBUG,则所有日志信息都会被保存。
下面代码是我们程序中使用logging的代码片段。
def repair_core_dump_file(self, dump_file, core_file): sp = [0, 0, 0, 0] lr = [0, 0, 0, 0] pc = [0, 0, 0, 0]self.get_reg_from_dump_file(dump_file, 13, sp)self.get_reg_from_dump_file(dump_file, 14, lr)self.get_reg_from_dump_file(dump_file, 15, pc)logging.info("sp: 0x{}".format(''.join(sp)))logging.info("lr: 0x{}".format(''.join(lr)))logging.info("pc: 0x{}".format(''.join(pc)))with open(core_file, 'rb') as coreHandle:content_by_hex = coreHandle.read().hex()rs = content_by_hex.rfind(''.join(sp))if rs != -1:content_by_hex = content_by_hex.replace(''.join(sp) + '0000000000000000',''.join(sp) + ''.join(lr) + ''.join(pc))with open('repaired-' + core_file, 'wb') as new_coreHandle:content_by_binary = binascii.unhexlify(content_by_hex)new_coreHandle.write(content_by_binary)logging.info("Position:{} is {}".format(rs, content_by_hex[rs:rs + 8]))logging.info("Position:{} is {}".format(rs, content_by_hex[rs + 8:rs + 16]))logging.info("Position:{} is {}".format(rs, content_by_hex[rs + 16:rs + 24]))logging.info("content_by_hex = {}".format(content_by_hex))logging.info("SP000 hint count: {}".format(content_by_hex.count(''.join(sp) + '0000000000000000')))logging.info("sp hint count: {}".format(content_by_hex.count(''.join(sp))))
20.3.3 os模块
os模块是一个比较常用的模块,从名字就可以看出跟操作系统相关。比如需要获取当前工作目录时,可以通过getcwd()接口。也可以很容易的将路径中的文件夹部分和文件名部分进行分离。
>>> import os>>> os.getcwd()'C:甥敳獲刚刚刘'>>> os.path.abspath('.')'C:甥敳獲刚刚刘'>>>>>> path = r'E:第09章 爬虫下载voa每日广播英语MP3文件auto-download-voa-broadcast.py'>>> os.path.split(path)('E: 第09章 爬虫下载voa每日广播英语MP3文件', 'auto-download-voa-broadcast.py')>>>
我们这里的程序将使用os模块判断文件是否存在,代码如下。
>>> path = r'E:第09章 爬虫下载voa每日广播英语MP3文件auto-download-voa-broadcast.py'>>> os.path.exists(path)True>>>
20.3.4 re模块
re模块是Python的正则表达式模块,跟其他语言的正则表达式一样,该模块提供正则表达式相关的处理。当然,该模块定义了一堆正则表达式相关的语法,需要使用的读者可以进行详细的学习。我们的程序这里只使用了sub接口,这里的用法是将字符串data中的''替换为''。如前面实现思路中提到的,我们需要将windows的换行符替换为linux的换行符,而这里就是在做这件事情,实现该功能的函数如下所示。
def dos_to_unix(src_file, dst_file): with open(src_file, 'rb+') as FOBS: data = FOBS.read() if b'' not in data: # if don't have MS newline, return directly return Falsedata = re.sub(b'', b'', data) with open(dst_file, 'wb') as DOBS: DOBS.write(data)return True
更多关于该模块的详细内容,可以参考下面的链接。
20.3.5 select模块
select模块提供了对大多数操作系统中select()和poll()函数的访问。在windows上,该接口只对socket有效,不能用于文件。我们的这里的程序用select来读取服务器端的返回信息,本质上是也给socket,因此可以在这里使用,代码片段如下。
read_list, write_list, err_list = select.select([channel], [], [])if channel not in read_list:time.sleep(1)if --timeout_counter <= 0:logging.error("There is no respond to %s within 30 seconds", command)breakelse:continue
更多详细信息可以参考下面链接的内容。
20.3.6 shelve模块
shelve模块是一个比较实用的模块,我们用它打开一个文件,然后像操作字典一样进行数据的读写。如果有程序需要保存一些临时数据,或者数据不大,可以实用此模块进行。比如,我们的程序中用该模块来存储文件名,例如存所有需要被上传到服务器的文件名及路径。
s = shelve.open("file-list", writeback=True)s['local_files_list'] = new_files_lists.close()
上面代码是我们用于存储用户名和密码的代码,下面再看看当我们需要使用时如何获取用户名和密码。
s = shelve.open("file-list", writeback=True)local_files_list = s['local_files_list']s.close()
20.3.7 sys模块
sys模块是一个内建模块,不需要单独安装。
sys模块提供了对Python解释器使用的一些变量的访问,并可以进行一些修改,例如对环境变量PATH的读取和修改,并提供了某些和解释器进行交互的函数以使我们的程序能够和解释器进行交互。
例如,sys.argv会将命令行参数以list的形式传递给Python脚本,sys.argv[0]是脚本的名字,sys.argv[1]是第一个参数,以此类推。
sys.exit()表示退出程序,也可以带参数表示退出码,如果有其他程序调用该程序,即可以通过返回的数字来确定被调用程序的退出原因。
sys.implementation查看当前正在运行的Python解释器的版本信息。
>>> sys.implementationnamespace(cache_tag='cpython-36', hexversion=50726384, name='cpython', version=sys.version_info(major=3, minor=6, micro=5, releaselevel='final', serial=0))>>>
sys.stdin,sys.stddout,sys.stderr解释器用于标准输入,标准输出和错误。
20.3.8 time模块
time模块提供了时间相关的各种函数。
time.asctime()函数可以将结构体struct_time所代表的时间转换为这样的字符串'Sun Jun 19 13:31:15 1994'。我们可以通过time.localtime()函数得到结构体struct_time。
time.sleep()函数的入参单位是秒,如果线程需要被挂起,可以通过调用此函数达到该目的。这里的入参也可以是小数,表示更精确的睡眠时间。我们将会使用该函数进行必要的等待以确保另一件事情结束。
time.strftime()函数也用于格式化时间。
>>> import time>>> time.strftime('%Y%m%d',time.localtime(time.time()))'20190714'>>>>>> time.strftime('%Y-%m-%d')'2019-07-14'>>>
另外一个常用的操作就是用time.strptime()接口来判断给定的字符串是否是一个有效的日期。
def is_valid_data(date_str):"""Check if date string is valid"""try:time.strptime(date_str, "%Y%m%d")return Trueexcept:return False
20.3.9 binascii模块
binascii模块包含了许多方法,这些方法可以用于在二进制和各种ASCII编码中进行转换。该模块包含了用C语言实现的底层函数,使得高层次模块调用的时候可以获得更快的速度。在我们的程序中将会使用到unhexlify()方法,用于将十六进制数据转化为二进制形式,并将该数据存入文件。
下面是我们程序中使用的代码片段,将数据以十六进制形式得到后,进行相应的替换处理,然后转化为二进制写入新的文件中。
with open(core_file, 'rb') as coreHandle:content_by_hex = coreHandle.read().hex()rs = content_by_hex.rfind(''.join(sp))if rs != -1:content_by_hex = content_by_hex.replace(''.join(sp) + '0000000000000000',''.join(sp) + ''.join(lr) + ''.join(pc))with open('repaired-' + core_file, 'wb') as new_coreHandle:content_by_binary = binascii.unhexlify(content_by_hex)new_coreHandle.write(content_by_binary)
更多关于该模块的详细信息可以参考下面的链接。
20.3.10 redis模块
Redis模块是一个内存中的数据结构存储系统,我们这里使用它作为我们的数据库。个人觉得比较简单实用包括部署和操作。而且跟Python常用的数据能够很好的融合,比如字符串,散列,列表和集合等等,同时也支持磁盘持久化。可以通过Redis的中文网站进行学习和下载。我们的例子将只用到最基本,最简单的特性,就是当作一个数据库来存储数据,同时可以为每条数据设置一个有效期。而实际上,Redis非常强大,性能也很好,感兴趣的读者可以进行深入的学习,这里只做一些比较简单的介绍。
图20-1 Redis中文官网
我们的程序中redis操作代码片段如下。
def redis_instance(ip, port):logging.debug("get data")pool = redis.ConnectionPool(host=ip, port=port, decode_responses=True, socket_connect_timeout=3)r = redis.Redis(connection_pool=pool)logging.debug(r)try:r.ping()logging.debug('ping success')return rexcept TimeoutError:logging.error('redis connection timeout ' + ip)return Noneexcept:logging.error("redis don't exist ")return Nonedef get_key_from_redis(ip, port, key):testline = []r = redis_instance(ip, port)if r:logging.debug("get data: " + key)testline = r.lrange(key, 0, -1)logging.debug(testline)return testlinedef add_key_to_redis(ip, port, key, value):r = redis_instance(ip, port)if r:logging.debug(f"add key: {key} {value}")redis_mutex.lock()if list == type(value):for item in value:r.rpush(key, item)elif str == type(value):r.rpush(key, value)#extime = datetime.datetime(0, 0, 0, 12, 00, 00)r.expire(key, 28800) #28800redis_mutex.unlock()def delete_value_of_key_from_redis(ip, port, key, value):r = redis_instance(ip, port)if r:logging.debug(f"del data: {key} {value}")redis_mutex.lock()r.lrem(key, 0, value)redis_mutex.unlock()def delete_key_from_redis(ip, port, key):r = redis_instance(ip, port)if r:logging.debug(f"del key: {key}")redis_mutex.lock()r.delete(key)redis_mutex.unlock()
这里我们将相应的操作都封装成了函数,并做了加锁操作。
20.3.11 requests模块
requests模块是一个用于访问网络的模块,我们这里的程序需要先进行用户鉴权。在通过用户名和密码进行鉴权通过后,我们的程序才被运行访问数据库,对数据库进行相应的读写操作。
requests模块也可以用于从网络上下载文件,在其他相关程序例子中会涉及这方面的介绍,这里只关注当前实例程序需要使用到的内容。
下面的代码片段就是我们程序中使用到的用于登陆到鉴权网页进行认证的函数,我们将用户名和密码及网页地址作为参数传递给post操作,然后通过post操作返回的文本进行判断是否鉴权成功。在我们的例子中,成功返回的页面文本中包含"Success"字样字符串。
def auth_via_requests(self, i):payload = {}payload['username'] = self.usernamepayload['password'] = self.passwordsuccess_flag = 'User Authentication : Success'ret = Falsewith requests.Session() as s:p = s.post(self.url, data=payload)if success_flag in p.text:logging.info(success_flag)ret = Truereturn ret