树莓派可以做什么

作为一个伪Geek,我去年下半年看到树莓派4出来之后,小手一抖买了两个。一些朋友问我:树莓派可以用来干啥?本篇就来总结一些树莓派的部分用途,从不同的领域来分析可能的应用场景,主要从四个方向介绍(本文略长):

作为个人电脑的使用场景
作为服务器的使用场景
作为多媒体终端的使用场景
作为物联网设备的使用场景
树莓派是什么
树莓派是一系列为编程教育而设计的只有信用卡大小的微型电脑(单板计算机),拥有丰富的硬件接口,能够安装任何ARM平台的操作系统,比如Linux的一些发行版、Android、Windows IoT等。下图是当下的一款RaspberryPi 4 Model B(图片来自http://www.yahboom.com):

一台这么多接口的单板计算机,实际只有掌心大小(RaspberryPi Zero/ZeroW系列只比大拇指大一点),树莓派3/4系列的四核Cortex A53/A72 CPU功耗低,算力不弱。最重要的是竟然还这么便宜,加上配件也只要3位数的¥!

作为一台个人电脑,树莓派可以…
树莓派4B出来的时候,某些标题党自媒体宣传“替代PC”,但Linux系统的PC市场份额很低,桌面软件生态不完善,作为PC用途而言,注定只是一个小众选择。我其中一台树莓派4B来当PC用,总体体验不错,下面从一个软件开发者的使用角度一一分析。

办公用途

阿特伍德定律告诉我们:一切可以用Javascript写的东西最终都会用Javascript写,推导一下就是:一切以前在客户端做的事情,最终都将可以在浏览器上做。

各种软件的Web化,让浏览器成为操作系统之上的“操作系统”,树莓派3B+就可以比较流畅地运行Chromium,树莓派4B表示打开数十个Web页面毫无压力。所以,能跑浏览器就可以满足大多数普通办公场景,树莓派CPU和GPU的算力也完全足以应对简单的办公场景了。

那么办公用途安装什么系统合适?对于普通终端用户,安卓系统的软件生态则更加繁荣,装Android可以覆盖更多办公娱乐场景(毕竟已经破产锤子TNT工作站已经验证了安卓PC的可行性 );专业领域用户装Raspbian则更合适,比如我这样的程序猿。

教育用途

树莓派基金会是一个慈善组织,让更多人可以接受到编程教育是树莓派的初衷。买一个老少皆宜的树莓派给孩子玩,比去什么少儿编程强多了。个人认为孩子参加少儿编程课程是没有必要的。计算机科学是建立在基础学科根基上的,而编程语言又是建立在计算机科学上的,连数学物理基础都没有的低年级孩子学积木式编程,如建空中楼阁。让孩子玩的开心、锻炼逻辑思维的方式很多,除非孩子对计算机有极大的兴趣,否则我不建议低年级孩子刻意参与编程教育课程。

但在树莓派上参与编程和计算机相关教育活动,是有别于现在市面上的课程的,因为玩树莓派学计算机更具有灵活性,Raspbian系统自带各种真正的编程教育软件和多种语言的初级IDE软件,有兴趣可以自由地深入探索,无兴趣不如多去读读书或者参加户外活动。下图是经典的编程教育软件Scratch的截图(Scratch当前版本也已经Web化,甚至无需安装客户端也可以玩,不一定要在树莓派上运行,图片来自Scratch网站 https://scratch.mit.edu/explore/projects/)

游戏用途

树莓派可以玩Minecraft,也可以运行复古游戏机模拟器,比如RetroPie (https://retropie.org.uk)。能在树莓派上玩的游戏有限,可能更适合小众动手能力很强的Geek的需求。

编程开发用途

Python开发:非常棒,现在Python不仅在机器学习领域是头把交椅,在嵌入式开发也是动态脚本语言第一(感谢
@limyel
提出,Python是强类型而不是弱类型的语言,前一版搞混了)。树莓派上使用Python的优势在于:
树莓派上Python的硬件开发库非常完善,甚至很多库都兼容Arduino上跑micropython
Python本身的跨平台特性,脚本写好放在哪都能跑,比如在树莓派上跑Tensorflow甚至是PyTorch都可以!不过在机器学习方面,树莓派的算力就捉襟见肘了。
Golang开发:差强人意。能写能跑,但调试不方便,因为Go的Debug工具dlv对ARM的支持2020年才初步解决(https://github.com/go-delve/delve/issues/118 )。另一个问题是Go的主场不在嵌入式开发,硬件相关库兼容性和稳定性不足。我尝试过Golang的硬件驱动库https://github.com/google/periph,比Python的庞大完善的生态还差很多。
JS/TS开发:还不错。VS Code作为JS/TS,甚至是C# .Net Core开发的最佳工具,既可以本机直接运行VS Code也可以用VS Code Remote模式。大部分常用的VSCode插件在树莓派上都能正常运行,具体来说:
前端开发体验完美,虽然Webpack构建时间比Intel i7的机子大概慢一倍多,但Hot Reload仍然是秒级,丝毫不影响开发
使用JS/TS开发NodeJS后端体验完美,常用的带C++ Binding的npm库也都能在ARM下正常编译
嵌入式开发不够好,生态不完善,一些硬件模块在npm找不到合适的库,目前rpi-gpio库在树莓派4上有问题,毕竟ECMAScript的主场在大前端领域,不是Python的对手。
小结
通过上面几个例子可以看出,作为开发者是可以尝试使用树莓派来替代PC做日常开发的,主流编程语言及其工具链、三方库大多保持着对ARM平台的兼容性。中小型项目在开发阶段对计算资源要求并不高,树莓派接上屏幕配个键盘,或是VS Code Remote进行远程开发,都挺好的。当然其局限性也不少,比如:ARM Linux下很难做传统C/C++的客户端软件开发;算力有限,大型项目和一些细分领域的专用软件只能用x86平台的中高端PC。

作为一台服务器,树莓派可以…
树莓派更多的用途是在服务器端。可以只在局域网使用,也可以搭配DDNS技术把树莓派挂到公网上(一般的企业甚至电信家庭宽带都可以申请到公网IP,或者直接买一台乞丐配置的云服务器也可以拥有公网IP),不到5W的功耗打造个人云服务器,一年只要20块钱电费,性价比极高。下面来细数一下具体用途。

注:实践这些应用场景,容器化技术是最佳选项,比如先装上Docker或者Containerd,再安装各种Docker镜像就可以满足大部分需求。对于多个树莓派组建集群的场景,推荐Rancher的K3S或者用Docker Swarm,K3S和Kubernetes在使用上几乎感觉不到区别,更轻量但一样强大。

Web服务器

树莓派的算力部署普通的Web服务绰绰有余了,制约并发能力和延时的的可能会是家庭宽带的上行带宽(家庭网络上行带宽远低于下行带宽,300Mbps的下行带宽或许只有30Mbps的上行带宽)。非高并发和高带宽要求的服务,完全可以用树莓派搭建服务端,比如:个人网站,App或小程序后台等等(前提是有固定公网IP,或者使用DDNS把非固定公网IP的树莓派挂到公网)。树莓派的官方网站据说就是18个树莓派服务器集群组成的。

因为各种主流的高级语言和开源组件几乎都支持ARM平台,所以选择熟悉的语言和技术来开发就行了,和在Intel/AMD CPU的Linux上运行差别不大。精简指令集的ARM低功耗优势在服务端非常明显,这几年ARM服务器的出现也印证了这一点。

网格计算

网格计算是分布式计算的一种形式,举个例子,我们可以用树莓派参与SETI@home项目,帮科学家们搜索外星人!全世界的志愿者都可以用算力闲置的设备安装BOINC客户端参与项目,分析射电望远镜的数据寻找可疑的电磁波。BOINC全称是伯克利开放式网络计算平台,现在已经有数十个项目可以参与,如果你不相信有外星人,也可以用BOINC客户端来参与其他更实际的项目,比如我比较喜欢这几个:

Rosetta@home 预测蛋白质结构和蛋白质设计
lhc@home 帮助CERN的大型强子对撞机模拟粒子的运行

注:一些项目在Windows上运行时会有酷炫的屏保(如上图),动态展现当前计算的状态,Linux下命令行运行不会有屏保图片。

我个人服务器上一直用50%的CPU在运行这些项目分摊下来的计算任务,树莓派也可以运行,但相对来说慢一些。我小时候的梦想是从事基础科学的研究,最终却因为种种原因变成了一只程序猿,贡献一些算力也算圆了一丢丢科学梦吧。

同属于分布式计算领域的区块链最近比较火,区块链技术其中一个应用就是虚拟货币,俗称挖矿。树莓派也可以挖矿,只是效率比较低。除了网格计算,还有两个云计算衍生的概念:边缘计算、雾计算,乍一听云里雾里的,大概意思就是:局域网有一堆传感器和硬件设备数据,找个算力还行的设备先汇总计算一下,做一些在内网就能做的事情。具体到应用,比如作为智能家居的中枢、影音娱乐中心等等,具体细节到下面讲物联网方面再展开。

私有云

云存储、云办公等公有云的服务对于大部分人来说已经很方便了,但存在隐私和数据安全方面的担忧,高质量的云服务必然是有代价的,最典型的例子就是某度云的网盘限速逼着用户充会员。

私有云大多在企业中应用,但其实每一个人都可以有一朵属于自己的云,既能像云盘一样随时随地存取和备份文件,又可以覆盖日常办公大多数场景,如文档/表格/PPT/思维导图/UML图编辑、笔记/便笺、任务规划、日历日程和联系人管理等等,所有的数据既掌握在自己手里,又可以任何时间地点访问和同步数据。这么多强大的功能,竟然在树莓派上部署一个NextCloud就全有了!部署这样一整套私有云,对熟悉Linux的人来说,完全是30分钟以内就能搞定的事情。

NextCloud是我看到过的最酷的PHP项目。上几个图来感受一下这开箱即用,功能齐全的私有云系统吧!

文件存储是核心功能,手机上安装NextCloud的App实时同步相册,备份联系人,上传下载文件,是非常实用的功能。导航栏还安装了一些主流的官方和社区的插件,“更多”里面还有:管理个人密码,手机位置追踪等我觉得很不错的插件。另外Office办公套件和http://Draw.io画图插件也非常棒,社区还有很多优秀的插件来满足各种需求。

其中手机追踪插件(PhoneTrack),需要手机安装PhoneTrack的App上传位置信息,然后就可以画出自己每天的轨迹,甚至包括海拔信息。

非IT从业者,或者没玩过LNMP技术栈的话,下载NextCloud Pi系统镜像写入SD卡,树莓派直接启动就可以快速体验NextCloud。关于树莓派支持安装的操作系统,我也整理了一下,用NextCloud的脑图插件,画了个思维导图,这个脑图也是在树莓派上画的。

私有云的用途写的比较多,因为这是我觉得这是老少皆宜最实用的防树莓派吃灰的用途!NextCloud除了强大的插件体系扩展了这个私有云系统的能力,其衍生产品NextCloud Talk甚至还可以用来聊天和语言视频通话。如果不用NextCloud系列搭建私有云,也有其他类似的软件可以尝试。

有人可能会觉得小小的树莓派,怎么可以胜任云存储?几千块钱的群晖NAS固然高端大气,但大部分人只是想同步一下手机相册,备份一下数据,偶尔记记笔记画画图,真的需要磁盘冗余阵列,万兆存取速度吗?用树莓派的USB3.0接个移动硬盘,插上网线,局域网1000Mbps的速率也足够了,5W功耗吊打专业NAS,写个爬虫脚本放上去运行又能作为一台资源下载机。至于公网访问,用DDNS + 免费TLS证书这种方案弄到公网,访问速度也远高于不充会员的百度云盘。

服务端的应用场景就介绍到这里吧,上述的用途还没有用到树莓派大部分硬件接口,下面细说一些跟硬件相关的用途。

作为一个多媒体终端,树莓派可以…
从上面的思维导图看,树莓派有不少多媒体硬件接口。音视频的输入输出也可以带来很多应用场景,下面一一介绍。

家庭安防监控

树莓派的CSI接口接上摄像头,写个录像脚本就变成了最简易的家庭监控摄像头,进阶一点可以搭建一个RTMP Server做实时视频流,想当个主播,把视频流转到直播平台就可以了。

只是作为监控摄像头效果肯定不如海康,360,小米等公司的成熟产品,但树莓派的优势在可扩展性,发挥想象力能够DIY出无限的可能性。我感触很深的一个例子是有一个大佬颇有创新的树莓派项目——共享鱼缸,用树莓派给自家水族馆直播,观众可以互动投食。详细信息参考:https://shumeipai.nxez.com/2017/09/27/nature-aquarium-for-sharing.html

家庭影音娱乐中心

树莓派加上一根HDMI线连到电视上,就是一个机顶盒,搭配OpenELEC/Kodi系统,当一个家庭多媒体中心也是不错的选择。但现在版权控制非常严格,即使OpenELEC虽然很优秀,又有丰富的插件,但新的影视资源并不好找,各家网络视频公司和运营商也是封闭式的发展各自的App和会员系统,他们拿到版权并不想把影视资源付费开放出去。如果有资源的话,把这些影视资源用装了OpenELEC系统的树莓派管理起来,是能够代替家里的机顶盒的,无需忍受各种广告。

音视频通信系统

拇指大的树莓派ZERO就足够扩展成一个小电话了,之前也看到过有Geek把树莓派电话做成了一个真正的产品。我尝试过编译BareSIP(一个C语言写的SIP终端软件)把树莓派变成一个视频会议设备,开Zoom会议或者其他支持SIP/H.323协议接入视频会议是没有问题的,会议期间CPU使用率仅有25%,音视频也很流畅,缺点是裸露的3.5mm耳机接口杂音比较大。对于开发者来说可以二次开发来解决问题,甚至变成真正的商用产品,也有人这么做了。

对于个人用户来说,Raspbian客户端软件的生态不齐全,比如没有ARM Linux版本的Zoom客户端,只能间接通过SIP终端接入(前提是需要买Zoom的Conference Room Connector的服务),如果是装安卓系统来实现音视频通信可能更方便一些。

注:上图为树莓派上BareSIP拨号后的输出,可以配置GPU对视频流硬件编码,帧率还不错。

作为物联网设备,树莓派可以…
上面的用途,基本都不会用到那40Pin引脚,而树莓派的40Pin引脚,是它变身物联网设备和边缘计算中枢的精髓所在。利用树莓派的通用输入输出(GPIO)能力,可能会找到是最富有创新点和最具有Geek范儿的用途,做无人机、做机器人、做智能音箱,还有无数的可能性等待发掘和探索。

从基础的电子积木开始——硬件模块控制中心
树莓派GPIO引脚直连传感器或者其他硬件模块,或是用UART等方式连上其他的MCU。树莓派收集到数据,做一些处理和控制逻辑,再做一些云端同步和消息推送之类的,就可以告诉别人实现“雾运算”了。

简单的搭积木式地组合一些硬件,是入门嵌入式开发的一个方法,如果具体到一些的实际场景,可以衍生出各种应用,比如门禁系统、3D打印机监控和控制组件、无人机图传组件等等,都可以基于树莓派去做。

智能小车

智能小车很多电子电气专业的同学在学校都做过,基础的功能有避障、测距、循迹、遥控等等。51系列单片机的例子比较多,而树莓派加个底盘、轮胎,连上传感器、马达、电源,也能摇身一变智能小车,网上可以找到很多树莓派小车的案例。

简单的智能小车显然实用价值不高,发散思考一下,放大一些可以做智能儿童玩具车,装上刷子可以做扫地机器人,配上室内定位系统可以变成一个智能仓储的运输车。不过工程化研发和量产是有很多挑战的,实际上真正的电动玩具车和扫地机器人也不可能用树莓派作为MCU,但这并不妨碍我们拿树莓派去学习、探索和尝试。

机器人、无人机

这两个产业算是IT技术的集大成者,涉及多个学科的前沿科技,技术含量和门槛都很高,但也有一些大佬用树莓派做成了简单的机器人、无人机的。这里说的机器人并不是简单遥控的“伪机器人”,而是说具有感知能力甚至人工智能的产品。比如斯坦福机器人俱乐部的四足机器人项目:

DogGo:斯坦福大牛们创造的开源四足机器人,使用的MCU是Teensy系列的板子 https://github.com/Nate711/StanfordDoggoProject
Pupper/Woofer:斯坦福机器人俱乐部的二代目四足机器人,使用的MCU正是树莓派ZEROhttps://github.com/stanfordroboticsclub/StanfordQuadruped
关于无人机,我一直有个梦想是组装一个自己的四轴无人机(目前还没有提上日程)。无人机最关键的部分是飞行控制系统,简称飞控,Geek们用的大多是当前最流行的开源飞控平台APM。APM发源于Arduino,目前也支持Linux运行,因此树莓派也是可以运行APM飞控的的。另一个开源飞控PX4,适用于Pixhawk的硬件,直接跑在树莓派Linux系统似乎不太方便。

直接拿树莓派跑飞控做无人机难度还是比较高的,简单的途径是用树莓派做图传,飞控用专用模块,积木式拼接来组装一个无人机。当然,要真正创造它们不仅需要软硬件开发能力和很强的动手能力,深究下去还需要数学、物理以及衍生学科的知识作为理论基础。

更进一步,更高级的感知和预测等AI能力离不开机器学习,在这方面也有一些可以在树莓派上跑的开源机器学习平台,比如:

Tensorflow Lite:Tensorflow的轻量化版本,适用于边缘计算,可以用来在树莓派上训练模型,实现简单的人工智能应用
Pytorch:Facebook的开源机器学习库,最近比较火,也能在树莓派上跑起来
最直接的例子是有Geek拿树莓派玩自动驾驶,有一些是基于Donkey Car项目开发的(Donkey Car是一个基于Tensorflow的开源自动驾驶项目,训练过程需要在高性能GPU上进行,树莓派算力不够)https://github.com/autorope/donkeycar

智能音箱

2014年亚马逊Echo发布之后,智能音箱产业蓬勃发展,2020年已是一片红海,占据国内大部分市场的只剩小米的小爱音箱,天猫精灵,百度小度这几个终端产品了。智能音箱这么火,我们是不是可以打造一个自己的智能音箱呢?其实智能音箱的最核心的几项技术:语音唤醒、语音识别、自然语言处理/语义理解、语言播报,各大厂开发者平台都提供了API,要自己打造一个“没有核心技术”的智能音箱,做API的集成就可以了,毕竟只有技术和数据方面的大厂才有可能拥有智能语音和NLP领域最好的模型。

如果用树莓派做智能音箱的话,软件方面可以选择开源项目Wukong Robot(https://github.com/wzpan/wukong-robot),代码清晰,并且已经实现了很多服务API的集成;硬件方面最好用麦克风阵列获取音频输入,推荐ReSpeaker 4麦克风阵列扩展板,远场识别效果还是不错的。

我拿其中一个树莓派4 + ReSpeaker 做了个“智障”音箱,另外装了DLNA的服务器和客户端(minidlna和gmediarender),手机可以连上去放音乐,但感觉相比专业的智能音箱差别还是太大了:

一个是唤醒灵敏度和准确性的问题,这个问题我家小爱音箱也比较严重,经常误唤醒
另一个是延迟和内容质量问题,集成第三方API比大厂音箱自己原生支持要差了一个级别,无论是响应时间,还是准确度和回答内容的质量,都比不上真正的智能音箱,而且个人无法得到优质内容的版权也是个问题。
当然,专业和业余的区别就在这里,虽然质量比不上,但DIY的优势在于灵活性,想集成什么只要开发代码就可以了。

智能家居中枢

要在自己家里部署智能家居,除了购买成熟的解决方案,比如小米系智能家居产品或者天猫精灵系的产品以外,还有个办法就是DIY智能家居。DIY有什么好处呢?

买带有智能功能的家电时,不用管这个品牌是站队了小米系还是站队了天猫系,或是自成一体要下载单独App的,开源社区有方案就用,没方案逆向工程搞起
实现不同品牌产品互通互联,自己定制非常灵活的自动化方案,无需受限于米家等App的功能
便宜,可以选用性价比更高的产品,小米生态链的智能家居产品性价比参差不齐,并不都是高性价比的产品
有捣腾的乐趣,可以扩展自己想要的功能,比如易微联的智能开关甚至可以拆开给ESP模块烧录自己写的固件
那么DIY智能家居具体要做什么呢?

除了选购或改造电子电器产品外,还需要有一个智能家居中枢负责所有硬件的控制和状态同步,以及自动化的实现。这个智能家居中枢控制系统的角色,树莓派完全可以胜任。

如果想多写一些代码,可以考虑基于NodeRed做
不想写太多代码的话,Home Assistant是不二之选
Home Assistant是一个Python的开源智能家居项目,功能非常强大,扩展性也很不错,社区活跃程度极高,已经实现了很多主流家居产品的集成。用Home Assistant能实现打通各种主流家电品牌的智能或非智能产品(非智能的传统产品通过红外控制等手段,也可以转变成为智能产品)。试想一下让iOS的Siri助理开一个十年前的电视机,或是让天猫精灵控制米家的风扇,是不是很激(ji)动(lei)?

要在树莓派上运行Home Assistant(或者其他地方,比如NAS上),用http://Hass.io部署更方便。http://Hass.io是包括了Home Assistant以及周边组件的一套东西的合集,另外,树莓派直接刷Hassbian系统也可以达到相同的效果。

运行成功之后需要将所有智能产品作为实体,通过Yaml的方式配置到系统中,再配置UI、添加一些自动化规则。

Home Assistant有一定的学习成本,我也只尝试了一些插件,学习了简单的流程,折腾过ESPHome并且添加了ESP8266系列的开关模块,但没有真正把家里改造一番。下图分别是一个卡片布局的Hass系统UI,以及一个配置了Floorplan的UI,平板装到墙上统一可视化控制,非常酷炫(图片来自网络,没找到真正的原创是谁)

其他类似产品

除了树莓派系列,Geek们常玩的还有一些AVR(Arduino系列)、ESP(ESP8266系列)、STC系列(89C51,89C52等)单片机。但这些板子的算力远低于树莓派这样的单板计算机,而下面介绍的是一些算力与树莓派相近的产品。

Nano Pi系列:也是一系列小巧的单板计算机,性价比也很高,不同配置的型号很多,有兴趣可以试一试
BananaPi/OrangePi:俗称香蕉派/香橙派,兼(shan)容(zhai)树莓派的开源硬件产品(树莓派硬件并不开源),据说稳定性不如树莓派,目前4代树莓派4核Cortex-A72 CPU的性能成倍提升,感觉这些板子几乎没有性价比优势了
Rock Pi及基于瑞芯微RK系列芯片的开发板:主打高性能和音视频处理能力,国内大多数机顶盒和智能电视都是RK系列芯片,RK3288、RK3399等等ARM芯片,相关的单板计算机产品性能是超过树莓派的,而且主板大多早已支持双摄、双4K视频输出,搭配安卓系统在生态上非常成熟。当然缺点就是价格和配件也稍贵一些。
Intel Edison/Galileo/Joule/Curie:英特尔的x86嵌入式板子,我有一块曾经在英特尔的好朋友赠送的Edison板子,做工精致,400MHz的双核Quark处理器,虽然算力有限,但是能直接跑x86的二进制程序,硬件接口兼容Arduino,是好东西,但价格不菲,而且前几代已经停产。
其他小众板子,大多数跑Linux的都ARM架构的,比如华硕ThinkBoard、荔枝派等等,也有例外,比如某宝上还有卖国产RISC-V架构的平头哥芯片开发板。
树莓派的博通SoC中的CPU是ARM架构的,其他同算力级别的单板计算机,CPU也大多是基于ARM的,少数是基于x86架构的芯片。

精简指令集(RISC)的ARM架构芯片,通常比复杂指令集(CISC)的x86架构芯片功耗低很多,这对物联网和移动端,乃至服务端都至关重要,除非以后电池技术出现革命性发展。这也是我们日常生活中各种电子产品,ARM芯片的身影越来越多的原因。最近苹果甚至宣布了一个新闻:以后Mac系列的PC和笔记本电脑也要用ARM芯片了!

结语

以上列举的所有树莓派的使用场景,大部分是从网络上搜集到的,都是有人已经做过的,我结合了一些个人经验和理解整理介绍了出来。这些也只是无数极客们探索尝试过的一小部分。上面的介绍涉及了很多领域,但树莓派一定还有很多未被发现的用途。

树莓派作为一台Linux计算机,在软件开发方面就有很多应用场景;而硬件和嵌入式开发方面,即使不了解数电、模电等这些电子专业的知识,我们也可以用树莓派加上现成的模块,搭一搭简单的电子积木。站在巨人肩膀上,让半导体中电子的转移按照我们想要的方式,变成声音、电磁波、热量、动能,或是感知这个世界,岂不是一件极其有趣的事情?

vuex常用注意事项

  • getter会缓存结果,不需要缓存,需要把getter变为函数

    1
    2
    3
    4
    5
    getters: {
    getTodoById: (state) => (id) => {
    return state.todos.find(todo => todo.id === id)
    }
    }

    每次调用为

    1
    store.getters.getTodoById(1)

    这样通过函数触发,不会缓存每次结果

  • dispatch和commit区别
    dispatch为异步执行,会返回promise
    commit为同步执行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    actions: {
    async actionA ({ commit }) {
    commit('gotData', await getData())
    },
    async actionB ({ dispatch, commit }) {
    await dispatch('actionA') // 等待 actionA 完成
    commit('gotOtherData', await getOtherData())
    }
    }

    store.dispatch('actionA').then(() => {
    // ...
    })

    一个 store.dispatch 在不同模块中可以触发多个 action 函数。在这种情况下,只有当所有触发函数完成后,返回的 Promise 才会执行。

mac远程连接raspberry

当身边没有显示屏或通过HDMI连接笔记本屏幕不成功时,本教程教你通过最少工具(无屏幕,鼠标,键盘),连接raspberry开发板。

准备工具

  • mac电脑
  • type-c电源线
  • sd卡
  • 读卡器
  • raspberry imager
  • raspberry OS
  • raspberry 开发板
  • tightvncserver
  • vnc viewer

安装Pi OS系统

根据如下视频,安装OS到SD卡

注意:

  1. 参考视频,烧录前,正确设置ssh和wifi的密码,方便通过mac直接远程ssh连接PI

启动系统

当SD卡烧录完成,插入开发版,并启动系统。

ssh连接开发板

查看开发板ip

参考《查看mac共享热点所有链接设备

连接开发板

通过mac,打开终端,输入:

1
ssh pi@开发板ip

安装tightvncserver

终端连接成功后,输入

1
sudo apt-get install tightvncserver

配置tightvncserver

终端输入

1
tightvncserver

此时会提示设置一个8位的登陆密码,以及是否view-only的选项,选no即可。
设置完成后,终端输出如下信息,表示成功

1
2
3
4
5
New 'X' desktop is hzz:1

Creating default startup script /home/pi/.vnc/xstartup
Starting applications specified in /home/pi/.vnc/xstartup
Log file is /home/pi/.vnc/hzz:1.log

mac客户端vnc viewer连接

通过连接下载vnc viewer
安装完成后,打开vnc viewer,输入:开发板ip:端口,如上述:hzz:1,其中1表示连接端口

vnc连接成功后,如下图:

查看mac共享热点所有链接设备

首先先关闭 WiFi 共享,运行下面的命令,并记录下都有哪些端口,比如 en0, en1, bridge0, fw0 等等:

ifconfig

记录好后,打开共享后再运行上面的命令,观察两次端口的变化。我的情况是多出了 bridge100

运行下面的命令,将 bridge100 换成你的系统对应值:

1
arp -i bridge100 -a

它会显示所连接的设备的 IP 和 MAC 地址,比如我的显示如下:

1
2
3
4
5
$ arp -i bridge100 -a

? (192.168.2.2) at ac:cf:c5:28:f3:e7 on bridge100 ifscope [bridge]

? (192.168.2.3) at 5c:f7:d3:aa:15:aa on bridge100 ifscope [bridge]

上面的结果是两个我的手机。

当然它不会特别及时地更新,比如当断掉一个连接后,系统会过一会儿才会更新,并显示连接设备状况,比如我的:

1
2
3
4
5
$ arp -i bridge100 -a

? (192.168.2.2) at (incomplete) on bridge100 ifscope [bridge]

? (192.168.2.3) at 5c:f7:c3:1a:55:aa on bridge100 ifscope [bridge]

第一个的 MAC 地址没有了,说明它断开了。
这个只是一个能用的例子,无法做到路由器那样的比较实时的反映网络状态。

mac烧录raspberry pi镜像

此篇博文用于记录在MacOS系统上为TF卡烧录树莓派操作系统。

下载镜像

网址:https://www.raspberrypi.org/downloads/

我试验下来,上图中的Raspberry Pi Imager for macOS并不好用。因此,我们点左下角的Raspbian图片下载镜像。

这边目前有三种版本:

系统+桌面+推荐软件
系统+桌面
系统
我选择了最简洁的Raspbian Buster Lite,下载.zip并解压出其中的.img文件。

TF卡格式化

首先需要下载格式化工具,我这边使用的是SD Memory Card Formatter,这个软件在windows和macos上都可以用。这里是macos下的下载链接:https://www.sdcard.org/downloads/formatter/eula_mac/index.html

格式化

  • 插入TF卡
  • 打开SD Memory Card Formatter

注意不要格式化错了卡,假如你插入了多个TF卡。上图中的Volume label是格式化后磁盘的命名。

开始烧录

查看驱动器列表

在控制台输入命令:diskutil list

这里,我们获取到TF卡的磁盘路径为/dev/disk2

取消TF卡的挂载

在控制台输入命令:diskutil unmountDisk + SD卡设备路径

烧录

在控制台输入命令:sudo dd if=镜像路径 of=SD卡设备的路径 bs=1m;sync,并输入管理员密码。

注意:文件路径不要出现中文。可以将bs=1m改为bs=4m加快烧录的速度。

这个时间有点长,需要耐心等待,400M的镜像大概耗时2分钟左右。

编写树莓派的ssh配置与wifi配置文件
新建两个.txt文件,分别命名为:

1
2
ssh
wpa_supplicant.conf

注意,取消.txt后缀。

ssh文件为空即可,wpa_supplicant.conf文件中写入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
country=CN
ctrl_interface=DIR=/var/run/wpa_supplicant Group=netdev
update_config=1

network={
ssid="Wifi1的名字"
psk="密码"
priority=优先级,越大越优先
}

network={
ssid="Wifi2的名字"
psk="密码"
priority=优先级
}

然后将这两个文件移动至烧录系统镜像后TF卡的根目录。

这里有两个坑点:

  • SSID名字中不能有符号.
  • 优先级范围为1-10

推出TF卡

diskutil eject SD卡设备路径

参考
MacOS下树莓派烧录img/iso文件到SD卡
mac下烧写树莓派系统
树莓派raspbian系统自动连接WIFI开启ssh

greendao缓存源码分析

GreenDao是Android中使用比较广泛的一个orm数据库,以高效和便捷著称。在项目开发过程中遇到过好几次特别奇葩的问题,最后排查下来,发现还是由于不熟悉它的缓存机制引起的。下面是自己稍微阅读了下它的源码后做的记录,避免以后发现类似的问题。

缓存机制相关源码

DaoMaster

DaoMaster是GreenDao的入口,它的继承自AbstractDaoMaster,有三个重要的参数,分别是实例、版本和Dao的信息。

1
2
3
4
5
6
7
8
//数据库示例
protected final SQLiteDatabase db;

//数据库版本
protected final int schemaVersion;

//dao和daoconfig的配置
protected final Map<Class<? extends AbstractDao<?, ?>>, DaoConfig> daoConfigMap;

DaoMaster中还有两个重要的方法:createAllTables和dropAllTables,和一个抽象的OpenHelper类,该类继承自系统的SQLiteOpenHelper类,主要用于数据库创建的时候初始化所有数据表。

创建DaoMaster需要传入SQLiteDatabase的实例,一般如下创建:

1
mDaoMaster = new DaoMaster(helper.getWritableDatabase())

跟踪代码可知数据库的初始化和升降级都是在调用helper.getWritableDatabase()时执行的。相关代码在SQLiteOpenHelper类中。

在getWritableDatabase方法中会调用getDatabaseLocked方法

1
2
3
4
5
public SQLiteDatabase getWritableDatabase() {
synchronized (this) {
return getDatabaseLocked(true);
}
}

getDatabaseLocked方法如下

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
private SQLiteDatabase getDatabaseLocked(boolean writable) {
// 首先方法接收一个是否可读的参数
if (mDatabase != null) {
if (!mDatabase.isOpen()) {
//数据库没有打开,关闭并且置空
mDatabase.close().
mDatabase = null;
} else if (!writable || !mDatabase.isReadOnly()) {
//只读或者数据库已经是读写状态了,则直接返回实例
return mDatabase;
}
}

if (mIsInitializing) {
throw new IllegalStateException("getDatabase called recursively");
}

SQLiteDatabase db = mDatabase;
try {
mIsInitializing = true;

if (db != null) {
if (writable && db.isReadOnly()) {
//只读状态的时候打开读写
db.reopenReadWrite();
}
} else if (mName == null) {
db = SQLiteDatabase.create(null);
} else {
//创建数据库实例
。。。代码省略。。。

//调用子类的onConfigure方法
onConfigure(db);

final int version = db.getVersion();
if (version != mNewVersion) {
if (db.isReadOnly()) {
throw new SQLiteException("Can't upgrade read-only database from version " +
db.getVersion() + " to " + mNewVersion + ": " + mName);
}

db.beginTransaction();
try {
if (version == 0) {
// 如果版本为0的时候初始化数据库,调用子类的onCreate方法。
onCreate(db);
} else {
//处理升降级
if (version > mNewVersion) {
onDowngrade(db, version, mNewVersion);
} else {
onUpgrade(db, version, mNewVersion);
}
db.setVersion(mNewVersion);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}

onOpen(db);

if (db.isReadOnly()) {
Log.w(TAG, "Opened " + mName + " in read-only mode");
}

mDatabase = db;
return db;
} finally {
mIsInitializing = false;
if (db != null && db != mDatabase) {
db.close();
}
}}
}

greendao的缓存到底是如何实现的呢?

DaoMaster构造方法中会把所有的Dao类注册到Map中,每个Dao对应一个DaoConfig配置类。

1
2
3
4
protected void registerDaoClass(Class<? extends AbstractDao<?, ?>> daoClass) {
DaoConfig daoConfig = new DaoConfig(db, daoClass);
daoConfigMap.put(daoClass, daoConfig);
}

DaoConfig是对数据库表的一个抽象,有数据库实例、表名、字段列表、SQL statements等类变量,最重要的是IdentityScope,它是GreenDao实现数据缓存的关键。在DaoSession类初始化的时候IdentityScope初始化,可以根据参数IdentityScopeType.Session和IdentityScopeType.None来配置是否开启缓存。

IdentityScope接口有两个实现类,分别是IdentityScopeLong和IdentityScopeObject,它们的实现类似,都是维护一个Map存放key和value,然后有一些put、get、remove、clear等方法,最主要的区别是前者的key是long,可以实现更高的读写效率,后面的key是Object。

判断主键字段类型是否是数字类型,如果是的话则使用IdentityScopeLong类型来缓存数据,否则使用IdentityScopeObject类型。

1
keyIsNumeric = type.equals(long.class) || type.equals(Long.class) || type.equals(int.class)|| type.equals(Integer.class) || type.equals(short.class) || type.equals(Short.class)|| type.equals(byte.class) || type.equals(Byte.class);
1
2
3
4
5
6
7
8
9
10
11
12
13
public void initIdentityScope(IdentityScopeType type) {
if (type == IdentityScopeType.None) {
identityScope = null;
} else if (type == IdentityScopeType.Session) {
if (keyIsNumeric) {
identityScope = new IdentityScopeLong();
} else {
identityScope = new IdentityScopeObject();
}
} else {
throw new IllegalArgumentException("Unsupported type: " + type);
}
}

缓存的使用

数据读取

以Query类中list方法为例,跟踪代码可知,最后会调用AbstractDao的loadCurrent方法,它首先会根据主键判断dentityScope中有没有对应的缓存,如何有直接返回,如果没有才会读取Cursor里面的数据。

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
final protected T loadCurrent(Cursor cursor, int offset, boolean lock) {
if (identityScopeLong != null) {
if (offset != 0) {
// Occurs with deep loads (left outer joins)
if (cursor.isNull(pkOrdinal + offset)) {
return null;
}
}
//读取主键
long key = cursor.getLong(pkOrdinal + offset);
//读取缓存
T entity = lock ? identityScopeLong.get2(key) :identityScopeLong.get2NoLock(key);
if (entity != null) {
//如果有,直接返回
return entity;
} else {
//如果没有,读取游标中的值
entity = readEntity(cursor, offset);
attachEntity(entity);
//把数据更新到缓存中
if (lock) {
identityScopeLong.put2(key, entity);
} else {
identityScopeLong.put2NoLock(key, entity);
}
return entity;
}
} else if (identityScope != null) {
K key = readKey(cursor, offset);
if (offset != 0 && key == null) {
// Occurs with deep loads (left outer joins)
return null;
}
T entity = lock ? identityScope.get(key) : identityScope.getNoLock(key);
if (entity != null) {
return entity;
} else {
entity = readEntity(cursor, offset);
attachEntity(key, entity, lock);
return entity;
}
} else {
// Check offset, assume a value !=0 indicating a potential outer join, so check PK
if (offset != 0) {
K key = readKey(cursor, offset);
if (key == null) {
// Occurs with deep loads (left outer joins)
return null;
}
}
T entity = readEntity(cursor, offset);
attachEntity(entity);
return entity;
}
}

数据删除
我们经常使用DeleteQuery的executeDeleteWithoutDetachingEntities来条件删除数据,这时候是不清除缓存的,当用主键查询的时候,还是会返回缓存中的数据。

Deletes all matching entities without detaching them from the identity scope (aka session/cache). Note that this method may lead to stale entity objects in the session cache. Stale entities may be returned when loaded by their primary key, but not using queries.

使用对象的方式删除数据的时候,比如deleteInTx()等面向对象的方法时,会删除对应的缓存。在AbstractDao中deleteInTxInternal方法里面,会调用identityScope的remove方法。

1
2
3
if (keysToRemoveFromIdentityScope != null && identityScope != null) {
identityScope.remove(keysToRemoveFromIdentityScope);
}

数据插入

以insert方法为例,它会在插入成功之后,调用attachEntity方法,存放缓存数据。

1
2
3
4
5
6
7
8
9
10
protected final void attachEntity(K key, T entity, boolean lock) {
attachEntity(entity);
if (identityScope != null && key != null) {
if (lock) {
identityScope.put(key, entity);
} else {
identityScope.putNoLock(key, entity);
}
}
}

数据更新

数据update的时候也会调用attachEntity方法。

缓存带来的坑和脱坑方案
1.触发器引起的数据不同步
我们在项目中有这么一个需求,当改变A对象的a字段的时候,要同时改变B对象的b字段,触发器代码类似如下。

1
2
3
String sql = "create trigger 触发器名 after insert on 表B "
+ "begin update 表A set 字段A.a = NEW. 字段B.b where 字段A.b = NEW.字段B.c; end;";
db.execSQL(sql);

b是A的外键,映射到表B的b字段。

这样设置触发器之后,更新表B数据的时候,会自动把更新同步到表A,但是这样其实没有更新表A对应DAO的缓存,当查询表A的时候还是更新前的数据。

解决方案:
1.在greendao2.x版本中,可以暴露DaoSession中对应的DaoConfig
,然后调用daoConfig.clearIdentityScope();在3.x版本中可以直接调用dao类的detachAll方法,它会清除所有的缓存。 同时也可以调用Entity的refresh方法来刷新缓存。

1
2
3
4
5
public void detachAll() {
if (identityScope != null) {
identityScope.clear();
}
}

上面的方法都是通过清除缓存来保证数据的同步性,但是频繁的清除缓存就大大影响数据查询效率,不建议这么使用。

2.尽量不要使用触发器,最好使用greenDao自带的一些接口,绝大部分情况下都是能满足要求的。对于能否使用触发器,开发者做了解释。

greenDAO uses a plain SQLite database. To use triggers you have to do a regular raw SQL query on your database. greenDAO can not help you there.

2.自定义SQL带来的数据不同步问题
项目中即使使用了GreenDao,我们还是免不了使用自定义的sql语句来操作数据库,类似下面较复杂的查询功能。

1
2
3
String sql = "select *, count(distinct " + columnPkgName + ") from " + tableName + " where STATUS = 0" + " group by " + columnPkgName
+ " order by " + columnTimestamp + " desc limit " + limitCount + " offset 0;";
Cursor query = mDaoMaster.getDatabase().rawQuery(sql, new String[] {});

这种查询语句除了没有使用GreenDao的缓存,其它倒是没有什么问题。但是一旦使用update或者delete等接口时,就会引起数据的不同步,因为数据库里面的数据更新了,但是greenDao里面的缓存还是旧的。

总结:使用第三方库的时候,最好能够深入理解它的代码,不然遇到坑了都不知道怎么爬出来,像greendao这种,由于自己不合理使用导致的问题还是很多的。

h5实现视频自动播放

最近项目中,需要用到网页视频自动播放功能,PC端没有问题,移动端无法自动播放。针对这个场景和网上资料查阅,分析各个方案优缺点,方便其他相似需求开发人员。

video标签说明

  • src: 设置显示视频路径
  • controls: 显示控制栏
  • loop: 控制视频循环播放
  • autoplay: 自动播放
  • muted: 设置静音播放
  • playsinline: 不全屏播放

PC端自动播放

1
2
3
4
5
6
7
8
<video id="video"
x5-video-player-type="h5" controls autoplay
controlsList="nofullscreen" playsinline
style="width: 100%; height:300px">
<source src="/video/test.mp4" type="video/mp4">
<source src="/video/test.webm" type="video/webm">
<p>Your browser doesn't support HTML5 video. Here is</p>
</video>

通过指定autoplay属性,即可实现自动播放

移动端自动播放分析

为了避免浪费流量和电池功耗损失,移动端浏览器禁止了自动播放功能,需要用户主动点击才会播放。

youtubo视频网站自动播放

针对视频访问量大网站,浏览器厂商有个白名单机制,放入白名单的域名,就能自动播放。参考链接https://blog.google/products/chrome/improving-autoplay-chrome/

设置autoplay、muted和playsinline

对video标签进行如下设置:

1
2
3
4
5
6
<video id="video" muted autoplay playsinline 
style="width: 100%; height:300px">
<source src="/video/test.mp4" type="video/mp4">
<source src="/video/test.webm" type="video/webm">
<p>Your browser doesn't support HTML5 video. Here is</p>
</video>

各个浏览器厂商表现不一样,具体如下:

平台/浏览器 自带 微信
iOS >= 10.0(关闭低功耗开关)
iOS < 10.0
Android
华为
PC

微信App自动播放(不点击播放器按钮)

在微信App中,h5通过集成微信sdk,并监听SDK初始化桥事件,然后开启播放器播放。设置如下:

  1. 设置SDK
1
2
3
4
5
6
7
8
9
10
11
<script src="//res.wx.qq.com/open/js/jweixin-1.0.0.js"></script>
<script>
window.wxInited = false
document.addEventListener('WeixinJSBridgeReady', e => {
const video = document.getElementById('video')
if (video) {
video.play()
video.pause()
}
}, false)
</script>
  1. 设置video标签
1
2
3
4
5
6
7
<video id="video" x5-video-player-type="h5"
controls controlsList="nofullscreen"
playsinline style="width: 100%; height:0px">
<source src="/video/test.mp4" type="video/mp4">
<source src="/video/test.webm" type="video/webm">
<p>Your browser doesn't support HTML5 video. Here is</p>
</video>

通过上述设置,播放器处于可播放状态,后续非人工触发play或pause操作,都可以操作播放器。

各个浏览器厂商表现一致,具体如下:

平台/浏览器 自带 微信
iOS
Android
华为
PC

人工触发事件(不点击播放器按钮)

除了点击播放器的播放或暂停按钮,可以触发播放操作。用户主动点击(click)事件或滑动(touchstart、touchmove、touchend)事件,可以触发播放器播放操作。设置如下:

1
2
3
4
5
6
7
8
document.addEventListener('click', function () {
const video = document.getElementById('video')
if (video) {
video.play()
}
},
{ capture: true, passive: true }
)

各个浏览器厂商表现一致,具体如下:

平台/浏览器 自带 微信
iOS
Android
华为
PC

非视频降级方案

根据上述的分析,每种方案都有些不足,并不能解决所有平台和浏览器的问题。针对静音的视频环境,可以把视频转化为图片,按照一定的周期播放图片,这样所有的平台都支持。

这里参考了「张鑫旭大佬」的方案,实现原理如下:

  1. 图片DOM对象预加载,放在内存中;
  2. 播放开始,页面append当前图片DOM,同时移除上一帧DOM图片(如果有),保证页面中仅有一个图片序列元素; 对,很简单,没什么高超的技巧,但就是这种实现策略,对页面的开销是最小的,最终运行体验是最好的。
1
2
3
4
5
6
7
8
<div class="page" style="background-image: url(&quot;https://website.didiglobal.com/dist/img/homepage1-app.fb9b0c10.jpg&quot;);">
<div class="home-video-wrap">
<div class="video-app-container">
<img src="https://website.didiglobal.com/dist/img/合成 1_00315.68c3cb63.jpg">
</div>
<div class="mask-whole"></div>
</div>
</div>

图片轮播demo,参考滴滴移动官网

视频转为图片

方案如下:

  1. 电脑打开premiere cc 2017,导入视频编辑好。 img
  2. 编辑好视频后,按导出快捷键Ctrl+M调出导出页面,然后格式选择JPEG格式。 img

3.点击输出名称选择保存路径。

img

4.设置好格式和保存路径后,点击底部的导出,等待图片的导出就可以了。 img

移动端自动播放兼容方案

自动播放需要满足几个条件所有移动端手机都可以自动播放:

  1. 必须要有用户点击事件touch
  2. 接收点击事件的标签和video标签不能是父子关系(就是body接收的点击事件调用video的play方法,播放器也是不会播放视频的
  3. 接收点击时候后,立即执行play()方法,网络请求后调用play()方法可能会无效

因此,页面初始化话的时候写一个video标签,这个video标签在可见区域之外,video标签和需要点击事件控件不是父子关系,当用户点击播放按钮时,立刻调用play()方法。设置video标签的样式到可见区域内,通过z-index控制层级关系,就能实现自动播放。

根据上述方案分析,选择合适自己的使用场景,同时做好兼容,如

  1. 当不支持自动播放时,通过文案提示或手势引导,提示用户点击播放

参考说明

PC端、移动端Video自动播放兼容完美解决方案(IOS、安卓、微信端)
移动端视频 video 的 Android 兼容,去除播放控件、全屏等