下面的例子通过 WithTimeout
创建了一个带有超时的 context
。在后面一个阻塞函数任务执行超时后,取消任务继续执行。
1 | package main |
通过 ctx.Err()
可以捕获到一个 Context Deadline Exceeded
异常。当 ctx.Done()
,被执行后,就可以捕获到并通过 errors.Is()
来判断这个异常是否为 context.DeadlineExceeded
:
1 | if errors.Is(err, context.DeadlineExceeded) { |
Context Deadline Exceeded
常用在 HTTP
请求的上下文。设置截止日期或超时设置(即请求应中止的时间),并捕获在 Go
中发生的错误。如果服务器响应的时间大于设置的超时时间,则返回此错误。为请求设置超时是生产环境中的一个好习惯,可确保您始终在有限时间内获得响应(或错误)。
1 | package main |
1 | 2021/08/19 06:39:09 ContextDeadlineExceeded: true |
ubuntu
源可以直接使用 apt install nginx
,安装完成后需要防火墙放行相应端口。
1 | firewall-cmd --permanent --add-service=https |
监听 443 端口,然后配置长连接代理到内部端口上:
1 | server { |
配置 ssl
证书:
1 | ssl_certificate /etc/ssl/certs/server.crt; |
1 | # 生成自签名根证书(即顶级CA) |
使用OpenSSL工具制作X.509证书的方法及其注意事项总结
Nginx 带有重定向选项,配置监听 80
端口,然后重定向到 443
即可。
1 | server { |
为了精简镜像,一般会加载两个镜像,第一层是编译环境,来根据源码编译出可执行的二进制文件。然后拷贝可执行文件到 scratch
镜像中,作为最终的镜像:
1 | FROM xxx:lastest AS builder |
Golang
语言程序编译时会将所有必须的依赖编译到二进制文件中,但也不完全使用的是静态链接,因为 Golang
的某些包是依赖系统标准库的,例如使用到 DNS
解析的包。只要代码中导入了这些包,编译的二进制文件就需要调用到某些系统库,Golang
中使用 CGO
,以允许 Golang
调用 C
代码,这样编译好的二进制文件就可以调用系统库。
也就是说,如果 Go
程序使用了 net
包,就会生成一个动态的二进制文件,没有那些动态链接库,镜像就不能够正常工作。
会出现以下的错误:
1 | exec user rocess caused "no such file or directory" |
如果想让镜像能够正常工作,有几种方法:
禁用 CGO
:过设置环境变量 CGO_ENABLED=0
来禁用 CGO
,此时程序会使用内置的实现来替代系统库(例如使用内置的 DNS
解析器)。这种情况下生成的二进制文件是不依赖外部动态链接库的,可以通过 ldd
命令查看。
静态链接:通过添加编译选项 -ldflags "-extldflags '-static'"
实现编译时,把需要的链接库都打包,这样生成的二进制文件就是静态链接的,可以直接在 scratch
中执行。
必须将需要的库文件复制到镜像中;
直接使用 busybox:glibc
镜像;
Golang
的 DNS
解析对协程支持很好, 即 DNS
解析时不会阻塞执行线程,只会阻塞当前协程。
根据官方文档中关于域名解析的描述:
域名解析会间接调用 Dial
函数或者直接使用 LokupHost
和 LookupAddr
函数。不同操作系统有不同的实现方式。在 Unix 类系统中有两种方法进行域名解析:
1) 纯 Golang
语言实现的域名解析:从 /etc/resolv.conf
中取出本地 dns server
列表,然后发送 UDP
报文(DNS请求) 来获得解析结果;
2) 使用 CGO
实现,调用到 C
标准库的 getaddrinfo
或 getnameinfo
函数(不建议使用,因为对协程不友好)
关于 cgo dns 解析的坑 参照以下链接:
https://jira.mongodb.org/browse/MGO-41
https://github.com/golang/go/issues/8602#issuecomment-66098142
默认会使用纯 Golang
实现的域名解析。但是会有一种情况,程序必须打开 CGO
编译且使用静态链接 (通过 -ldflags "-extldflags '-static'"
来实现静态链接)。这时候就会出现程序静态链接了 CGO
的 DNS
解析库,打包进 scratch
之类的镜像之后,就会出现解析域名服务失败的问题。
编译的时候,指定标签 -tags='netgo'
,来使用指定使用内置纯 Golang
实现的 netgo
库。
导出 rootfs 的时候,出现一个问题,sudo
出现权限错误 /usr/bin/sudo must be owned by uid 0 and have the setuid bit set
。
查看这个文件的属性,会看到 sudo
属性缺少了 SUID
,而一个正常的 sudo
文件应该的权限如下:
1 | ls -l /usr/bin/sudo |
上面的命令显示,sudo
属于 root/root
,所有用户都拥有可执行的权限,同时设置了 Set UID (SUID)
;简单来说就是在执行时具有文件所有者的权限。
所以切到 root
账号,修改文件权限即可:
1 | chmod 4751 /usr/bin/sudo |
SUID/SGID/SBIT
权限设置与一般rwx
属性的设置类似:
并且也可以通过 ls -ll
来查看属性设置
SUID
仅可在二进制文件上使用。
任何用户在此目录下创建的文件,所属 用户组
都和目录所属 用户组
相同;
在拥有 SBIT
的目录下,用户若在该目录下拥有 w
及 x
的权限,则当用户在该目录下创建的子目录或者文件,也只有拥有者与 root
有权限删除。
up
1 | ifconfig |
查看不到网卡设备,加参数 -a
后可以显示,说明网卡的驱动正常加载,但是没有被启用
1 | ifconfig -a |
nmcli
没有 connection
先使用 ifconfig
启用网卡:
1 | ifconfig eth0 up |
然后使用 nmcli
查看链接,会发现不存在:
1 | nmcli connection show |
并且当查看 NetworkManager 管理的网卡设备, 会发现这些网卡设备都是 unmanaged
:
1 | nmcli d |
用图形化界面查看也是一样,因为后端 NetworkManager
没有管理这些网卡设备:
检查未管理的接口是否出现在 /etc/network/interfaces
中。默认情况下,NetworkManager
不管理出现在 /etc/network/interfaces
中的接口。
1 | sudo vim /etc/NetworkManager/NetworkManager.conf |
将 managed=false
行更改为 managed=true
,然后保存、停止和启动网络管理器:
1 | sudo service network-manager restart |
或者,从 /etc/network/interfaces
中删除该接口:
1 | # 备份当前接口文件: |
Ubuntu
会安装一个配置文件,该文件将大多数设备设置为非托管:
1 | cat /usr/lib/NetworkManager/conf.d/10-globally-managed-devices.conf |
要禁用此功能,可以在以下位置创建一个具有相同名称的空白文件 /etc
:
1 | sudo touch /etc/NetworkManager/conf.d/10-globally-managed-devices.conf |
此时有线和无线网卡会被 NetworkManager
接管。
原生的 Ubuntu20.04.5-base
使用 sudo
会出现无法解析主机的问题。通常,在更改系统的主机名之后也会发生此错误。
主机名是用于标识网络上设备的标签。 您不应该在同一网络上拥有相同主机名的计算机。主机名分别是以下三类:
static
主机名,即传统的主机名。 主机名存储在 /etc/hostname
文件中,可以由用户设置。
pretty
主机名用于向用户展示的自由格式UTF8字符编码的主机名。 例如 Zou's WorkStation
。
transient
主机名由内核维护的动态主机名。 DHCP或mDNS服务器可以在运行时更改临时的主机名。 默认情况下,它与static主机名相同。
对于
static
和transient
名称,例如host.example.com
,建议使用完全限定的域名FQDN
。
执行以下 hostnamectl
命令。在此示例中,默认主机名为 localhost.localdomain
:
1 | hostname |
1 | hostnamectl |
在 Ubuntu
和其它使用 systemd
作为初始化的程序的 Linux
发行版中,可以使用 hostnamectl
命令更改系统主机名。hostnamectl
命令语法如下:
1 | sudo hostnamectl set-hostname host.example.com |
例如,要将系统 static
主机名更改为 host.zhzh.xyz
,可以使用以下 hostnamectl
命令:
1 | sudo hostnamectl set-hostname host.zhzh.xyz |
要将 pretty
主机名设置为 Zou's WorkStation
,请输入:
1 | sudo hostnamectl set-hostname "Zou's WorkStation" --pretty |
hostnamectl
命令不产生任何输出。 成功时返回 0
,否则返回非零失败代码。要验证主机名是否已成功更改,再次运行 hostnamectl
命令查看即可。
接下来,运行以下命令来错误依然会存在:
1 | sudo |
简单来说,hostname 命令无法解析系统的主机名。由于这是一台本地计算机,并且在 DNS 中没有这样的记录。
为了修复错误,需要在系统本地添加 DNS
记录。本地 DNS
记录存储在 /etc/hosts
中。
首先,使用以下命令登录到 root
用户:
1 | su - root |
接下来,编辑 /etc/hosts
文件的内容:
1 | vim /etc/hosts |
添加以下内容:
1 | 127.0.0.1 localhost.localdomain localhost |
默认系统中是连 localhost
这样的记录都不存在的。所以由于缺少主机名并且系统无法找出主机名,因此会引发“无法解析主机”的错误。
要修复此错误,在 /etc/hosts 文件中,将主机名设置为环回地址 (127.0.0.1)即可修复这个错误。
]]>基于 rk3568
芯片编译了一个 debian
系统上,想要配置静态 IP
,直接在 /etc/network/interfaces.d/
下创建了文件,然后编写了配置:
1 | auto eth0 |
结果使用 ip addr
查看的时候,发现 eth0
下存在两个 inet
。尝试内网 ping
这个 ip
是通的,但是访问外网的时候发现哒咩。
猜测,要么就是配置文件写错了,要么就是有内鬼……
尝试把 auto eth0
关掉,还是不行。再检查了一下配置文件,觉得没错;
怀疑是 NetworkManager
自作主张,给整出来的幺蛾子,直接用 systemctl disable
了,还是不行;
stack overflow
上有老哥说要去 /etc/sysconfig/network
下修改网卡配置,显然不是同一个发行版,别说文件了,连那个目录都不存在;
于是怀疑是否是 dhcpcd
这要老哥给整的,用 systmctl status dhcpcd
,实锤~~。log
里明明白白写着 rebinding
然后就从同一个冲突域下的另一个路由器上拿了个 ip
过来。
PS. 以后开发环境的网络也得小心,能用三层交换机隔离开就别拿个二层路由凑合,前前后后折腾将近1h才解决……
以后还是用 nmcli
来配置网络好些,用这个工具可以直接看到当前网卡上的配置,以及查看修改后配置是否生效,这次就是默认走了dhcp拉回来的那个,导致访问不了外网。
1 | nmcli connectiion show eth0 |
任何一门编程语言写出的第一个程序几乎都是 Hello World
,这在编程界已经成为经典了。本文就从这个经典开始,了解 HarmonyOS
的应用开发。
在欢迎界面点击新建项目,选择 Java
的 Empty Ability
新建,填入包名等参数。这里的 Ability
是指应用所具备的能力的抽象,一个应用可以包含一个或多个Ability。Ability分为两种类型:FA(Feature Ability)和PA(Particle Ability)。FA/PA是应用的基本组成单元,能够实现特定的业务功能。FA有UI界面,而PA无UI界面。
目前 DevEco Studio 2.1
版本已经支持自动签名,需要注册华为开发者账号,然后在开发者管理中心创建项目和应用。详情可以查看鸿蒙文档
需要注意的是其中应用的包名必须与 config.json
文件中的 bundleName
一致。
之做自动签名的时候已经将手机和电脑连接,现在顶部右侧工具栏中会显示已经连接的手机,点击右边的运行按钮,稍微等待一会,项目就会运行到手机上了。返回手机桌面会看到应用图标,同时长按应用还可以添加一张服务卡片到桌面上。
左侧文件管理器,显示了用 Java
编写 HarmonyOS
应用的项目结构。
.gradle
和 .idea
这两个目录下放置的都是 DevEco Studio
自动生成的一些文件
entry
默认启动模块,是项目的主模块。之后应用程序的代码、资源等内容都放置在这个目录下
EntryCard
服务卡片的图片按照包名放置在这个目录下
build
这个目录主要包含了一些编译时自动生成的文件
gradle
这个目录下包含了 gradle wrapper
的配置文件,使用 gradle wrapper
的方式不需要提前将 gradle
下载好,而是会自动根据本地的缓存情况决定是否需要联网下载 gradle
.gitignore
这个文件是用来将指定的目录或文件排除在版本控制之外的
build.gradle
这是项目全局的gradle
构建脚本,里面会包含之前自动签名的配置信息和其他编译选项,通常是不需要修改的
gradle.properties
这个文件是全局的gradle
配置文件,在这里配置的属性将会影响到项目中所有的gradle
编译
gradlew
和 gradlew.bat
这两个文件是用来在命令行界面中执行 gradle
命令的,其中gradlew
是在Linux
或Mac
系统中使用的,gradlew.bat
是在Windows
系统中使用的(不过当前 DevEco Studio
还没有推出支持 Liunx
运行的版本)
local.properties
这个文件用于指定本机中的HOS SDK
路径,内容是自动生成的
settings.gradle
这个文件用于指定项目中所有引入的模块。如果是新建的 HelloWorld
项目只有一个 entry
,那么该文件中也就只引入了 entry
这一个模块,比如 include ':entry'
。通常情况下,模块的引入是自动完成的
顶层目录中除了 entry
外,大多数文件和目录都不需要频繁做大量修改,甚至可以直接使用自动生成的。而 entry
才是之后开发的重点,为了方便之后的开发和学习,本次就先研究下其目录结构。展开之后的目录结构如下图所示:
build
这个目录和外层的 build
目录类似,也包含一些编译时自动生成的文件
libs
使用第三方 .jar
包,就需要把这些 jar
包都放在 libs
目录下,放在这个目录下的 jar
包会被自动添加到项目的构建路径里
ohosTest
编写 HarmonyOS Test
测试用例,用于项目的自动化测试
java
放置 Java
代码的地方,展开目录会看到系统自动生成的 MyApplication
文件和 MainAbility
文件
res
项目的 UI
布局、图片、国际化字符串等资源都存在这个目录下。由于内容很多,所以下面还分了很多子目录,比如 resources\base\layout
用于存放布局文件
config.json
这个是整个 HarmonyOS
项目的配置文件,由app
、deviceConfig
和 module
三个部分组成。包含应用全局配置,以及应用包含的各类 Ability
的配置。还有类似于 AndroidManifest.xml
的权限声明。
test
也是一种项目自动化测试
build.gradle
这是 entry
模块的 gradle
构建脚本,这个文件中会指定很多项目构建相关的配置
proguard-rules.pro
这个文件用于指定项目代码的混淆规则,当代码开发完成后打包成安装包文件,如果不希望代码被别人破解,通常会将代码进行混淆,从而让破解者难以阅读。
这样整个项目的目录结构就基本了解了。接下来先分析下这个项目究竟是怎么运行起来的。
首先打开 config.json
文件,找到 module
对象,从中可以看到如下代码:
1 | "module": { |
每个HAP
的根目录下都存在一个config.json
配置文件,文件内容主要涵盖以下三个方面:
① 应用的全局配置信息,包含应用的包名、生产厂商、版本号等基本信息。
② 应用在具体设备上的配置信息,包含应用的备份恢复、网络安全等能力。
③ HAP
包的配置信息,包含每个Ability
必须定义的基本属性(如包名、类名、类型以及Ability
提供的能力),以及应用访问系统或其他应用受保护部分所需的权限等
另外上面这段代码还表示对 MainAbility
的注册,其中包含了配置图标(”icon”)、名称(”label”)、描述(”description”)和桌面卡片的配置。通常一个 HAP
里可能会包含多个 Ability
,但是只会有一个主 Ability
,在手机上点击应用图标,首先启动的就是这个 MainAbility
。
打开 MainAbility
文件,代码如下所示:
1 | public class MainAbility extends Ability { |
首先可以看到,MainAbility
是继承自 Ability
的。而 Ability
是 HarmonyOS
系统提供的一个基类,所有自定义的 Ability
都必须继承它或者它的子类才能拥有其特性。然后可以看到其中有一个 onStart()
方法,这个方法是被创建时必定要执行的方法。这里不编写 UI
,可以看到在 onStart()
方法的第二行调用了 super.setMainRoute(MainAbilitySlice.class.getName())
方法,引入了 MainAbilitySlice
,打开文件查看代码如下:
1 | public class MainAbilitySlice extends AbilitySlice { |
这里 MainAbilitySlice
继承自 AbilitySlice
,是指应用的单个页面及其控制逻辑的总和。其中用 super.setUIContent(ResourceTable.Layout_ability_main)
引入布局文件。打开路径在项目 entry > src > main > resources > base > layout
的 ability_main.xml
的这个文件,可以看到如下代码:
1 |
|
这里用到了 DirectionalLatout
这种布局,这是 HarmonyOS
中比较常用的一种布局方式,用于将组件按照水平或者垂直这一个方向排布。里面第一个组件是 Text
,也就是用来显示屏幕上的 你好,世界
。当然除了 Text
外,还有 Button
、Image
、Tab
等等各类基础组件可供使用,之后再慢慢学习研究吧。
Gradle
是一个非常先进的项目构建工具,它使用了一种基于 Groovy
的领域特定语言(DSL
)来进行项目设置,摒弃了传统基于 XML
(如 Ant
和 Maven
)的各种烦琐配置。HarmonyOS
项目中有两个 build.gradle
文件,一个是在最外层目录下的,一个是在 entry
目录下的。这两个文件对构建项目都起到了至关重要的作用,下面就来对这两个文件中的内容进行详细的分析。先看最外层目录下的 build.gradle
文件,代码如下所示:
1 | apply plugin: 'com.huawei.ohos.app' |
这些代码都是自动生成的,虽然语法结构看上去可能有点难以理解,但是如果忽略语法结构,只看最关键的部分,其实还是很好懂的。首先,两处 repositories
的闭包中都声明了:maven
、repo
和 jcenter()
这三个配置。它们分别对应了一个代码仓库,maven
、repo
仓库中包含的主要是华为自家的扩展依赖库,而 jcenter
仓库中包含的大多是一些第三方的开源库。声明了这三行配置之后,我们就可以在项目中轻松引用任何仓库中的依赖库了。接下来,dependencies
闭包中使用 classpath
声明了两个插件:一个 hap
插件和一个 decctest
插件。这两个插件都是为了构建项目必需的。
这样就将最外层目录下的 build.gradle
文件粗略分析完了,通常情况下,并不需要修改这个文件中的内容,除非想添加一些全局的项目构建配置。下面再来看 entry
目录下的 build.gradle
文件,代码如下所示:
1 | apply plugin: 'com.huawei.ohos.hap' |
这个文件中的内容就要相对复杂一些了,首先应用了两个 HarmonyOS
的插件,接着是一个 ohos
的闭包。在这个闭包中我们可以配置项目构建的各种属性。其中,compileSdkVersion
用于指定项目的编译版本。buildTypes
闭包中用于指定生成安装文件的相关配置,通常只会有两个子闭包:一个是 debug
,一个是 release
。debug
闭包用于指定生成测试版安装文件的配置,release
闭包用于指定生成正式版安装文件的配置。另外,debug
闭包是可以忽略不写的,因此上面的代码中就只有一个 release
闭包。下面来看一下 release
闭包中的具体内容,proguardEnabled
用于指定是否对项目的代码进行混淆,true
表示混淆,false
表示不混淆。rulesFiles
用于指定混淆时使用的规则文件,这里指定了 proguard-rules.pro
是在当前项目的 entry
目录下的,里面可以编写当前项目特有的混淆规则。
接下来还剩一个 dependencies
闭包。这个闭包的功能非常强大,它可以指定当前项目所有的依赖关系。通常项目一共有3种依赖方式:本地依赖、库依赖和远程依赖。本地依赖可以对本地的 jar
包或目录添加依赖关系,库依赖可以对项目中的库模块添加依赖关系,远程依赖则可以对 jcenter
仓库上的开源项目添加依赖关系。观察一下 dependencies
闭包中的配置,第一行的 implementation fileTree
就是一个本地依赖声明,它表示将 libs
目录下所有 .jar
后缀的文件都添加到项目的构建路径中。
CameraX
是 Google
为了简化 Android
的 camera
开发而提供的一个库. 这个库包含在 Jetpack
中. 在 2019 Google IO 大会
上发布了 alpha
版本, 并且在今年发布了 beta
版本.
本文介绍使用 CameraX
来预览摄像头图像, 拍照并分析来自相机的图像流
打开 Android Studio
新建一个 Android
项目. (语言选择 Kotlin
)
打开 build.gradle(Module: app)
文件, 添加 CameraX
的依赖:
1 | implementation fileTree(dir: "libs", include: ["*.jar"]) |
CameraX
需要一些 Java 8
的方法, 所以在 buildTypes
里要设置编译参数:
1 | compileOptions { |
修改完成后点击同步, 就可以在 app
中调用 CameraX
的依赖了.
先创建一个 values/strings.xml
文件:
1 | <resources> |
然后打开 layout/activity_main.xml
, 删除其中的 <Text/>
标签, 加上一个 Button
和一个 androidx.camera.view.PreviewView
(预览摄像头画面)
修改后代码如下:
1 |
|
用下面的骨架代码替换 MainActivity.kt
原有代码:
1 | import androidx.appcompat.app.AppCompatActivity |
骨架包括导入语句, 将要实例化的变量, 将要实现的函数和常量. onCreate()
已经实现了检查相机权限, 启动相机功能, 按钮的监听onClickListener()
, 并实现 outputDirectory
和 cameraExecutor
. 目前相机也将无法工作, 需要在之后实现那些方法.
现在的程序运行后可以看到:
在调用摄像头前, 需要获得摄像头访问权限
首先打开 AndroidManifest.xml
然后把下面的代码粘贴在 application
标签前面:
1 | <uses-feature android:name="android.hardware.camera.any" /> |
第一行使用 android.hardware.camera.any
可确保该设备具有摄像头, 而 .any
则意味着它可以是前置摄像头或后置摄像头.如果你没有使用 .any
, 马尔如果你在没有后置摄像头的设备上运行程序就不会工作
第二行才是真正 app
添加了访问该摄像机的权限
首先修改 allPermissionsGranted()
方法, 在骨架代码中这个方法默认返回 false
, 重写这个方法:
1 | private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all { |
在 MainActivity.kt
中重写 onRequestPermissionsResult
来获取权限请求的结果:
1 | override fun onRequestPermissionsResult( |
上面的代码首先使用:
1 | if (requestCode == REQUEST_CODE_PERMISSIONS) { |
调用上面修改的 allPermissionsGranted()
方法来检查请求代码是否正确, 如果不正确则忽略. 如果正确则检查用户是否授予了权限.
如果授权则启动摄像头:
1 | if (allPermissionsGranted()) { |
如果未授予权限就用 Tosat
通知用户未授予权限:
1 | else { |
现在重新运行程序, 首次打开的时候会在应用内请求访问摄像头的权限:
在得到访问摄像头的权限后, 会调用 startCamera()
方法来启动摄像头, 这里在 startCamera()
方法实现获取摄像头实例并把摄像头预览图像显示出来:
1 | private fun startCamera() { |
首先创建一个 ProcessCameraProvider
的实例:
1 | val cameraProviderFuture = ProcessCameraProvider.getInstance(this) |
这用于绑定摄像机的生命周期. CameraX
库接管了摄像头的生命周期, 因此使用的时候不需要关心打开和关闭相机.
然后在 cameraProviderFuture
添加一个监听:
第一个参数是 Runnable {}
, 第二个参数是 ContextCompat.getMainExecutor()
. ContextCompat.getMainExecutor()
将会返回一个在主线程上运行的 Executor
1 | cameraProviderFuture.addListener(Runnable {}, ContextCompat.getMainExecutor(this)) |
在上面地一个参数 Runnable
中, 添加 ProcessCameraProvider
, 用于把摄像头的生命周期绑定到应用程序的 LifecycleOwner
:
1 | val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() |
上面的代码已经实现了托管摄像头的生命周期, 接下来要实现摄像头画面预览就只需要实例化一个 Preview
对象, 并绑定到摄像头的生命周期内即可. 首先初始化一个 Preview
对象:
1 | preview = Preview.Builder().build() |
然后创建一个 CameraSelector
对象,然后使用它的 CameraSelector.Builder.requireLensFacing
方法调用你想访问的摄像头(CameraSelector.LENS_FACING_BACK
表示后置摄像头)
1 | val cameraSelector = CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build() |
最后编写一个 try-catch
代码块. 在该代码块内, 先使用 unbindAll
确保没有任何内容绑定到 cameraProvider
, 然后将前面的 cameraSelector
和预览对象 unbindAll
绑定到 cameraProvider
. 再将 viewFinder
的 SurfaceProvider
设置为 preview
的 SurfaceProvider
:
1 | try { |
运行程序, 你就可以看到摄像头捕获的预览画面:
在骨架代码中, 预留了一个按钮用于捕获图片. 在按钮点击后会运行 takePhoto()
方法, 所以只需要在该方法实现图像捕获的业务逻辑即可.
takePhoto()
方法首先获取 ImageCapture
对象, 如果获取不到则直接返回:
1 | val imageCapture = imageCapture ?: return |
然后创建一个 File
对象用于指定储存一会捕获到的图片的位置和文件名:
1 | val photoFile = File( |
创建一个 OutputFileOptions
对象用于制定输出. 这里需要将输出保存在我们刚刚创建的文件中, 因此刚才创建的 photoFile
:
1 | val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build() |
最后就可以调用 imageCapture
对象的 takePicture()
方法. 传入 outputOptions
, 执行程序 {...}
和保存图像的 callback
方法:
1 | imageCapture.takePicture( |
如果图像捕获失败或保存图像捕获失败:
1 | override fun onError(exc: ImageCaptureException) { |
如果图像捕获成功, 则照片保存到之前创建的文件中, 并打印保存路径:
1 | override fun onImageSaved(output: ImageCapture.OutputFileResults) { |
最后 takePhoto()
完整的代码如下:
1 | private fun takePhoto() { |
ImageCapture
对象并绑定到摄像头生命周期前面图像捕获的功能用到了 ImageCapture
, 所以之前的 Preview
一样, 要用相同的步骤把 ImageCapture
也绑定到生命周期:
在 startCamera()
方法的创建 Preview
代码下添加创建 ImageCapture
的代码:
1 | imageCapture = ImageCapture.Builder() |
然后在 cameraProvider.bindToLifecycle
中把上面的创建的 ImageCapture
实例绑定到摄像头生命周期上:
1 | camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture) |
现在运行程序, 点击按钮就会保存捕获图片, 并且你可以在本地图库找到保存的图片.
上面介绍了 CameraX
库进行摄像头预览和摄像头图片捕获的功能, 最后一个图像分析的功能和上面过程类似. 也是创建一个图像分析的对象, 然后绑定到摄像头生命周期里.
首先在 MainActivity
里添加一个内部类 LuminosityAnalyzer
用于分析图像的平均亮度:
1 | private class LuminosityAnalyzer(private val listener: LumaListener) : ImageAnalysis.Analyzer { |
ImageAnalysis
对象并绑定到摄像头生命周期图像分析对象的绑定方法和画面预览, 图像捕获对象绑定方法一样, 都是首先实例化一个对象. 在 startCamera()
方法中, 在实例化 ImageCapture
对象下面添加实例化 ImageAnnalysis
对象的代码:
1 | imageAnalyzer = ImageAnalysis.Builder() |
实例化 ImageAnnalysis
对象的时候, 传入了一个 LuminosityAnalyzer
对象用于分析i并打印图片平均亮度的结果.
最后在 cameraProvider.bindToLifecycle
方法里添加实例化的 ImageAnalysis
对象:
1 | camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture, imageAnalyzer) |
运行程序, 可以看到 logcat
中输出图片分析的结果:
要在 21
世纪获得成功, 就需要拥有超级学习者的技能. 在这个技术快速更新的时代, 依靠持续不断的自学才能保持对新模式, 技术和思想的掌握.
生活在高速发展的世界, 尽可能快速地学习和掌握新技能变得越来越有必要. 好消息是, 即便你有一份全职的工作, 你也不需要惊人的天赋才能学习新知识.
很多学识渊博的人(比如, 查尔斯·达尔文, 列奥纳多·达·芬奇和诺贝尔物理学奖得主理查德·费曼)都曾经表示自己并没有特别的天赋.
我们每个人都拥有足够的能力去学习一件新的事物. 只要使用正确的工具和学习技术, 我们可以学会几乎所有事物.
更好的学习方法可以使得学习过程变得非常享受.
PS. 学习本身是一件消耗能量, 逆人性的事情
关键是快速学习的技能并不复杂. 如果你有目标通过学习新技能提高你的职位, 下面有一些习惯可能会对你有用.
阅读是对你本身思想的一种锻炼. 它让你自动漫步在空间, 时间和历史当中, 然后提供给你一些深层次的视角, 想法, 概念, 情绪和知识体系.
你的大脑会在阅读时变得活跃, 根据你阅读的材料, 你的大脑会开始成长, 改变原有心智模式并与新的领域建立联系. 那些所有成就的人都会大量阅读.
实际上, 许多成功的人都分享了对阅读的欣赏, 他们不认为阅读是一件琐事, 而是一个改善生活, 职位和业务的机会.
埃隆·马斯克(Elon Musk
)的哥哥说,他每天长大读两本书. 比尔·盖茨每年读 50
本书。马克·扎克伯格(Mark Zuckerberg
)每两周读至少一本书. 沃伦·巴菲特(Warren Buffett
)每天花五到六个小时阅读五份报纸和500页公司报告.
在信息是新货币的时代里, 阅读是不断学习知识和获取更多这种货币的最佳来源.
学习是一段探索新知识的旅程, 永远没有终点.
这是一个令人享受的长期过程, 一段自我导向和驱动的探险旅程. 理解任何的论题, 想法或新的思维模式都不仅需要洞察力, 还需要发自内心的好奇.
IBM
的 Sonia Malik
写道: “学习之旅是正式和非正式学习资源的精选集合, 可用于获取特定位置或技术领域的所需的技能.”
学习是一项通常可以增加收入的投资. 如果您想在不断变化的世界中, 保持不可替代性, 学习将比以往任何时候都重要.
超级学习者重视学习的过程. 他们寻求持续的改进, 永远没有终点. 他们不断掌握新的原理, 过程, 世界观, 思维模式等. 对知识的“持续,自愿和自我激励”的追求对个人的成长至关重要.
培养成长心态是不会错的 – 是卡罗尔·德威克博士(Carol Dweck
)提出的一种学习理论. 它围绕着增加智慧, 提高能力展开.
“21世纪的文盲不会是那些不认识字的人, 而是那些不会学习的人.” 作家, 未来主义者和商人阿尔文·托夫勒(Alvin Toffler
)认为, 他的作品讨论现代技术而闻名.
培养成长和适应的开放心态可以帮助您更专注于人生中最重要的目标. 它可能会使您产生动力, 并使您更容易看到学习和发展能力的机会.
保持开放的心态, 掌握更多知识并在必要时运用知识的能力可以极大地改善您的生活和工作.
中国传统文化里讲七证修养: 知 止 定 静 安 虑 得. 这看似简单的一部, 实际上是一个很长的过程. 学习也是一个很长的过程. 学习是一种生活方式, 是一生的习惯, 对世界的探索也就是修身. 前两步修养, 所谓
正统
的解释是知道自己的社会位置, 不能逾越职位, 越权行事. 但是南怀谨的解释, 是个人更认可和推崇的, 他说与这两步修养其实是自觉能知之性与止息思想杂念. 因为这个世界是客观存在的, 不以人的意志转移, 但是人的意识是虚幻的, 不存在的. 包括我们的身体和物质的自我在内的所有现象都在不断地变动流转中. 我们通过思想东西具象化, 在我们的意识里塑造物质性实体的形象, 但这个形象是个虚幻的错觉. 我们只有秉承开放的, 成长的心态, 才有可能放下我们固有意识里的错觉, 从而窥探到真正的现实.
根据研究, 那些能把学到的东西给别人解释清楚或者马上实践的学习者, 能够保留大约 90%
他们学习到的知识.
把你认知的东西教给别人是一种最有效的方式去学习, 记忆和回忆新学习到的知识. 心理学上讲这个叫做 检索实践(retrieval pratice)
. 这是一种最可靠的办法去建立牢固的记忆.
通过简单地教别人一个议题来学习, 这样你就可以快速查明您的知识漏洞. 这是由著名物理学家理查德·费曼(Richard Feynman
)创造的一种心智模式.
费曼被誉为”伟大的解释者”, 因为他能够为几乎任何人清楚地说明诸如量子物理学之类的话题. 费曼学习法在詹姆斯·格里克(James Gleick
)的传记《天才:理查德·费曼的生活与科学》中有明确的阐述.
对您的知识的最终考验是您将知识转移到另一个人身上的能力. 学习, 处理, 记忆和回忆信息的更好方法是一半时间学习, 一半时间共享. 例如. 不要读完书, 而是要阅读 50%
的内容, 然后尝试回忆, 分享或写下您学到的关键思想, 然后再继续.
保证大脑健康可以使其大脑一直活跃. 您为大脑做什么或不做什么都会大大改变您的记忆, 处理和检索信息的方式. 每个人都希望尽可能长寿. 这个目标取决于大脑健康.
这意味着要吃很多与减缓认知能力下降相关的食物-蓝莓, 蔬菜(多叶蔬菜-羽衣甘蓝,菠菜,西兰花), 全谷类, 从鱼类和豆类中获取蛋白质以及选择健康的不饱和脂肪(橄榄油)而不是饱和脂肪(黄油).
“水果和蔬菜可以抵抗与年龄有关的氧化应激, 这种氧化应激会导致脑细胞的磨损. “, 精神病学和衰老教授加里·斯莫尔(Gary Small
)博士说.
如果我们不采取任何保护措施, 我们的大脑自然就会衰落. 但是, 如果您尽早进行干预, 则可以减缓衰弱过程-保护健康的大脑比尝试修复受损的大脑要容易得多.
休息对保留您选择学习的所有内容都至关重要. 根据最近的研究, 及早和经常短暂休息可以帮助您更好地学习, 甚至提高保留率.
“每个人都认为学习新事物时需要’练习 练习 练习’. 相反, 我们发现休息对于早期学习和实践而言同样至关重要”, 美国国立卫生研究院国家神经疾病与中风研究所高级研究员莱昂纳多·科恩(Leonardo G. Cohen
)博士说.
更好的休息有助于大脑在休息期间巩固记忆. 无论您选择学习什么, 优化休息间隔的时间以获得更好的结果都很重要.
路易斯安那州立大学学术成功中心的专家建议进行 30
至 50
分钟的课程. 学习策略研究生助理艾伦·邓恩(Ellen Dunn
)表示: “少于 30min
获得的信息量还远远不够, 但超过 50min
获得的信息就无法一次吸收您的大脑.”
我们大脑的神经网络需要时间去处理信息. 因此, 将学习间隔开有助于您更有效地记住新信息-给大脑足够的时间来休息和恢复.
]]>在你学习编程的过程中会有一个时刻, 一切都开发发生改变. 在 Firehose
, 我们常常称之为编程的拐点(inflection point of coding
). 在这个阶段之后, 作为一个开发人员, 你的各种行为都会变得不同. 经历过这个拐点之后, 你将能够自己编写程序解决问题, 而不再需要手把手的帮助. 这可能是一段痛苦的经历, 但是当你经历过后, 你将从中获得巨大的提升.
在 Firehose
, 我们的目标不仅仅是教你 Ruby
语言, 怎么编写 web
程序或这怎么编写测试脚本, 虽然我们也教你这些技能,但是我们最根本的目标是加速学生通过编程转折点, 从而使他们能够拥有解决现实问题的能力. 我们相信能自行解决问题是一项宝贵的能力, 并且这个教学方法也可以让你学习到更多的知识, 而不仅仅学习一些程序的编写.
当你开始学习编写程序的时候, 会有大量你并不知道的问题. 这些信息叫做 特定领域知识(domain-specific knowledge
). 比如: 知道怎么用 Ruby
语言编写一个循环或从数据库中提取数据等. 特定领域知识包括在一个特定的编程环境下所需要了解的全部内容.
成为一个独立的开发者第一步就是学习如何完成一个具体指定的任务. 当你能够完成一些任务, 你就会慢慢清楚这些代码如何在更大的范畴内组合. 随着时间的推移, 你会慢慢认识到设计模式的重要性, 那些一开始陌生和令人困惑的东西, 最后也会也会变得很好理解且易用.
PS. 从编写一段代码到写一个程序再到写一个软件, 就是范畴的不断增大
对于初学者来说, 这最重要的能力是关注细节
在查阅文档或者教程的时候, 关注细节是非常重要的. 即便是最小的拼写错误也可能会导致错误的信息或 bug
出现. 一开始看到错误信息是令人沮丧的, 但是这是整个学习过程中至关重要的一部分. 在这个阶段, 处理错误信息和问题能够帮助你掌握一个重要的编程技能: 关注细节.
根据错误信息进行调试是非常重要的, 因为处理错误信息本身就是编程的一部分. 无论是菜鸟还是老鸟在编程过程中都会面临错误信息. 唯一的区别是, 处理错误信息的经验越丰富解决问题就越快. 这主要是因为:
随着经验的积累, 你将会学到如何根据错误信息快速定位到问题的细节. 一开始你看到错误信息的时候, 你可能要花很多时间去发现问题所在. 但是当你看过成百上千的错误信息之后, 你就能熟练地根据错误信息快速定位到问题的细节了.
你应该从你解决过的每一条错误信息里学习. 在修复程序程序错误的时候, 更重要的事情是理解为什么会出现这样的错误. 从你遇到的每一个错误信息里学习, 下一次你遇到类似错误的时候就能够快速解决.
一开始遇到问题的时候, 你可能需要寻求外部的帮助. 随着经验的积累, 你会自己检查代码或者使用 Google
快速解决问题, 这样你寻求别人帮助的频率就会大大下降.
在接受指导阶段, 你会接受一系列的具体教学指引. 一开始, 你可能会发现跟上教学的进度并不那么容易, 错误信息此起彼伏. 随着时间推移, 你锻炼出了 debug
的能力, 并且越来越注重细节, 这个时候你跟上进度就变得更加容易. 当你完成了整个接受指导的阶段, 编写代码的速度会大幅上升.
在这个时候, 一些人觉得非常自信——好像他们已经不再需要接受系统性的指导并试着造一些东西——最后开心的掉入深渊而不自知. 另外一些人会读更多的教程, 试图获取更多的特定领域知识, 以达到“完全理解”的程度. 不幸的是, 教学指导只能带你走到这里, 仅靠教程和指导无法获得真正的自信. 真正的自信来自于面对一个你无法解决的问题, 经过一番挣扎之后最终独立解决问题的过程.
关于编程一个不可告人的小秘密是……
你永远不会完全知道解决问题所需要的一切知识. 你也许认为持续编程之旅, 总有一天你会学到一切需要学习的东西, 然后觉得编程索然无味. 想多了, 这一天永远不会到来.
PS. 选择了计算机编程, 就是选择了面向未知编程, 选择了一生逆流而上. 学习是逆人性的, 但是满怀好奇心去探索未知, 不断解决问题, 创造出精准的程序是令人兴奋的. 这就是编程的痛并快乐着.
编程是一种终身学习体验. 经验丰富的软件工程师对于他们尚未解决的问题孜孜不倦地寻求解决方案, 是因为这能让他们有机会学到更多的东西. 如果你期望某一天你会了解编程的一切, 记住, 那一天永远不会到来. 这不是一件坏事.
当你做好如下的准备时, 就可以开始下一阶段了:
你对错误信息已经习以为常, 因为你看过太多的错误信息了, 你可以迅速解码出错误信息的意义, 并且找到代码中出问题的地方
你已经非常擅长使用 Google
解决问题了. 当你要有一个新功能需求或者遇到了一个令人困扰的错误信息时, 你知道如何去搜索所需信息
你能够使用你在应用的其他部分编写的代码并遵循设计模式去解决问题, 而不总是寻求一步一步的说明/指南
拐点阶段是学习编程时最让人沮丧的阶段, 从各种角度来说, 这也是一个非常重要的阶段. 当你不再依靠教程, 开始解决没人提供完整解决方案的问题时, 这就是拐点阶段.
有时你会觉得你还没有准备好面对这个阶段, 可能想要回去根据详尽的说明和指南去编写程序. 不要沦为这种心态的牺牲品. 你觉得很受打击的原因可能是:
在拐点阶段, 你编程的速度可能是之前阶段的 1/10 ~ 1/20
你可能开始怀疑自己是否有能力成为一个程序员. 在这个阶段感到这些问题是正常的.
尽管你学习和编程的速度在下降, 但事实上, 你在完成最重要的事情. 当你在特定领域的知识足够丰富的时候, 你所学的东西实际上是过程性知识.
过程性知识是一种持续学习的能力. 当你需要实现一个新功能的时候, 你要用什么关键词搜索? 这个阶段很多你想要做意见事情的时候, 你会感到自己处于一片黑暗之中. 黑夜给了你黑色的眼睛, 你却用它寻找光明, 这是一种很重要的能力. 因为你永远不可能了解所有东西, 所以你必须教会自己如何自我学习去解决手头的问题.
大部分人没有意识到, 学习编程需要同时学习特定领域知识与过程性知识.
在人生的每一天里探索自我边界以外的东西
很多软件工程师一但找到自己的立足点, 就停留在自己的舒适区内. 这种程序员被叫做维护程序员——显然不是你应该成为的类型. 相反, 每一天你都应该试着做超越自我的事情. 大部分程序员辞职的原因是: 我已经解决了所有有趣的问题, 再也没什么挑战了.
PS. 在中国有一个, 程序员35岁中年危机的说法. 个人以为也是因为这些程序员丧失了探索未知的梦想, 信心和孩子气. 一个人开始废掉的标志之一就是习惯于待在自己的舒适区.
相比将代码和项目拉入到你的舒适区, 你更应该去尝试解决那些超出你的能力范围之外的问题, 这是习得和拓展新技能的唯一方法.
下边是一个越过了拐点的学员的感想:
我依然感觉到身处深渊, 但我感觉棒极了, 因为这就是我要呆的地方.
第一个拐点: Web
开发拐点, 出现在你能够创建任何基于数据库驱动的应用时. 这意味着你有能力创建一个拥有许多页面, 从一个简单的数据库进行存取数据的Web应用. Web开发者管这个叫做: “精通增删改查”. 在这个阶段, 你还可以通过阅读文档, Github
或者技术博客, 将一个第三方库(比如 Ruby Gem
)集成到自己的 Web
应用中.
第二个拐点: 算法与数据结构拐点, 是一个不那么明显的拐点, 但实际上更加重要. 已经征服了这个拐点的人, 会精通他们所用的编程语言, 了解编程的基础体系, 拥有面对复杂问题的深厚知识储备.
越过算法与数据结构拐点的人通常可以:
简要的说, 一旦你越过了这个拐点, 你就掌握了如何操作数据, 并且了解代码的性能影响. 大学里传统的计算机科学教育特别注重让学生越过算法与数据结构拐点. 但很多大学都使用在业界已经不流行的语言来教授这一点, 比如 Scheme
, Racket
和 LISP
.
在大部分技术面试中, 面试官都假设面试者已经越过了 Web
开发拐点, 因为这个拐点相对容易, 所以会将问题集中在评估面试者的算法和数据结构能力上. 通常面试问题会集中在上边提到的几个方面: 排序算法, 反转链表, 使用栈、队列和树.
一旦开发者同时越过了 Web
开发拐点和算法与数据结构拐点, 他就掌握了整个世界.
这些开发者能够解决两者兼备的挑战: 在复杂的高级Web应用内构建复杂的算法. 这是专业的Web开发者每天都要做的事情.
征服拐点之后, 你将明白的东西听上去有点违反直觉:
对于学习编程, 特定领域的知识在宏观角度并不重要.
我并没有开玩笑, 特定领域的知识确实没有那么重要. 一旦你通过拐点, 你会在一到两个星期, 甚至几天内理解这句话.
真正重要的东西是:
HR
希望开发者同时具备 Web
开发技能和算法技能
我在 Paypal
工作时, 我的团队招进来一个高级 Ruby Rails
开发工程师, 但是之前他没有任何 Rails
经验, 只有 Python
, LISP
和 Perl
经验. 仅仅在几天之内, 他已经给我们带来很大震撼. 几星期之后是巨大的震撼. 他迅速被提升为技术团队的领导, 也是我当时最成功的招聘决定之一.
不要盲目追求热点, 许多人说: “最近 Angular.JS
好热门”, “ JavaScript
上升势头猛烈”, “最近流行用…”. 我对此的回复是: “那么又如何呢?”. 当你开始学习编程时, 你唯一的目标是找到拐点然后越过拐点, 当你成功之后, 学习那些新的, 好玩的东西毫不困难.
自力更生, 拥有无需结构化的指导, 就可以学习新编程技能的能力, 意味着你无需等待别人帮助你. 对于你所需要学习的大部分知识, 你可以搜索互联网或者阅读各种材料.
PS. 自己去探索学习, 而不是学习别人写好指南, 整套告诉你的知识. 这样反而能够学到更多东西.
这并不意味着你可以迅速“了解”所有东西, 而是意味着现在所有的东西都是“可以去了解”的, 实际上, 你将所向披靡.
作为一个软件开发者, 最好的参考资料, 就是你编写过的类似代码. 你完全理解了你所编写的代码之后, 不必将所有的细节都记住. 当你开发一个新功能的时候, 首先问自己: “我之前做过类似的东西吗?” 如果答案是Yes, 看一下以前写过的类似代码, 在脑子里一行行的重放代码, 重新解释给自己看, 然后问自己: “我现在可以使用相同的方式解决问题吗?”
视频并不是学习特定领域知识的好材料, 因为视频通常耗费大量观看时间. 比如你想调用 Google Maps API
, 你亲自动手, 在 Github
上找到相关代码, 复制到一个新建的项目, 可能花费不到一分钟. 而通过视频指导, 看一遍可能需要10-30
分钟.
由于征服拐点是学习编程中最重要的事情, 所以你必须让自己尽可能顺利的越过这个阶段. 这意味着你必须在接受指导阶段就开始进行准备, 并在整个过程中保持正确的心态.
在接受指导阶段, 除了学习结构性的指导材料, 还要全程给自己一些挑战.
对于每一课, 尝试做超过教学范围的事情. 如果书上有“挑战”或者“自己努力完成”的问题, 把它们全部做掉. 解决没有指导的问题会带来无需结构化指导便能解决问题的重要经验.
尝试尽可能少的依赖教学材料. 在 Firehose
, 我们通常让学生通过文档了解如何集成一些gems或者实现一些代码. 对学生来说, 他们主要根据程序文档操作, 教学材料仅作为备查, 而不是单纯的依赖教学材料按部就班的学习. 注意, 文档会将阅读者视为已经跨越了拐点的开发者. 习惯了在 Github
上阅读文档, 对你自力更生是莫大的帮助.
关注要点与可复用性. 从头开始编写应用程序, 提交一个新应用到 Github
或者 Heroku
, 尽早构建数据库迁移.
越过拐点可能具有挑战性, 这里有一些建议值得尝试:
了解越过拐点是一个困难的过程, 别给自己太大压力, 并且设定现实的目标. 你不能将你在接受指导阶段的“超人”一般敲代码速度和这个阶段的蜗牛速度进行比较. 要记得你一直在学习很多东西, 在这个阶段, 你正在学习的是可以自己搞定全新事物的能力.
如果你自信心略有不足, 要知道你的感受是完全正常的. 继续努力, 如果你依然很挣扎, 试着和已经越过拐点的人交流一下, 也许他们会了解你现在所处的位置, 并且告诉你这种感受只是暂时的. 持续努力, 但不要过度劳累. 在学习编程的这个阶段, 你每天最多只能有效率的工作6个小时. 在疲劳的状态下学习只会延长你跨越拐点的时间.
这个阶段获取信心的最佳方式是解决自己遇到的疑难问题. 你的情绪可能会像过山车一样. 有时候你觉得你自己好像被放在火上烤, 但经过在一个问题上15小时的努力之后, 你可能有完全相反的感受.
如果你在某件事情上花费了5分钟到5个小时, 依然没有头绪, 确实很令人沮丧, 但每次你成功搞定一个问题, 实现一个功能, 那种自信源源不断涌现的感觉是你最需要的. 在不依靠帮助解决了很多困难的问题之后, 你会沉醉于在你的舒适区之外不断解决问题的感觉.
拐点过程的最后阶段是接受. 接受软件开发是一个持续学习的过程. 如果你感觉已经学习了所有东西, 只意味着你应该想一想如何解决更复杂的问题.
]]>原文地址: What is digital transformation? – The Enterprisers Project
数字化转型(digital transformation) 是将数字技术集成到业务的所有领域以实现根本上改变原有的运营模式和为客户创造价值模式. 这也是在一种要求组织以开放的心态不断观察和挑战现实, 不断实验原型并从原型中学习的组织文化变革.
在越来越多的关于世界数字化趋势和保持企业竞争力的主题演讲, 小组讨论和文章中越来越清楚地提出数字化转型对中小型企业到大型企业都非常重要. 但是很多企业的领导者都不清楚数字化转型的含义. 这就是一种吸引企业迁移到云的方式吗? 具体需要采取哪些行动? 我们需要创造一个新的职位或者雇佣咨询服务来帮助我们创建一套数字化转型的框架吗? 我们业务的战略需要作出哪些改变? 这件事情是否真的值得去做呢?
注意: 一些 leaders 可能认为 数字化转型(digital transformation)
这个术语已经被广泛使用了. 因为使用得太广泛, 所以这个术语没有太多帮助. 但是不论你喜欢不喜欢, 这个术语背后的业务要求(重新思考运营模式, 进行更多实验, 提高更加灵活地回应客户和竞争对手的能力) 这些都还远远没有被广泛使用.
这篇文章旨在向 CIO
和技术领导, 对数字化转型的一些常见问题的答案和经验, 提供清晰明确的说明. 由于技术随着组织的市场发展和不断提高业务价值的能力中起着至关重要的作用,因此 CIO
在数字化转型中起着关键作用.
还有一点值得注意的是不同组织处于数字化转型的不同位置. 如果你感到自己在数字化转型的过程中不知所措, 你并不知特例. 数字化转型中最困难的问题之一就是如何从愿景过度到执行. 它造成了焦虑:许多 CIO
和组织都认为, 在这种情况下, 他们在转型方面远远落后于同行.
PS. 个人认为<第五项修炼>中提到的 U形过程 对数字化转型有指导意义.
即便是已经在进行数字化转型的组织也持续面临艰难的障碍, 比如 预算, 人才竞争, 和文化变革. 让我们来为在数字化转型不同位置的组织提供一些建议.
因为数字化转型对于每一家组织都是不同的, 所以很难准确的给出适用于所有企业的定义. 然而总的来说, 我们把数字化转型定义为将数字技术集成到业务的所有领域中, 从而使得原有的运营模式和为客户创造价值的方式发生根本转变. 除此以外, 这是一种文化的变革. 这要求组织以开放的心态持续观察和改革现状, 不断实验并从中学习. 有时这意味着摆脱组织之前建立的长期业务流程, 转而采用仍在定义中的新的实践方法.
PS. 个人这个<第五项修炼>中的学习型组织有点类似
从大量的文章和关于数字化转型的各种定义中可以很容易看出为什么这个主题有些混乱. 举个例子, 作者格雷格·韦尔迪诺(Greg Verdino)给出的定义专注于经历数字化转型的企业期望实现的目标. 他说: “数字化转型弥合了数字客户的期望与模拟业务的实际之间的鸿沟.”
一个来自 Agile Elephant
(一家数字化转型咨询公司) 的定义强调了组织可能需要调整其现有实践的所有方式: “数字化转型涉及领导力的变革,思维方式的变革,鼓励创新和新的商业模式,资源数字化以及使用技术来改善组织的员工,客户,供应商,合作伙伴和利益相关者的体验.”
而Wikipedia的定义虽然含糊不清,但涉及到数字化转型的影响如何从企业扩展到整个社会。报告指出: “数字化转型是把数字技术的应用与人类社会各个方面相关联的变革”
考虑一下数字化转型在实践中将对您的组织意味着什么, 以及您将如何表述它. Bayer Crop Science
的高级副总裁/首席信息官(CIO
)兼数字化转型负责人吉姆·斯万森(曾是 Monsanto
的CIO)说: “数字化对许多人来说意味着很多不同的事情.” 他建议, 当您讨论数字化转型时, 先解释它的含义.
在 Monsanto
, Swanson
讨论了以客户为中心的数字化转型. 他说: “我们谈论的是关于人员和新的商业模式的自动化运营. 这包含了数据分析, 技术和软件. 但是所有这些都是使能因素, 而不是驱动因素. 核心是领导力和组织文化.” “您可能拥有这些东西: 客户需求, 产品和服务, 数据以及非常优秀的技术, 但如果领导力和文化不在核心位置, 那么它将失败. 了解数字化对您的组织(无论是金融, 农业, 制药还是零售机构)意味着什么是至关重要的.”
Korn Ferry
企业的北美和全球客户数字咨询部门负责人的梅利莎·斯威夫特(Melissa Swift)同意斯旺森的观点. “数字” 一词有问题, 因为这对很多不同的人人来说意味着很多不同的事情.
她说:” 对一个人说’数字化’, 一个人可能会想到无纸化;另一个人可能会想到数据分析和人工智能;另一个人可能会想到敏捷团队;而另一个人可能会想到开放式办公室.”
“‘数字’ 是一个烂摊子. 这会在组织中引起了很多悲伤”
她说:“想象一下,你一遍又一遍地点汉堡,却得到了从热狗到鸡肉三明治再到沙拉……”
领导者在围绕数字化转型进行对话时需要充分意识到这一现实. 如何在不焦躁的情况下谈论这个话题, 可以阅读相关文章为什么人们总是讨厌数字化转型
组织可能会进行数字化转型有几个原因. 但到目前为止, 最可能的原因是他们必须这么做:对许多组织来说,这是一个生存问题.
霍华德·金(Howard King)在《卫报》的一篇投稿文章中这样说: “企业不会因选择而转型,因为它既昂贵又冒险. 当企业无法发展时,它们就会经历转型.”
先锋集团的首席信息官约翰·马坎特(John Marcante)也指出: “仅看 S&P 500(标准普尔500, 是一个由1957年起记录美国股市的平均记录, 观察范围达美国的500家上市公司)
. 根据美国企业基金会的数据, 1958年, 美国公司在该指数上的平均停留时间为 61
年. 到2011年, 已经有 18
年了. 如今, 大约每两周更换一次标准普尔公司. 技术推动了这一转变, 想要成功的公司必须了解如何将技术与战略融合.”
企业领导者已经大致了解了这一点, 并且正在相应地进行优先级排序. IDC
预测, 根据(IDC)全球半年度数字化转型支出指南, 到2022年, 全球在实现数字化转型的技术和服务上的支出将达到1.97万亿
美元. IDC
预测数字化转型支出将稳步增长, 2017年至2022年的五年, 年增长率将达到 16.7%
.
全球数字化转型战略研究总监肖恩·菲茨杰拉德说: “IDC
预测, 到2020年, 将有 30%
的 G2000
公司分配至少等于收入 10%
的资本预算在推动其数字化战略的发展上.” 随着企业高管逐渐认识到数字化转型是一项长期投资, 资金投入是一项重要的工作. 这种为DX投入资金的承诺将继续推动未来十年的支出.
根据 Hackett Group
的研究, 截至2018年, 进阶分析(Advanced Analytics)已成为数字化投资的第一名, 很多企业计划在未来12到18个月内将进阶分析相关部署增加 75%
. 这特别包括了数据可视化工具和机器学习.
不同组织在数字化转型过程中处于不同的位置. 但是, 速度已成为所有组织的当务之急. IT领导者面临压力, 要求他们证明数字化计划将为整个组织带来敏捷性和速度的提高.
就像 Dion Hinchcliffe
在Constellation Research写道: “在当今快速发展的企业中的高管必须有想匹配的步伐, 不进则退。在当今的数字化时代里, 这是一个普遍存在的问题, 必须通过原型实验和探险来积极支持大胆的尝试行动. 而且这必须在管理每天不可避免的业务问题、服务交付和不可预测的令人分心的异常情况(如重大网络攻击或信息泄露)的日常工作中并行完成. 这些 CIO
必须既是一位精通技巧的杂耍演员, 又是一位来自一线的数字化领导者.”
改善客户体验已成为一个关键目标, 因此这也是数字化转型的关键部分. Hinchcliffe
将无缝的客户体验称为企业运作方式的最重要的区别因素
尽管数字化转型会根据组织的特定挑战和要求而千差万别, 但是在现有案例研究和已发布的框架中, 有一些不变和共同的主题是所有组织和技术领导者在着手进行数字化转型时应考虑的.
例如, 这些数字化转型元素经常被引用:
尽管每个指南都有自己的建议和不同的步骤或注意事项, 但 CIO
在制定自己的数字化转型策略时应寻找那些重要的共享主题.
数字化转型框架的一些示例包括:
麻省理工学院斯隆:数字化转型的九大要素
认知: 数字业务转型的框架
高度计: 数字化转型的六个阶段
离子学: 数字化转型分步指南
近年来, IT的角色已发生根本性转变. CEO
们越来越希望 CIO
们能帮助组织创造收入. 根据对超过 4,600
位 CIO
的 2018
年 Harvey Nash / KPMG CIO
调查, CIO
的首要业务是 “改善业务流程”. 但是在”数字化领导者”(被认为是表现最佳的公司)的 CIO
们的首要经营重点是 “开发创新的新产品”.
比起关于节省成本, IT
开始成为业务创新的主要驱动力. 拥抱这种转变需要公司中的每个人在他们的日常经历中重新考虑 IT
的角色和影响.
Equifax
首席技术官 Bryson Koehler
说: “当您将 IT
从一种运营模式中脱身时, 会有一种截然不同的心态: 从 ‘让我们运行一堆我们已经购买并持久使用的解决方案’ 到 ‘让我们来创建以前不存在的新功能’.”
“如果你看看绝大多数的创业公司,你会发现他们并不是以巨大的、包装完好的软件包作为公司的基础. 如果您想在大型企业内部进行创新, 那么您也不应该从运行现有解决方案开始. 你不是来运行主机的, 你也不是来运行服务器的. 您不该在这里运行数据中心、网络中心或运营. 这些是 table stakes
, 这是你可以外包的东西.”
尽管 IT
在推动数字化转型战略中将扮演重要角色, 但实施和适应数字化转型所伴随的巨大变化的工作却落在每个人身上. 因此数字化转型还是要解决人的问题.
IT
领导者会发现自己比以往任何时候都更多得在在跨职能团队中工作. 数字化转型计划通常会重塑工作组, 职位和长期的业务流程. 当人们担心自己的价值, 甚至他们的工作可能受到威胁时, IT
领导者会感到一些来自内部其他人的阻碍. 因此, 对领导力的 “软技能” (事实证明是相当困难的)的需求很大.
Mattel
执行副总裁兼首席技术官 Sven Gerjets
说, 领先的转型始于同理心. 他说: “当您的同情心真诚时, 就会开始建立信任.” “如果您没有一个能够支持并完全参与转型工作的组织, 那么成功是不可能的. 您需要让领导者知道 ‘好’ 的模样, 并激励他们帮助整个组织了解你为什么做你正在做的工作.”
“当您听到类似的信息时: ‘嘿,我们正在与您的团队一起工作,感觉很不一样’ 或 ‘我们无法相信它提前交付了这个项目,并且满足了我的业务需求’, 你就会明白这一点.”
负责北美和全球客户数字咨询业务的科恩·费里(Korn Ferry
)的斯威夫特(Swift
)在她的咨询工作中发现,三类员工趋向于减缓转型的势头:守旧的人, 照本宣科的人和独行侠.
她写道, 组织不能忽视这三个群体, 而必须与之接触,否则数字化转型将面临危险的停滞. 怎么做呢? 她的第一个建议是: 以一种细分的方式来考虑你的人口,并努力满足不同的人群. 怎么做?她的第一个建议:以细分的方式考虑不同的人, 并努力满足他们所在的不同群体.
她写道 “许多组织以高度统一的方式推出了数字化转型的旅程, 并在整个过程中部署了相同的消息和技术. 但是每个人都需要学习新技能!组建新队伍!迎接新世界!从 变革管理(change management)
的角度来看,在不同子群体用相同方式推动数字化转型, 纯粹是愚蠢之举,也是对投资资金的滥用. 这些资金本可以更有策略地投向规模较小的子群体. 公司应同时考虑其组织内不同子群体的数字化体验和行为偏好, 并且他们应精心设计消息传递, 程序甚至环境, 以便为不同的子群体找到正确的起点和现实可靠的终点.”
数字化转型重要的要素当然是技术. 但是通常情况下, 更重要的是淘汰过时的流程和传统的技术, 而不是采用新技术. Federal IT Dashboard 显示, 在财政年度2017年, 超过70%的政府 IT
支出的是为了运营和维护旧系统.
在医疗保健行业中, 尽管医疗保健提供商广泛使用智能手机和其他移动设备, 但”仍有近 80%(79.8%)
的临床医生继续使用医院提供的寻呼机, 其中 49%
的临床医生表示他们收到了与患者护理相关的消息最常见的是传呼机
诸如此类的例子涉及所有行业, 而传统技术的盛行阻碍了 CIO
成功实施数字化转型战略的能力. Forrester
的研究表明, CIO
平均将其预算的 72%
用于解决现有的 IT
问题, 而只有 28%
用于新项目和创新.
如果组织希望随着当今数字化变革的快速发展而发展, 则他们必须努力通过技术来提高效率.对于许多组织来说,这意味着在整个组织中采用敏捷原则. 自动化技术也可以帮助许多 IT
组织提高速度和减少技术债务.
正如《企业家》杂志的斯蒂芬妮·奥弗比(Stephanie Overby
)最近报道的那样, “跨行业的持续数字化转型在2019年已成定局. 与此同时, 数字化转型的疲软 也变得非常真实.” 现在是一个好机会去问自己和团队是否正在疲倦或参与度降低.
2020年将是数字化转型计划重要的一年, 那些继续低估文化变革需求的组织将会面临风险.
全球技术研究与咨询公司ISG的合伙人兼总裁史蒂夫·霍尔(Steve Hall
)说: “2020年仍将看到跨行业的数字化计划的迅速扩展. 在许多领域, CIO
和组织已经为组织进行变革做好了准备, 但是还没有完全转变其文化以充分接受数字化变革”.
以下是企业和IT领导者在2020年应注意的八个关键数字化转型趋势:
AI
和机器学习为了证明数字化转型工作的成功, 领导者需要量化投资回报率. 对于那些跨越职能和业务边界、改变公司走向市场方式、通常还会从根本上重塑与客户和员工互动的项目来说,这说起来容易做起来难.
诸如改造移动应用程序之类的项目可能会带来短期收益, 但其他项目则追求长期的商业价值.
此外, 正如我们最近报道的那样, “数字化转型工作正在进行且不断发展, 这可能会使传统的业务价值计算和财务治理方法变得无效.”
尽管如此, 成功的衡量对于持续投资至关重要. 管理咨询公司 Pace Harmon
的主管 Brian Caplan
说: “仅仅实施这项技术还不够, 该技术需要与监控消费者需求和业务流程有效性的关键性能指标相关联.”
“在确定数字化转型投资的绩效时, 最好是从投资组合的角度来看, 而不是从项目层面的角度来看. “ 数字化转型咨询和研究公司Everest Group
的合伙人塞西莉亚·爱德华兹(Cecilia Edwards
)说. 正如基金经理或风险投资公司将通过整体绩效来确定事情进展的方式一样, 数字化转型领导者必须对数字化变革工作有一个整体的看法.
PS. 系统化的思考方式
这一点特别重要,这样一个特定项目的不佳表现就不会对它的总体工作产生负面影响. 它还建立了对为实现真正的数字化转型而承担必要风险的容忍度.
如果这一切让你觉得自己落后了, 不要害怕. CIO
们对数字转型最大的误解之一就是: 他们所有的竞争对手都比他们领先得多. 红帽公司首席营销官蒂姆•伊顿表示, 这是因为人们对速度最快的变形金刚(以及周围的流行媒体)赞赏有加, 但很少有人指出转型有多难, 也很少有人指出一家典型的2000家全球公司需要多长时间才能转型.
随着企业制定自己的数字化转型战略, 已经开始他们的旅程的 CIO
和 IT领导者已经学到很多东西. 以下是故事和数字化转型案例研究的集合, 您可以进一步探索.
仔细回想了一下, 大一下开始, 就再也没有刷过题. 然而眼见马上就要实习了, 几个提前去面试的同学回来说应聘居然考的是大一那会的算法… 好吧, 虽然算法很重要, 但是计算机不应该使用来解决问题的吗? 我还以为面试会针对一些具体的开发场景或者开发框架问一些问题么…
算了, 都一样. 算法到哪里都用得着. 那么今天就开始回坑刷题, 但毕竟手上还压着两个项目没做, 所以权当是消遣, 娱乐局…
Given an array nums containing n + 1 integers where each integer is between 1 and n (inclusive), prove that at least one duplicate number must exist. Assume that there is only one duplicate number, find the duplicate one.
Example 1:
Input: [1,3,4,2,2]
Output: 2
Example 2:
Input: [3,1,3,4,2]
Output: 3
Note:
You must not modify the array (assume the array is read only).
You must use only constant, O(1) extra space.
Your runtime complexity should be less than O(n2).
There is only one duplicate number in the array, but it could be repeated more than once.
第一个反应就是用空间换时间, 用桶排法的思想, 创建一个相同长度的 vector<int>
来记录数组中出现的数字个数, 然后遍历一遍数组.
1 | int findDuplicate(std::vector<int> &nums) { |
这种写法很简单, 时间复杂度 O(N)
. 程序也简单, 5分钟就能写完. 当然结果也不出所料:
虽然内存消耗也不小. 但毕竟功能实现了…
因为这里新开一了一个容器, 导致内存占用较大. 如果不新开内存也是有办法的, 那就是直接对其排序, 然后遍历一遍这个有序数组, 把每一个元素都和下一个元素比较是否相等. 排序的算法也很多, 但不管用什么排序算法, 时间复杂度一定会大于等于 O(N)
. 个人一项主张用空间换时间, 主要可能是换 CPU 没有内存条方便吧:-)
刚才的角度是从整体看整个数组存储的数据结构就是一个数组. 但是如果换个角度, 这个数组内存储的这些数据是一个 环链表 的话, 就全部都不一样了. 把每一个元素看成是链表的一个节点, 把每一个数组的下标看成是链表节点的地址(索引), 再把每一个数组内储存的值看成是节点的 next
, 以 [1,3,4,2,2]
为例, 整个链表的上帝视角应该是这样的:
数组内重复的那个元素其实就是环链表的起始环点. 设置两根指针, 一根 slow
, 一根 fast
, slow
每次前进一个节点而 fast
每次前进两个. 当他们相遇的时候, slow
从起点出发, fast
从相遇点出发, 一次走一步. 再次相遇的时候就是起始环点.
1 | int findDuplicate(std::vector<int> &nums) { |
证明如下:
这种算法时间复杂度还是 O(N), 空间复杂度也降低了. 最后运行时间跟上面一样 16ms
, 内存少了 1MB
. 没错,,,,费老鼻子劲, 就少了 1MB
内存…
实在没忍住, 看了一眼 0ms
, 2ms
的解法, 意外发现居然是暴力解法…那些看上去时间复杂度是 O(N2)
的算法, 反而是最快的. 仔细想了下, 要不就是测试数据设置不合理, 循环前几次就得到答案, 要不就是后台解释器对暴力循环妖魔般优化了. 不管怎么说, 是在下输了…告辞
Jupyterlab Micropython Extension
是一个用于连接 MicroPython
开发板的 Jupyter Lab
插件. 目前仅仅在 Windows
和 Ubuntu 18.04
系统下测试可用.
插件依靠 JupyterLab MicroPython Kernel
与 MicroPython
通讯, 使用 JupyterLab MicroPython Kernel
的 Jupyter Lab
可以通过输入 %serialconnect --port=/dev/ttyUSB0
来连接:
插件的不同按钮对应了不同的 magic command
以此实现通过 UI
与 MicroPython
开发板交互.
项目地址: JupyterLab MicroPython Ext
这个插件是通过 JupyterLab MicroPython Kernel
与 MicroPython
交互的. 所以要使用插件需要首先安装 JupyterLab MicroPython Kernel
首先下载 JupyterLab MicroPython Kernel
到本地, 并在 bash
中切换到项目路径:
1 | git clone https://github.com/zhouzaihang/jupyterlab_micropython_kernel.git |
然后使用 pip
安装它 作为一个 Python3
的库, 使用 -e
参数表示在可编辑模式(editable mode
)下从一个本地的项目路径或 VCS URL
中安装一个项目:
1 | pip install -e . |
在 python/../site-packages
(Python
默认库的安装位置) 会创建一个文件指向到当前目录, 当你使用 git pull
或者其他操作修改代码后, 会自动调用最新的代码.(如果出错的话请检查你使用 Python
环境下是否应该使用 pip3
, sudo pip
等)
然后输入以下命令把 kernelspec
添加到 jupyter
中:
1 | python -m jupyterlab_micropython_kernel.install |
这会创建文件 .local/share/jupyter/kernels/micropython/kernel.json
, 并指向当前安装的 kernelspec
.
你可以使用以下命令查看当前所有安装的 kernelspec
极其位置:
1 | jupyter kernelspec list |
首先下载 JupyterLab MicroPython Ext
到本地, 并在 bash
中切换到项目路径:
1 | # Clone the repo to your local environment |
jlpm
命令是一个 Jupyter Lab
使用的 yarn 固定版本, yarn会在安装 Jupyter Lab
的时候一起安装. 下面的命令中你也可以使用yarn
或 npm
替换 jlpm
.
1 | # Install dependencies |
当项目有更新时, 你可以运行以下命令更新代码并 rebuild
插件和 Jupyter Lab
程序.
1 | # Update source code |
在开发过程中, 你可以监听文件的变化, 然后在文件修改后自动 rebuild
插件和 Jupyter Lab
程序.
1 | # Watch the source directory in another terminal tab |
搭建开发环境可以见 Jupyter Lab 官方文档
打开 src/index.ts
, 找到 extension: JupyterFrontEndPlugin<void>
, 修改其中的 activate
:
1 | /** |
编写 activate
函数:
1 | /** |
然后编写在 activate
中调用的 MicropythonExtension
类:
1 | /** |
接下来需要在面板的工具栏中添加按钮, 首先在 MicropythonExtension
的 createNew
方法中创建一个 ToolbarButton
:
1 | let serialConnectButton = new ToolbarButton({ |
把创建好的按钮插入到 toolbar
中:
1 | panel.toolbar.insertItem(8, 'Serial Connect', serialConnectButton) |
最后要记得在 DisposableDelegate
中 dispose
按钮:
1 | return new DisposableDelegate(() => { |
最后编写按钮的业务逻辑, 把按钮的点击事件修改为一个异步函数:
1 | let serialConnectCallback = async () => { |
使用 InputDialog
可以设置一个弹出的输入框, 接收输入数据. 使用 input.button.accept
判断是否收到输入:
1 | const input = await InputDialog.getText({ |
使用 context.session.kernel
可以获取当前运行的 kernel
, 然后通过 kernel.requestExecute()
方法给 kernel
发送要执行的语句:
1 | const port = input.value; |
kernel.requestExecute
执行后会返回的内容是异步的, 可以使用 done
监听是否执行完成, 使用 onIOPub
监听收到的返回数据:
1 | let future = context.session.kernel.requestExecute(content, false); |
因为 kernel
返回数据是 ANSI Escape sequences
, 所以使用正则表达式 \u001b\[\d+m
过滤: msg.content.text.replace(/\u001b\[\d+m/g, '')
最终的代码如下:
1 | /** |
运行以下命令卸载插件:
1 | jupyter labextension uninstall jupyterlab_micropython_ext |
本文介绍如何开发一个 Flutter Packge
以实现调用 Andorid
设备摄像头精确追踪并识别十指的运动路径/轨迹和手势动作, 且输出22个手部关键点以支持更多手势自定义. 基于这个包可以编写业务逻辑将手势信息实时转化为指令信息: 一二三四五, rock, spiderman…同时基于 Flutter
可以对不同手势编写不同特效. 可用于短视频直播特效, 智能硬件等领域, 为人机互动带来更自然丰富的体验.
源码托管于 Github: https://github.com/zhouzaihang/flutter_hand_tracking_plugin
Flutter Plugin Package
Docker
配置 MediaPipe
开发环境Gradle
中使用 MediaPipe
Flutter
程序运行 MediaPipe
图Flutter
页面中嵌入原生视图protobuf
的使用Flutter Package
Flutter Package
有以下两种类型:
Dart Package
: 完全用 Dart
编写的包, 例如 path
包. 其中一些可能包含 Flutter
特定的功能, 这类包完全依赖于 Flutter
框架.
Plugin Package
: 一类依赖于运行平台的包, 其中包含用 Dart
代码编写的 API
, 并结合了针对 Android
(使用 Java
或 Kotlin
)和 iOS
(使用 ObjC
或 Swift
)平台特定的实现. 比如说 battery
包.
Flutter Plugin Package
Flutter
作为一个跨平台的 UI
框架, 本身是不能够直接调用原生的功能的. 如果需要使用原生系统的功能, 就需要对平台特定实现, 然后在 Flutter
的 Dart
层进行兼容.
此处需要使用调用摄像头和 GPU
实现业务. 所以使用 Flutter Plugin Package
.
Flutter Plugin Package
是如何工作的以下是 Flutter Plugin Package
项目的目录:
其中 pubspec.yaml
用于添加 Plugin
可能会用到的依赖或者资源(图片, 字体等)
example
目录下是一个完整的 Flutter APP
, 用于测试编写的 Plugin
另外, 无论在一个 Flutter app
项目还是在一个 Flutter Plugin
项目中都会有三个目录 android
, ios
和 lib
. lib
目录用于存放 Dart
代码, 而另外两个目录则是用于存放平台特定实现的代码. Flutter
会运行根据实际运行平台来运行平台对应的代码, 然后使用 Platform Channels 把代码运行的结果返回给 Dart
层.
以下是 Flutter
官方给出的一张 Flutter
架构图:
从架构图中可以看到 Flutter
之所以是一个跨平台框架是因为有 Embedder
作为操作系统适配层, Engine
层实现渲染引擎等功能, 而 Framework
层是一个用 Dart
实现的 UI SDK
. 对于一个 Flutter Plugin Package
来说, 就是要在 Embedder
层用原生的平台特定实现, 并且在 Dart
层中封装为一个 UI API
, 从而实现跨平台. Embedder
层并不能直接和 Framework
直接连接, 还必须经过 Engine
层的 Platform Channels
.
使用 Platform Channels
在客户端(UI
) 和主机(特定平台)之间传递的过程如下图所示:
Flutter Plugin Package
Android Studio
, 点击 New Flutter Project
Flutter Plugin
选项Android
平台 view
首先在 android/src/main/kotlin/xyz/zhzh/flutter_hand_tracking_plugin
目录下创建两个 kotlin
文件: FlutterHandTrackingPlugin.kt
和 HandTrackingViewFactory.kt
文件.
Factory
类在 HandTrackingViewFactory.kt
中编写一个 HandTrackingViewFactory
类实现抽象类 PlatformViewFactory
. 之后编写的 Android
平台组件都需要用这个 Factory
类来生成. 在生成视图的时候需要传入一个参数 id
来辨别视图(id
会由 Flutter
创建并传递给 Factory
):
1 | package xyz.zhzh.flutter_hand_tracking_plugin |
AndroidView
类在 FlutterHandTrackingPlugin.kt
中编写 FlutterHandTrackingPlugin
实现 PlatformView
接口, 这个接口需要实现两个方法 getView
和 dispose
.
getView
用于返回一个将要嵌入到 Flutter
界面的视图
dispose
则是在试图关闭的时候进行一些操作
首先要添加一个 SurfaceView
:
1 | class FlutterHandTrackingPlugin(r: Registrar, id: Int) : PlatformView, MethodCallHandler { |
然后通过 getView
返回添加的 SurfaceView
:
1 | class FlutterHandTrackingPlugin(r: Registrar, id: Int) : PlatformView, MethodCallHandler { |
Dart
中调用原生实现的 View
打开
plugin package
项目的lib/flutter_hand_tracking_plugin.dart
进行编辑(具体文件名依据新建项目时创建的包名).
在 Flutter
中调用原生的 Android
组件需要创建一个 AndroidView
并告诉它组建的注册名称, 创建 AndroidView
的时候, 会给组件分配一个 id
, 这个 id
可以通过参数 onPlatformViewCreated
传入方法获得:
1 | AndroidView( |
由于只实现了 Android
平台的组件, 在其他系统上并不可使用, 所以还需要获取 defaultTargetPlatform
来判断运行的平台:
1 | import 'dart:async'; |
上面使用 typedef
定义了一个 HandTrackingViewCreatedCallback
, 传入的参数类型为 HandTrackingViewController
, 这个 controller
用于管理对应 AndroidView
的 id
:
1 | class HandTrackingViewController { |
其中的 MethodChannel
用于调用 Flutter Plugin Package
的方法, 本次不需要使用到 MethodChannel
, 所以不用关注.
Docker
构建 MediaPipe AAR
并添加到项目中MediaPipe
是一个 Google
发布的使用 ML pipelines
技术构建多个模型连接在一起的跨平台框架. (Machine learning pipelines
: 简单说就是一套 API
解决各个模型/算法/workflow
之间的数据传输). MediaPipe
支持视频, 音频, 等任何 time series data
(WiKi–Time Series).
这里利用 MediaPipe
将摄像头数据传入到手势检测的 TFlite
模型中处理. 然后再把整套程序构建为 Android archive library
.
MediaPipe Android archive library
是一个把 MediaPipe
与 Gradle
一起使用的方法. MediaPipe
不会发布可用于所有项目的常规AAR, 所以需要开发者自行构建. 这是官方给出的MediaPipe 安装教程. 笔者这里是 Ubuntu
系统, 选择了 Docker
的安装方式(git clone
和 docker pull
的时候网络不稳定的话可以设置一下 proxy
或换源).
安装完成后使用 docker exec -it mediapipe /bin/bash
进入 bash
操作.
mediapipe_aar()
首先在 mediapipe/examples/android/src/java/com/google/mediapipe/apps/aar_example
里创建一个 BUILD
文件, 并把一下内容添加到文本文件里.
1 | load("//mediapipe/java/com/google/mediapipe:mediapipe_aar.bzl", "mediapipe_aar") |
aar
根据上面创建的文本文件, 运行 bazel build
命令就可以生成一个 AAR
, 其中 --action_env=HTTP_PROXY=$HTTP_PROXY
, 这个参数是用来指定设置代理的(因为在构建的过程中会从 Github
下载很多依赖.)
1 | bazel build -c opt --action_env=HTTP_PROXY=$HTTP_PROXY --action_env=HTTPS_PROXY=$HTTPS_PROXY --fat_apk_cpu=arm64-v8a,armeabi-v7a mediapipe/examples/android/src/java/com/google/mediapipe/apps/aar_example:hand_tracking_aar |
在容器内的 /mediapipe/examples/android/src/java/com/google/mediapipe/apps/aar_example
路径下, 你可以找到刚刚构建得到的 hand_tracking_aar.aar
, 使用 docker cp
从容器中拷贝到项目的 android/libs
下.
binary graph
上面的 aar
执行还需要依赖 MediaPipe binary graph
, 使用以下命令可以构建生成一个 binary graph
:
1 | bazel build -c opt mediapipe/examples/android/src/java/com/google/mediapipe/apps/handtrackinggpu:binary_graph |
从 bazel-bin/mediapipe/examples/android/src/java/com/google/mediapipe/apps/handtrackinggpu
中拷贝刚才 build
出来的 binary graph
, 当到 android/src/main/assets
下
assets
和 OpenCV library
在容器的 /mediapipe/models
目录下, 会找到 hand_lanmark.tflite
, palm_detection.tflite
和 palm_detection_labelmap.txt
把这些也拷贝出来放到 android/src/main/assets
下.
另外 MediaPipe
依赖 OpenCV
, 是哦一需要下载 OpenCV
预编译的 JNI libraries
库, 并放置到 android/src/main/jniLibs
路径下. 可以从此处下载官方的 OpenCV Android SDK
然后运行 cp
放到对应路径中
1 | cp -R ~/Downloads/OpenCV-android-sdk/sdk/native/libs/arm* /path/to/your/plugin/android/src/main/jniLibs/ |
MediaPipe
框架使用 OpenCV
, 要加载 MediaPipe
框架首先要在 Flutter Plugin
中加载 OpenCV
, 在 FlutterHandTrackingPlugin
的 companion object
中使用以下代码来加载这两个依赖项:
1 | class FlutterHandTrackingPlugin(r: Registrar, id: Int) : PlatformView, MethodCallHandler { |
build.grage
打开 android/build.gradle
, 添加 MediaPipe dependencies
和 MediaPipe AAR
到 app/build.gradle
:
1 | dependencies { |
CameraX
调用摄像头要在我们的应用程序中使用相机, 我们需要请求用户提供对相机的访问权限. 要请求相机权限, 请将以下内容添加到 android/src/main/AndroidManifest.xml
:
1 | <!-- For using the camera --> |
同时在 build.gradle
中将最低 SDK
版本更改为 21
以上, 并将目标 SDK
版本更改为 27
以上
1 | defaultConfig { |
为了确保提示用户请求照相机权限, 并使我们能够使用 CameraX
库访问摄像头. 请求摄像机许可, 可以使用 MediaPipe
组件提供的组建 PermissionHelper
要使用它. 首先在组件 init
内添加请求权限的代码:
1 | PermissionHelper.checkAndRequestCameraPermissions(activity) |
这会在屏幕上以对话框提示用户, 以请求在此应用程序中使用相机的权限.
然后在添加以下代码来处理用户响应:
1 | class FlutterHandTrackingPlugin(r: Registrar, id: Int) : PlatformView, MethodCallHandler { |
暂时将 startCamera()
方法保留为空. 当用户响应提示后, onResume()
方法会被调用调用. 该代码将确认已授予使用相机的权限, 然后将启动相机.
现在将 SurfaceTexture
和 SurfaceView
添加到插件:
1 | // {@link SurfaceTexture} where the camera-preview frames can be accessed. |
在组件 init
的方法里, 添加 setupPreviewDisplayView()
方法到请求请求摄像头权限的前面:
1 | init { |
然后编写 setupPreviewDisplayView
方法:
1 | private fun setupPreviewDisplayView() { |
要 previewDisplayView
用于获取摄像头的数据, 可以使用 CameraX
, MediaPipe
提供了一个名为 CameraXPreviewHelper
的类使用 CameraX
. 开启相机时, 可以更新监听函数 onCameraStarted(@Nullable SurfaceTexture)
现在定义一个 CameraXPreviewHelper
:
1 | class FlutterHandTrackingPlugin(r: Registrar, id: Int) : PlatformView, MethodCallHandler { |
然后实现之前的 startCamera()
:
1 | private fun startCamera() { |
这将 new
一个 CameraXPreviewHelper
对象, 并在该对象上添加一个匿名监听. 当有 cameraHelper
监听到相机已启动时, surfaceTexture
可以抓取摄像头帧, 将其传给 previewFrameTexture
, 并使 previewDisplayView
可见.
在调用摄像头时, 需要确定要使用的相机. CameraXPreviewHelper
继承 CameraHelper
的两个选项: FRONT
和 BACK
. 并且作为参数 CAMERA_FACING
传入 cameraHelper!!.startCamera
方法. 这里设置前置摄像头为一个静态变量:
1 | class FlutterHandTrackingPlugin(r: Registrar, id: Int) : PlatformView, MethodCallHandler { |
ExternalTextureConverter
转换摄像头图像帧数据上面使用 SurfaceTexture
将流中的摄像头图像帧捕获并存在 OpenGL ES texture
对象中. 要使用 MediaPipe graph
, 需要把摄像机捕获的帧存在在普通的 Open GL texture
对象中. MediaPipe
提供了类 ExternalTextureConverter
类用于将存储在 SurfaceTexture
对象中的图像帧转换为常规 OpenGL texture
对象.
要使用 ExternalTextureConverter
, 还需要一个由 EglManager
对象创建和管理的 EGLContext
. 在插件中添加以下声明:
1 | // Creates and manages an {@link EGLContext}. |
修改之前编写的 onResume()
方法添加初始化 converter
对象的代码:
1 | private fun onResume() { |
要把 previewFrameTexture
传输到 converter
进行转换, 将以下代码块添加到 setupPreviewDisplayView()
:
1 | private fun setupPreviewDisplayView() { |
在上面的代码中, 首先自定义并添加 SurfaceHolder.Callback
到 previewDisplayView
并实现 surfaceChanged(SurfaceHolder holder, int format, int width, int height)
:
previewFrameTexture
和 displaySize
到 converter
现在摄像头获取到的图像帧已经可以传入到 MediaPipe graph
中了.
MediaPipe graph
首先需要加载所有 MediaPipe graph
需要的资源(之前从容器中拷贝出来的 tflite
模型, binary graph
等) 可以使用 MediaPipe
的组件 AndroidAssetUtil
类:
1 | // Initialize asset manager so that MediaPipe native libraries can access the app assets, e.g., |
然后添加以下代码设置 processor
:
1 | init { |
然后根据用到的 graph
的名称声明静态变量, 这些静态变量用于之后使用 graph
:
1 | class FlutterHandTrackingPlugin(r: Registrar, id: Int) : PlatformView, MethodCallHandler { |
现在设置一个 FrameProcessor
对象, 把之前 converter
转换好的的摄像头图像帧发送到 MediaPipe graph
并运行该图获得输出的图像帧, 然后更新 previewDisplayView
来显示输出. 添加以下代码以声明 FrameProcessor
:
1 | // Sends camera-preview frames into a MediaPipe graph for processing, and displays the processed |
然后编辑 onResume()
通过 converter!!.setConsumer(processor)
设置 convert
把转换好的图像帧输出到 processor
:
1 | private fun onResume() { |
接着就是把 processor
处理后的图像帧输出到 previewDisplayView
. 重新编辑 setupPreviewDisplayView
修改之前定义的 SurfaceHolder.Callback
:
1 | private fun setupPreviewDisplayView() { |
当 SurfaceHolder
被创建时, 摄像头图像帧就会经由 convert
转换后输出到 processor
, 然后再通过 VideoSurfaceOutput
输出一个 Surface
.
EventChannel
实现原生组件与 Flutter
的通讯之前的处理仅仅在处理图像帧, processor
除了处理图像之外, 还可以获取手部关键点的坐标. 通过 EventChannel
可以把这些数据传输给 Flutter
的 Dart
层, 进而根据这些关键点可以编写各种业务逻辑将手势信息实时转化为指令信息: 一二三四五, rock, spiderman…或者对不同手势编写不同特效. 从而为人机互动带来更自然丰富的体验.
Android
平台打开 EventChannel
首先定义一个 EventChannel
和一个 EventChannel.EventSink
:
1 | private val eventChannel: EventChannel = EventChannel(r.messenger(), "$NAMESPACE/$id/landmarks") |
EventChannel.EventSink
用于之后发送消息. 然后在 init
方法中初始化 eventChannel
:
1 | inti { |
设置完消息通道后, 编辑之前的 setupProcess()
方法. 在设置 processor
的输出前添加代码, 实现获得手部关键点的位置并通过 EventChannel.EventSink
发送到之前打开的 eventChannel
:
1 | private val uiThreadHandler: Handler = Handler(Looper.getMainLooper()) |
这里 LandmarkProto.NormalizedLandmarkList.parseFrom()
用来解析标记点 byte array
格式的数据. 因为所有标记点的数据都是使用 protobuf
封装的. Protocol buffers
是一种 Google
可以在各种语言使用的跨平台序列化结构数据的工具, 详情可以查看官网.
另外最后用到了 uiThreadHandler
来发送数据, 因为 processor
的 callback
会在线程中执行, 但是 Flutter
框架往 eventChannel
里发送消息需要在 UI
线程中, 所以使用 uiThreadHandler
来 post
.
完整的 FlutterHandTrackingPlugin.kt
的详情可见github
Dart
层获得 eventChannel
的数据再一次打开 lib/flutter_hand_tracking_plugin.dart
, 编辑 HandTrackingViewController
类. 根据 id
添加一个 EventChannel
, 然后使用 receiveBroadcastStream
接受这个通道消息:
1 | class HandTrackingViewController { |
之前已经介绍过了, 传输的数据格式是使用 protobuf
序列化的有一定结构的 byte array
. 所以需要使用 NormalizedLandmarkList.fromBuffer()
, 来解析. NormalizedLandmarkList.fromBuffer()
这个接口, 是由 protobuf
根据 protos/landmark.proto 生成的 .dart
文件提供.
首先打开 pubspec.yaml
添加 protoc_plugin
的依赖:
1 | dependencies: |
然后安装和激活插件:
1 | pub install |
再根据protobuf 安装教程配置 Protocol
然后运行 protoc
命令就可以生成 .dart
文件了:
1 | protoc --dart_out=../lib/gen ./landmark.proto |
也可以直接使用已经生成好的flutter_hand_tracking_plugin/lib/gen/:
生成完了以后就可以通过 NormalizedLandmarkList
来存储收到的数据, 并且 NormalizedLandmarkList
对象有 fromBuffer()
, fromJson()
各种方法来反序列化数据.
本
Codelab
介绍如何实现Android
平台的经典蓝牙功能并封装为一个Flutter Plugin Package
被Flutter app
或其他Flutter package
调用
项目需要开发一个 Flutter Packge
以提供以下功能(API
):
Flutter Package
有以下两种类型:
Dart Package
: 完全用 Dart
编写的包, 例如 path
包. 其中一些可能包含 Flutter
特定的功能, 这类包完全依赖于 Flutter
框架.
Plugin Package
: 一类依赖于运行平台的包, 其中包含用 Dart
代码编写的 API
, 并结合了针对 Android
(使用 Java
或 Kotlin
)和 iOS
(使用 ObjC
或 Swift
)平台特定的实现. 比如说 battery
包.
Flutter
作为一个跨平台的 UI
框架, 本身是不能够直接调用原生的功能的. 如果需要使用原生系统的功能, 就需要对平台特定实现, 然后在 Flutter
的 Dart
层进行兼容.
此处需要使用经典蓝牙的功能, 实现一些业务. 但是 Flutter
现有的蓝牙库并不能满足需求. 因为现有库仅仅支持低功耗蓝牙, 而这里需要用到经典蓝牙实现蓝牙传输文本/图片/视频流
而在这个 Codelab
中因为涉及到蓝牙需要调用平台原生的 API
所以使用 Flutter Plugin Package
.
以下是 Flutter Plugin Package
项目的目录:
其中 pubspec.yaml
用于添加 Plugin
可能会用到的依赖或者资源(图片, 字体等)
example
目录下是一个完整的 Flutter APP
, 用于测试编写的 Plugin
另外, 无论在一个 Flutter app
项目还是在一个 Flutter Plugin
项目中都会有三个目录 android
, ios
和 lib
. lib
目录用于存放 Dart
代码, 而另外两个目录则是用于存放平台特定实现的代码. Flutter
会运行根据实际运行平台来运行平台对应的代码, 然后使用 Platform Channels 把代码运行的结果返回给 Dart
层.
以下是 Flutter
官方给出的一张 Flutter
架构图:
从架构图中可以看到 Flutter
之所以是一个跨平台框架是因为有 Embedder
作为操作系统适配层, Engine
层实现渲染引擎等功能, 而 Framework
层是一个用 Dart
实现的 UI SDK
. 对于一个 Flutter Plugin Package
来说, 就是要在 Embedder
层用原生的平台特定实现, 并且在 Dart
层中封装为一个 UI API
, 从而实现跨平台. Embedder
层并不能直接和 Framework
直接连接, 还必须经过 Engine
层的 Platform Channels
.
使用 Platform Channels
在客户端(UI
) 和主机(特定平台)之间传递的过程如下图所示:
Android Studio
, 点击 New Flutter Project
Flutter Plugin
选项项目下有 android
和 ios
两个目录用于平台特定实现. 首先打开 android/src/main/kotlin/xyz/zhzh/flutter_bt_bluebooth/FlutterBtBluetoothPlugin.kt
(flutter_bt_bluebooth
为新建的包名)将里面的代码修改为:
1 | package xyz.zhzh.flutter_bt_bluebooth |
这里 FlutterBtBluetoothPlugin
实现了 MethodCallHandler
接口, 其中 registerWith
方法, 会在创建连接 FlutterBtBluetoothPlugin
的时候执行 (Flutter
会自动执行), 在这个方法里新建了一个 MethodChannel
并打开了名字为 flutter_plugin
的通道并设置监听.
Flutter Plugin
通过覆盖 onMethodCall
方法实现对通道中名为 getPlatformVersion
的消息响应逻辑.
打开 example/lib/main.dart
, 此处使用了 Platform Channel
获取当前运行平台的系统版本:
1 | import 'dart:async'; |
example/lib/main.dart
中封装了一个类 FlutterPlugin
, 以此提供一个获取平台版本好的 API
, 也就是前面提到的在 Dart
层统一各个平台的差异
等待创建完成后, 打开项目, 会看到一个自动新建好的 example
项目用作调试 Plugin Package
.
example
默认导入了新建的 Plugin Package
, 打开 example/pubspec.yaml
会看到:
1 | dev_dependencies: |
之前已经在 Dart
层封装好 API
了, example
在 example/lib/main.dart
中可以直接调用这个 API
而不需要考虑平台差异:
1 | // Platform messages are asynchronous, so we initialize in an async method. |
点击运行后, 程序就会自动运行显示出当前运行平台的系统版本:
Platform Channel
可以看成 Linux
里面的管道的概念, 双方要通讯是需要花费一定的代价的. 而对于蓝牙传输视频流数据这样的情况, 如果把蓝牙接收到的视频帧再通过 Platform Channel
传输到 Flutter UI
, 再由 Flutter
渲染绘制出图像显示出来的话, 会造成很大的开销. 所以这里采用一种技术方案: 把原生 Android
端渲染视频帧, 然后在 Flutter
布局中嵌入原生的 View
.
在开发原生组件开发过程中, 热重载功能无法使用. 每次修改后都需要重新编译原生工程才能使之生效
在 Flutter
中添加原生组件的流程如下:
PlatformView
, 构建一个原生的 view
PlatformViewFactory
用于生成 PlatformView
Flutter
工程中调用原生 View
要创建可以直接嵌入 Flutter
布局的原生试图. 需要实现 PlatformView
接口. 类在初始化时需要两个参数:
Registrar
类型的参数用于之后申请蓝牙权限, 打开 Platform Channel
等.Int
类型的参数用于表示原生试图的 id
.创建的原生试图可以基于原生 UI
的一些基础试图, 本 CodeLabs
要显示蓝牙接收到的视频帧, 所以使用 Android
的 ImageView
.
1 | // 类名可以自定义 |
实现 PlatformView
接口需要实现两个方法, getView
用于提供 Flutter
要嵌入的 View
, dispose
则在试图关闭时执行:
1 | override fun getView(): View { |
上面创建的类并不能直接被 Flutter
调用, 需要一个继承了 PlatformViewFactory
的类, 创建 View
再返回给 Flutter
. 新建一个 BtVideoViewFactory.kt
(名字可以自定义) 文件, 在实现 create
方法里面调用上面创建的视图类
1 | import android.content.Context |
找到之前的 registerWith
方法, 编写注册组件时的业务逻辑:
1 | companion object { |
在注册的时候, 使用了 NAMESPACE
的字符串作为组件的注册名称, 在 Flutter
调用时需要用到, 具体名称可以自定义.
打开
plugin package
项目的lib/flutter_bt_bluetooth.dart
进行编辑(具体文件名依据新建项目时创建的包名).
在 Flutter
中调用原生的 Android
组件需要创建一个 AndroidView
并告诉它组建的注册名称, 创建 AndroidView
的时候, 会给组件分配一个 id
, 这个 id
可以通过参数 onPlatformViewCreated
传入方法获得:
1 | AndroidView( |
由于只实现了 Android
平台的组件, 在其他系统上并不可使用, 所以还需要获取 defaultTargetPlatform
来判断运行的平台:
1 | const NAMESPACE = 'plugins.zhzh.xyz/flutter_bt_bluetooth'; |
使用经典蓝牙需要申请定位权限, 应用内申请定位权限后, 在组件初始化时通过 addRequestPermissionsResultListener
添加权限申请结果的监听:
1 | class FlutterBtBluetoothPlugin(r: Registrar, id: Int) : PlatformView, MethodCallHandler { |
LocationRequestPermissionsListener
继承 PluginRegistry.RequestPermissionsResultListener
用来编写收到权限申请结果后的业务逻辑, 此处还设置了一个静态的查询 id
为 REQUEST_COARSE_LOCATION_PERMISSIONS
. 申请权限时带上这个 id
:
1 | ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION), REQUEST_COARSE_LOCATION_PERMISSIONS) |
在监听结果的时候, 就可以根据 id
来判断是否是组件发起的权限申请了:
1 | class FlutterBtBluetoothPlugin(r: Registrar, id: Int) : PlatformView, MethodCallHandler { |
首先需要和开发原生 Android
程序一样在 android/src/main/AndroidManifest.xml
里添加权限:
1 | <manifest xmlns:android="http://schemas.android.com/apk/res/android" |
然后需要添加三个文件, 这三个文件封装了一些调用经典蓝牙的功能, 实现经典蓝牙传输图片:
把这三份代码放在 android/src/main/kotlin/xyz/zhzh
下 (如果包名不一致的话, 请修改代码内的包名)
同时在初始化 view
的时候添加一个 BluetoothUtil
用来之后调用蓝牙:
1 | class FlutterBtBluetoothPlugin(r: Registrar, id: Int) : PlatformView, MethodCallHandler { |
MethodChannel: 用于 Dart
层向原生平台通讯
EventChannel: 用于原生向平台通讯
设置一个 methodChannel
用来响应 Flutter
层的方法调用, 设置一个 eventChannel
来返回蓝牙接受到的数据, 设置一个 stateChannel
来返回蓝牙连接状态, 三个消息通道的名字都以 ${NAMESPACE}/${id}/
开头, 因为 id
的唯一, 保证不会发生冲突:
1 | /** FlutterBtBluetoothPlugin */ |
然后给三个消息通道设置监听:
1 | init { |
对于 methodChannel
的监听设置的是 this
, 而 FlutterBtBluetoothPlugin
已经实现了 MethodCallHandler
接口, 所以直接在 onMethodCall
方法里编写代码:
1 | override fun onMethodCall(MethodCall, result: Result) call: { |
对于 stateChannel
, 使用 mBluetoothUtil.setBluetoothConnectionListener
监听蓝牙连接状态, 同时在 stateChannel
被监听 (onListen
) 的时候, 记录下通道 EventSink
, 并通过 EventSink
把蓝牙连接状态发送到 Dart
层
1 | private fun bluetoothConnectionStreamHandler(): EventChannel.StreamHandler { |
对于 eventChannel
, 使用 mBluetoothUtil.setOnDataReceivedListener
监听蓝牙接受到的数据, 同时在 eventChannel
被监听 (onListen
) 的时候, 记录下通道 EventSink
, 并通过 EventSink
把蓝牙接受到的数据发送到 Dart
层
1 | private fun bluetoothOutputStreamHandler(): EventChannel.StreamHandler { |
android/src/main/kotlin/xyz/zhzh/flutter_bt_bluetooth/FlutterBtBluetoothPlugin.kt
的完整代码见: http://ubibots.zucc.edu.cn/zzh/flutter_bt_bluetooth/blob/master/android/src/main/kotlin/xyz/zhzh/flutter_bt_bluetooth/FlutterBtBluetoothPlugin.kt
新建一个 BlueViewController
类用来根据 AndroidView
创建时分配的 id
来打开消息通道
1 | const NAMESPACE = 'plugins.zhzh.xyz/flutter_bt_bluetooth'; |
对于 MethodChannel
类型的消息通道, 需要使用 _channel.invokeMethod
来主动发起请求, 消息通道的数据是异步传输的, 所以在编写请求数据的 API
时, 返回的数据也是异步的:
1 | Future<Map<dynamic, dynamic>> get bondedDevices async => |
对于 EventChannel
类型的消息通道, 是一个 Stream
, 使用 receiveBroadcastStream
打开通道后:
1 | Stream<int> get stateStream async* { |
封装三个消息通道所有的 API
:
1 | class BlueViewController { |
封装完后, 需要修改一下之前编写的 BlueView
, 因为 BlueViewController
需要 AndroidView
创建时生成的 id
. 而 Flutter Plugin Package
是为了在编写 Flutter app
时直接调用的, 所以给 BlueView
设置一个参数. 使用 BlueView
的使用可以传入一个函数, 用来自定义对 id
的处理逻辑.
1 | typedef void BlueViewCreatedCallback(BlueViewController controller); |
最终 lib/flutter_bt_bluetooth.dart
的完整代码: http://ubibots.zucc.edu.cn/zzh/flutter_bt_bluetooth/blob/master/lib/flutter_bt_bluetooth.dart
出于某些不可描述却又无可奈何的原因, 需要部署一个十年前的 PHP
老项目. 这个项目用到了 MySQL 5.5.54
, PHP 5.3.10
和 Apache 2.2.22
, 最坑的是不知道是 PHP
牛逼还是这项目牛逼, 在 PHP
版本号上有一点差别网站就起不来了…
所以考虑祭出 Docker
这个配环境神器.
Docker
中大多数镜像都是基于 Ubuntu
基础镜像进行了修改构建,但是如果我们也从 Ubuntu
开始构建,那么工作量可想而知. 因此选取一个合适的基础镜像可以加快配置速度,也能减少我们配置的工作量. Docker Hub
这时候就是个好东西了, 几乎你想要的都能找到. 根据需要,选取了两个镜像作为本次开发环境配置的基础镜像.
1 | docker pull corvax19/php5.3.10-apache2.2.22-ubuntu14 |
拉取成功后可以通过 docker images
命令查看当前系统中的镜像:
如果由于某些不可描述却又无可奈何的原因导致拉取速度过慢的话, 建议更换镜像站为七牛云, 阿里云和亚马逊(中国)的 Docker 镜像
MySQL
作为公共的基础服务, 这里配置的 MySQL
不仅可以供其他容器调用, 也可以使用本地连接后使用, 很方便调试程序.
1 | docker run --name pbcls_mysql -e MYSQL_ROOT_PASSWORD=456123 -p 3306:3306 -d mysql:5.5.54 |
-d
表示以守护进程的模式启动一个名为 pbcls_mysql
的容器并且配置环境变量 MYSQL_ROOT_PASSWORD=456123
, 就是设置 root
密码为 456123
, 同时把容器的 3306
端口映射到本地的 3306
端口上(如果本地装有 MySQL
或者 MariaDB
等其他占用了 3306
端口的进程则可以映射到本地其他空闲端口上)
启动成功后就可以直接访问本地 3306
端口访问容器内的数据库了.
这个容器最主要的就是指定 /var/www/html
的位置方便启动网站, 同时为了能使用数据库, 还需要连接刚才已经启动的 MySQL
容器.
1 | sudo docker run -d --name pbcls_webserver --link pbcls_mysql:db -p 81:80 -v /home/zhou/Documents/PHPProjects/sourcecode:/var/www -v /home/zhou/Documents/PHPProjects/sourcecode:/var/www/html corvax19/php5.3.10-apache2.2.22-ubuntu14 |
--link
用于把 Mysql
容器作为 db
组件链接到当前容器中, 这里在 /var/www
和 /var/www/html
分别挂载了本地代码的位置 /home/zhou/Documents/PHPProjects/sourcecode
.这样就方便修改代码, 每次在本地修改完代码, 直接刷新浏览器页面就可以看到生效的更改. 然后把 80
端口映射到 外部的 81
端口上.
如果一切正常, 现在访问 127.0.0.1:81
就可以看到这个十年祖传代码已经 run
起来了:
1 | WARNING: Error loading config file: /home/zhou/.docker/config.json: stat /home/zhou/.docker/config.json: permission denied |
其实可以忽略, 如果有强迫症的话, 可以考虑把这个文件加入用户组, 或者所有命令带上 sudo
首先检查这份祖传代码的 system/application/config/database.php
内的配置项;
打开容器的 shell
, 查看 MySQL
数据库地址:
1 | docker exec -it pbcls_webserver bash |
把配置项的端口和地址修改为和容器环境变量一致, 然后刷新浏览器就 OKK
了:
Maven
是一种 Java
构建工具, 主要有以下几个功能:
帮程序员甄别和调用第三方库
调用 javac
完整项目的编译
调用 JUnit
完成项目单元测试
完成项目打包
Ubuntu
下可以使用 apt
直接安装这个软件:
1 | sudo apt install maven |
安装完成后正常显示版本即可
如果使用 apt
安装 Maven
, 软件安装目录一般在 /usr/share/maven
下, 切换到软件的 conf
内编辑 settings.xml
, 添加阿里源的镜像:
1 | <mirror> |
打开 IDEA
的 Settings
, 在搜索里输入 Maven
, 找到并配置 Maven
相关路径:
另外直接使用会出现编译比较慢的情况, 有两种方式可以进行修改:
一. 就是在新建 Maven
项目的时候,设置该参数 archetypeCatalog = internal
二. 直接修改 IDEA
中 Maven
配置参数,在 Settings
中 Maven
选项中点击 Runner
修改 VM Options
: 为 -DarchetypeCatalog=internal
, 也就相当于在使用 mvn archetype:generate
命令时,加上参数 -DarchetypeCatalog=internal
新建一个 Maven
项目
在中央仓库寻找第三方 jar
的依赖文本(这里以一个汉语转拼音的依赖为例: https://mvnrepository.com/artifact/com.belerweb/pinyin4j/2.5.1)
拷贝依赖文本到项目的 pom.xml
:
1 | <!-- https://mvnrepository.com/artifact/com.belerweb/pinyin4j --> |
然后就可以在代码中像引用 jar
包那样引用
编译项目, 在 IDEA
上 Maven
工程有自己的窗口, 如果没有显示, 需要打开 Maven
项目窗口在 工具栏-->Help-->Find Action
然后在弹出的窗口里的文本框输入: maven projects
即可出现窗口. 在打开的窗口中,点击 YourProject->Lifecycle->package->Run Maven Build
执行打包, 生成的 war
默认在工程的 target
目录下
如果在编译过程中出现版本错误, 则需要在 pom.xml
中指定版本:
1 | <properties> |
build success
的时候运行即可:起初我们来到这个世界, 是因为我们不得不来, 最后我们离开这个世界, 是因为我们不得不走. 出生和死亡都是我们没有把握的事情, 但是我们能把握的是从出生到死亡这段时间. 中国目前的人均寿命是 76.25
年, 算上程序员通宵透支的生命, 大约是 75
年, 也就是 30 * 30
个月. 本案例是用 Flutter
记录并计算生命的剩余时间:
项目地址: https://github.com/zhouzaihang/life_countdown
完成的代码见 Github
首先新建 life.dart
, 编写第一个类 Life
. 这个类主要有三个功能:
根据生日自动和当前时间计算逝去的生命和剩余的岁月
使用 SharedPreferences
存储数据到本地, 方便下次打开应用的时候自动从本地读取数据
使用 ScopedModel
作为状态管理, 在生日修改后, 更新所有相关的组件
1 | import 'package:scoped_model/scoped_model.dart'; |
首先分析下首页的页面可以使用 Material
的 Scaffold
脚手架组件构建, 第一部分使用 APPBar
, 第二三部分作为 body
部分. 另外在上面构建life
类的时候, 用到了 ScopedModel
进行状态管理. 所以这里在所有组件的根节点上先创一个 ScopedModel
组件, 使得后面的所有组件都能够直接通过 model
内的 life
对象且, 当对应内容修改时, 能够自动更新视图. 关于状态管理组件 ScopedModel
介绍参考这篇文章 ScopedModel 和官方文档.
具体实现代码如下:
1 | import 'package:flutter/material.dart'; |
第一部分主要是一个居中的日期和一个按钮:
1 | appBar: AppBar( |
这里的字体是自定义的, Flutter
使用自定义字体的方法:
asset
下:pubspec.yaml
下编写资源名字和相对路径:1 | flutter: |
fontFamily
调用:1 | Text( |
第一部分右边的按钮是可以弹出时间选择器进行生日设置并保存且更新视图, Flutter
内置了时间选择器, 可以使用 showDatePicker
构建, 完整代码如下:
1 | void showDefaultYearPicker(BuildContext context, Life life) async { |
Body
由第二部分: 一个网格和第三部分: 一个表示剩余时间的 Text
组件.
新建 grid.dart
文件, 然后创建一个有状态组件 Grid
, 把 life
作为传入组件的必要参数:
1 | import 'dart:math'; |
设置一个 List
用于存放网格的随机颜色:
1 | static List<int> colors = [ |
网格是由一个一个小格子组成, 这里使用 Border
组件实现. 还需要定义一个根据对应的位置生成 Border
的方法:
1 | static BorderSide _borderThin = |
网格可以看成是由 30
行组成, 每一行有 30
个 border
组件, 先定义生成一行的方法:
1 | List<Widget> rowDetail(rowIndex) { |
再定义生成所有行的代码:
1 | List<Widget> rowList() { |
最后把生成的所有行使用 Column
组件包起来就OK了, 最后完整的 grid.dart
如下:
1 | import 'dart:math'; |
然后在 main.dart
里引用 grid.dart
代码, 并在调用 Text
组件实现第三部分功能:
1 | ScopedModelDescendant<Life>( |
最后完整的 main.dart
文件的内容如下:
1 | import 'package:flutter/material.dart'; |
基础安装过程和一般的 arduino
开发板安装过程是一样的.官方文档给出了详细的教程,基本分以下几步:
Ardino IDE
中 在“文件” - “首选项”
中 附加板管理器URL
框中输入: http://digistump.com/package_digistump_index.json
转到 工具
菜单,然后选择 板
子菜单 - 选择 板管理器
然后搜索选择 Digistump AVR
软件包, 安装即可.
Ubuntu
系统是无需安装的驱动的, 多数 Windows
系统也可以自动安装驱动, 如果没有则需要自行安装.
另外 Ubuntu
系统需要修改配置文件, 在 Linux
中所有设备都是文件, 所以要正确识别 Digispark
板子, 就需要修改读取当前设备后如何识别.
编辑配置文件:
1 | sudo vim /etc/udev/rules.d/49-micronucleus.rules |
把内容修改为:
1 | SUBSYSTEMS=="usb", ATTRS{idVendor}=="16d0", ATTRS{idProduct}=="0753", MODE:="0666" |
修改完成后重新加载配置文件:
1 | sudo udevadm control --reload-rules |
一开始不懂事, 插入之后怎么也找不到端口. 经过搜索才知道, 这块板子接上电源只保持 5s
的连接, 然后自动运行板子内的程序. 所以正确的烧录方式是先点击上传, 等到终端提示接入设备的时候, 再接入设备烧录程序.
出现上述提示后再插入板子, 检测到板子后会自动上传, 如下图所示:
电脑接口是 USB 3.0
明明可以连接 ESP32
, ESP8266
, Arduino UNO
等开发板, 偏偏对这块板子不感冒, 测试了其他几台电脑, 有一些电脑也有这个问题且基本是 USB3.0
的口子有问题. 想象之中 USB3.0
是向下兼容的, 应该不会有问题…
最后的解决方案是插到 USB集线器
或者 type-c
转接头等 hub
设备上就可以正常识别了. 个人感觉应该还是驱动和兼容性哪里有问题, 暂时没有找到好的办法.
具体表现为出现以下报错:
1 | This tool doesn't know how to upload to this new device. Updates may be available. Device reports version as: 2.2 |
Digispark
这块板子用到的引导程序是 micronucleus
, 之前安装板子的时候, 会自动安装这个程序. 但是由于自动安装的程序版本过低达不到板子的要求导致无法正常上传程序.
解决方法是使用开源代码自行编译一个最新版的 micronucleus
替代自动安装的程序. 具体方法如下:
编译安装需要 libusb-dev
, 当然还有 make
等编译 c++
程序必须的工具, 如果没有的话下面编译的时候会报错, 根据报错自行安装解决即可.
1 | sudo apt-get install libusb-dev |
micronucleus
的源码可以在 github
上获得: https://github.com/micronucleus/micronucleus
1 | git clone https://github.com/micronucleus/micronucleus.git # 下载代码 |
如果编译正常会有以下输出, 并在目录下出现编译得到的程序: micronucleus
:
如果编译完成, 目录下会有 micronucleus
程序, 首先把程序赋予执行权限:
1 | sudo chmod 777 ./micronucleus |
然后找到 arduino ide
默认的开发板存放位置, 一般是 /home/zhou/.arduino15/packages
.
在 /home/zhou/.arduino15/packages
中找到 digistump/tools/micronucleus
, 在此路径下是目前自动安装的程序的版本:
将刚刚编译好的程序移动到目录下且替换原有的程序即可
1 | sudo cp micronucleus /home/zhou/.arduino15/packages/digistump/tools/micronucleus/2.0a4 |
替换完成后重新上传代码即可解决
]]>蜂鸣器分为有源和无源两种,这次的 MH-FMD
是无源蜂鸣器, 也就是需要通过 PWM
更改频率实现对蜂鸣器的控制.
关于 PWM
可以参考这篇文章MicroPython-ESP32 PWM
代码分为三部分,第一部分导包和创建 PWM
对象:
1 | from machine import Pin,PWM |
第二部分为各个音符的频率和歌曲对应的音符和节奏:
1 | CL = [0, 131, 147, 165, 175, 196, 211, 248] |
第三部分为循环歌曲的音符实现播放歌曲:
1 | for i in range(len(song)): |
完整的代码:
1 | from machine import Pin,PWM |
PWM
的全称为 Pulse Width Modulation
, 翻译成中文是 脉冲宽度调节
, 是把模拟信号调制成脉波的技术. 数字信号只有低电平 0
和高电平 1
两种. 通过以快速切换高低电平来控制传感器引脚上的电压(和电流)的平均值.
举个 LED
灯的例子: 控制LED, 亮 1s
灭 1s
往复循环, 肉眼可见 LED
在不断闪烁. 然后把间隔时间缩短为: 亮 100ms
灭 100ms
往复循环就可以看到 LED
在快速闪烁. 把这个变换的持续缩小, 小于肉眼的视觉暂留时间, 人眼就会分辨不出来 LED
在闪烁, 而此时 LED
的亮度处在灭与亮之间,这时候 LED
就达到了正常的 1/2
亮度.
当然亮的时间和暗的时间是可以不相等的, 这个比例是可以调节的. 通过这个比例的调节可以实现控制 LED
亮度的目的. 这个比例就是占空比(duty
), 占空比的完整定义: 在一个周期内, 高电平时间占总体周期的比例. 例如假设 PWM
的控制周期为 10ms
, 其中 8ms
为高电平, 2ms
为低电平, 则占空比就是 8/10 = 80%
在多数单片机中, 占空比并不是百分比. ESP32
的占空比取值范围为 0 <= duty <= 1023
. 其本质就是把工作电压 0-3.3v
划分为 1024
个离散值.PWM
的第二个属性就是频率. 根据常识,频率为控制周期T的倒数. 假设 10ms
就是控制周期,那频率就是 1 / 0.1 = 10HZ
. 频率的取值范围也是由由硬件决定的, ESP32
的 PWM
频率范围为 0 < freq < 78126
.
MicroPython
提供了专门针对 PWM
的模块.
首先是使用相应的引脚创建对应的对象, 然后调用对象的 init
方法传入频率和占空比初始化对象:
1 | from machine import Pin,PWM |
也可以在新建 PWM
对象的时候直接传入频率和占空比:
1 | from machine import Pin,PWM |
除了定义的时候申明外, PWM
类还提供了单独设置频率和占空比的方法:
1 | led.freq(1000) # 设置频率 |
使用完了 PWM
需要关闭对象:
1 | led.deinit() |
1 | from machine import Pin,PWM |
结合 MQTT
就可以写出一个可以通过网络控制的 LED
灯节点:
1 | from machine import Pin,PWM |
ESP32
共有 38
个引脚如下所示:
功能简介 | 引脚编号 |
---|---|
ADC : 模拟信号采样 | 32, 33, 34, 35, 36, 39 |
DAC : 模拟信号输出 | 25, 26 |
UART : 串行通信 | 1(TX0),3(RX0) ; 10(TX1),9(RX1) ; 17(TX2),16(RX2) |
TOUCHPAD : 触摸传感器(检测手指接触引脚产生的电容差) | 0, 2, 4, 12, 13, 14, 15, 27, 32, 33 |
SPI : SPI总线接口 | hspi(14,12,13,15) vspi(23,19,18,5) |
I2C : I2C总线接口 | SDA(21) SCL(22) |
除了 GPIO34, GPIO35, GPIO36, GPIO39 这四个 GPIO
引脚只能作为输入,而无法作为输出外,其他所有 GPIO
引脚都能进行双向输入输出
理解使用引脚接收传感器数据的过程,首先要理解 信息
, 数据
和 信号
的概念:
信息是对现实事物存在方式或运动状态的描述,信息的表现形式可以是数学、文字、语言、图形及动画等,信息使这些表示形式包含了具体的内容和意义。
数据是信息的载体,它是信息的表示形式,可以是数字、字符、符号等。单独的数据没有实际的含义。数据和信息既有区别又有联系,数据是独立的,虽然数据本身并没有含义,但把数据按一定规则、形式组织起来时,就可以传达某种意义,这种具体意义的数据集合就是信息。例如,单独的英文字母:“h”,“o”,“r”,“s”,“e”,是没有意义的,但我们把它们组合起来成为一个单词“horse”时,它就具有“马”这样一个信息的意义了。
信号是数据在传输过程中的具体物理表示形式,具有确定的物理描述,如电压、磁场强度等。
使用引脚的目的是为了发送或者接收一些信息,而这些信息是用数据表示的.计算机底层是没有办法理解数据的,但是能理解电平的高低,所以数据最终要以信号的方式从引脚流入或者流出,然后通过程序的处理转换为数据,被人类使用.也就是说信号是运载数据的工具,信号是数据的载体。信号包含光信号、声信号和电信号等.在今天的计算机内部处理中主要是 电信号
(不是全部,比如计算机网络中的光纤是光信号).
电信号的本质是随着时间而改变电压或电流大小.电信号主要分为两种,一种是数字信号,另一种是模拟信号.
数字信号: 电压或者电流的取值是离散的,幅值表示被限制在有限个数值之内。最常用的二进制码就是一种数字信号。在这种二进制体系中,用 0
表示 低电平
, 1
表示 高电平
.(在编程的时候直接控制引脚 高电平
和 低电平
就可以了,具体高低电平对应什么电压,雨我无瓜)
模拟信号: 电压或者电流的取值是连续的.
假设横轴为时间可以把两种信号的波形化成下面的图展示出来:
MicroPython
通过 machine
模块的 Pin
类控制各类 GPIO
引脚.
定义 Pin
可以传入以下参数:
id:任意引脚号
mode:引脚模式(输入,输出)
pull:是否接入上拉\下拉电阻
value:引脚电平状态(0 —低电平,1 —高电平)
1 | pin = Pin(12, mode=Pin.OUT, pull=None, value=0) |
在pin
定义中 id
为必填参数,其他参数可以通过 init
方法传入. init
方法用于初始化引脚
1 | from machine import Pin |
通过 value
方法,控制引脚发出的信号.
1 | pin = Pin(12, mode=Pin.OUT, pull=None, value=0) |
设置引脚的中断处理程序,在引脚的电平满足条件时运行回调函数
trigger
可选的参数有:
Pin.IRQ_FALLING — 下降沿触发
Pin.IRQ_RISING — 上升沿触发
handler
为中断被触发之后的回调函数
1 | pin = Pin(32, Pin.IN) |
然后用手触摸 32
号引脚之后,可以在终端看到输出.
使用 ESP32
开发板点亮上面的 LED
灯.点亮 LED
灯两种方式:
GND
, 当输出端输出高电平时灯点亮;V5
, 当输出端输出低电平时灯点亮.首先从 machine
模块中导入了 Pin
这个类. 然后定义引脚为输入类型还是输出类型, 最后通过给引脚写入高低电平来控制 LED
灯的亮暗:
1 | from machine import Pin |
NeoPixels
也被称为 WS2812 LED
彩带,是连接在一起的全彩色 led
灯串。你可以设置他它们的 R
红色, G
绿色和 B
蓝色值(0~255). neopixel
模块可通过精确的时间控制,生成 WS2812
的控制信号.
控制 NeoPixels
需要两个包,一个 NeoPixels
和一个 Pin
1 | from machine import Pin |
NeoPixel
对象的构造函数如下:
1 | NeoPixel(pin, n, bpp=3, timing=1) |
参数 | 描述 |
---|---|
pin | 输出引脚(灯带采用单总线协议,只需要一个输出引脚即可) |
n | 灯珠个数 |
bpp | 有两个值 3 和 4 .默认为 3 . 3 表示使用 3 元组RGB控制灯珠的颜色, 4 表示对于具有 3 种以上颜色的灯珠,例如RGBW像素或RGBY像素,采用4元组RGBY或RGBY像素 |
timin | 有 0 和 1 两个值,默认等于 0 ,为 400KHz 速率;等于1 的时候为800KHz 速率 |
写入数据有两种方式,一种是指定灯珠和颜色:
1 | np[0] = (255, 0, 0) # 设置第一个LED像素为红色 |
另一种是填充所有灯珠颜色:
1 | np.fill( (255, 0, 0) ) |
不管上面哪一种设置方式,设置完成后都需要调用 np.write()
接口讲准备好的数据传输给灯带.
1 | np.write() |
以下是一个示例程序,功能是使用
1 | from machine import Pin |
UART
stands for Universal Asynchronous Receiver/Transmitter. It’s not a communication protocol likeSPI
andI2C
, but a physical circuit in a microcontroller, or a stand-alone IC.
摘自 Circuit Basics | http://www.circuitbasics.com/basics-uart-communication/
简单来说,就是一种比较常用的通信协议,使用 RX
和 TX
两个引脚进行通信.使用Bandrate
(波特率)控制通信速率
RX
表示耳朵,用来通信过程中接收数据
TX
表示嘴巴,用来通信过程中发送数据
BandRate
表示传输和接收数据的速度,单位是 bit/s
也就是每秒钟接收或者发送多少个 bit
,收发双方必须保证使用同一个波特率才能保证数据的正常收发.
ESP32
开发板引脚基本相同,这里采用某宝上果云科技的 GOOUUU-ESP32
开发板,然后从网上找了 NodeMCU-32S
的开发板的引脚图,并没有什么区别.
ESP32
共有三对串口:
组号 | RX | TX |
---|---|---|
0 | GPIO3 | GPIO1 |
1 | GPIO9 | GPIO10 |
2 | GPIO16 | GPIO17 |
第一组为板子上标出的 RX
和 TX
引脚, USB
连接板子的使用需要占用 0
号串口引脚,因此可用的只有 1
号和 2
号引脚.除了天生的串口引脚外, ESP32
还可以自定义 GPIO
引脚作为串口引脚.(PS.部分 ESP32
的 34,35,36,39
这四个 GPIO
引脚只能作为输入,但是作为 TX
必须要能够输出,作为 RX
必须要能够输入,所以在选择 GPIO
引脚的时候需要注意这个问题)
以下内容来源于 MicroPython
官方文档
UART
模块可以直接从 machine
中导入:
1 | from machine import UART |
UART作为一个对象,其构造函数定义如下:
1 | UART(id, baudrate, databits, parity, rx, tx, stopbit, timeout) |
参数 | 描述 |
---|---|
id | 串口编号,总共有三对串口,但是0号通常被占用,所以常用的是1和2 |
bandrate | 波特率,常用波特率有: 9600 115200 |
databits | 数据位,是通信中的每一个字节单元所包含的比特位数。可选的值为 6, 7, 8, 9,默认 8 个位,也就是一个 byte |
parity | 基础校验方式 ,None不进行校验,0 表示偶校验 1 表示奇校验, 校验方式通常是传感器给出的,如果既不是奇校验也不是偶校验而是其他校验方式,需要在这里写None后,接收数据并自行校验 |
rx | 接收口的GPIO编号 |
tx | 发送口的GPIO编号 |
stopbit | 停止位个数,用来表示数据收发完成,可以为1或2, 默认为1 |
timeout | 超时时间,取值范围: 0 < timeout ≤ 2147483647 |
这里以 HH06.03
分贝传感器为例,可以使用以下代码构造一个串口通信对象
1 | from machine import UART |
length
表示从串口读取数据的长度,该函数若长度未指定则读取所有数据。
1 | uart.read(10) # 读入10个字符 |
顾名思义,就是从串口读取一行数据
1 | uart.readline() # 读入一行 |
读入数据并且保存到缓冲区
向串口写入数据,函数会返回 data
的长度
1 | uart.write(b'\xbb\xaa') # 向串口写入2个Byte |
检查待读入数据的长度,并返回.如果没有数据则返回长度 0
采用 HH06.03
与 ESP32
进行串口通信作为示例.该模块的参数如下:
项目 | 参数描述 |
---|---|
产品型号 | HH_06.03 |
模块功能 | 检测声音分贝值 |
通信形式 | 主动型 |
工作电压 | DC 5.0V |
工作电流 | 29mA/5.0V |
通信方式 | 串行 TTL/5V |
测量范围 | 40-130dB |
频率范围 | 40-8kHz |
计权方式 | A 计权 |
分辨率 | 0.1dB |
帧间隔 | 约 500ms |
PCB 尺寸 | 47.12*28.00mm |
整体尺寸 | 54.4628.009.70(H)mm |
模块采用如下主动发送协议:
1 | 一、 串口参数及协议 |
串口收到的数据类型为 byte
,在 Python3
中需要使用 int.from_bytes(byteData, 'big')
转换为整数方便计算,完整的代码如下:
1 | from machine import UART |
ESP32
开发板只有烧录了 MicroPython
的固件,才能使用 MicroPython
进行编程。
需要的工具有:
- Python 3
- ESPTOOL 工具模块
- Micropython 固件
由于MicroPython是完全开源的,所以MicroPython的固件你可以自己从源码编译,也可以下载编译好的固件。
下载地址:https://micropython.org/download/#esp32
进入下载地址后找到对应的开发板下载即可
可以使用 pip
安装这个模块 pip install esptool
(有时会出现环境问题),也可以直接在乐鑫官方 Github
下载工具.下载链接
全部下载完成后即可烧录固件
在开始之前,你需要知道你插入到电脑上的 ESP32
设备在系统中的端口号。不同于 Windows
,根据 linux
中一切皆是文件的思想,任何设备都可以在文件系统中的 /dev/
目录下找到,通常以 ttyUSB+数字编号
的方式为这些 USB
设备命名。可以使用如下的命令来查看:
1 | ls -l /dev/ttyUSB* |
为了保证固件刷入的成功率,先要对 ESP32
的 flash
进行清除:
1 | sudo python esptool.py --port <你的端口号> erase_flash |
Ubuntu
下如果没有其他串口设备,那端口号一般都为 /dev/ttyUSB0
所以可以通过以下命令擦除 Flash
:
1 | sudo python esptool.py --port /dev/ttyUSB0 erase_flash |
找到之前下载的固件存放路径,使用 esptool
工具烧写固件进板子:
1 | sudo python esptool.py --chip esp32 --port <你的端口号> write_flash -z 0x1000 <你的固件的完整路径> |
烧写的过程中不要断开,等待烧写完成
REPL
是以下四个英语单词的首字母缩写:
Read (读入)
Evaluate(执行)
Print (打印)
Loop (循环)
这四个单词准确的概括了交互式解释器环境的特点,因此 REPL
通常也就代指交互式解释器环境。
一般的脚本语言都拥有自己的解释器, MicroPython也不例外,我们可以把写好的脚本文件一次性扔给解释器,同样的也可以这样和解释器进行交互.一般可以通过 USB
和 WIFI
两种方式连接到板子的 REPL
,这里仅仅是为了测试固件烧写是否成功,所以采用 USB
连接后测试下即可.
picocom
:picocom
是基于命令行的串口(终端)调试工具,当然有其他你热爱的工具也可.
1 | sudo apt-get install picocom |
picocom
连接 ESP32
板子:使用一下命令连接
1 | sudo picocom -b 115200 /dev/ttyUSB0 |
-b 是指定波特率 boundrate
为 115200
,如果你嫌慢,可以自己调整
/dev/ttyUSB0
就是端口号,需要替换为你自己的端口号
如果没出出现 >>>
命令行提示符标识,请按下回车,如果还未出现命令行提示符,说明正在执行其他程序,你需要先 CTRL+C
中断程序,接下来就可以在终端里面敲入 Python
代码为所欲为了.
首先在下载地址下载安装下载地址
下载完成后双击运行,然后一路确定。如果出现提示是否安装驱动记得选安装。
首先在下载地址下载安装下载地址
下载完整后,解压然后移动到你希望存放软件的路径中,然后运行软件内的 install.sh
进行安装
1 | cd arduino-1.8.9 |
然后你可以在你的开始菜单找到安装好的软件
Arduino IDE 官方提供可下载的开发板有限,对于 ESP32
、 NodeMCU
这些板子,需要单独添加开发板下载地址。在菜单栏找到 文件-首选项-设置
:
会看到下面这个弹出窗口:
我一般用到的是以下这么几个:
1 | https://github.com/espressif/arduino-esp32.git, |
上面的配置完成后,就可以在 开发板管理
里面搜到配置的开发板了。打开 工具->开发板->开发板管理器
等待索引完成后,输入需要的开发板,比如: esp8266
进行搜索,然后选择需要的固件,比如: esp8266 by ESP8266 Community
安装即可:
很多板子,比如:Node MCU V0.9
和 Node MCU V1.0
都是通过串口通信协议把编译好的 Arduino
程序烧进板子里的,这也就是为什么烧写的时候不能打开串口,并且如果烧写的时候占用了串口的引脚会出错的原因。但是板子连接计算机用的是 USB
的口子,多数 Linux
系统发行版和 MAC
系统都是内置支持 USB to UART
, Windows
并没有内置支持,但是当接入 USB
设备的时候,会自动安装驱动程序。也就是说正常情况下,你可以跳过本步骤。然而如果计算机不是一个充满了未知的东西,那不知道该少了多少乐趣。所以本人就遇到的驱动程序没有自动安装的情况,需要用下面的方法安装:
配置环境的最后一步,总是使用 Hello World
程序来测试下环境配置是否出现问题,这里可以随便找一个示例程序测试下:
首先打开示例程序,比如下面这个:
1 | // digital pin 2 has a pushbutton attached to it. Give it a name: |
然后选择开发板,比如 ESP8266
:
使用 USB
连接计算机,点击右上角的烧写按钮开始编译烧写,最后打开串口监视器查看即可
本文基于 Ubuntu 18.04
系统通过使用 Wireshark
软件对微信网页版进行抓包来总结 Wireshark
抓包软件的使用和计算机网络的基础知识.
Wireshark
是一款网络分包分析软件.主要功能是获取网络封包,并显示出详细资料.这里主要通过这个软件来学习网络协议.
直接使用 apt
安装即可:
1 | sudo apt-get install wireshark |
如果不给授权,目前的 Wireshark
大概率是会:
所以需要设置用户权限
1 | sudo dpkg-reconfigure wireshark-common |
在弹出的选项中选择 Yes
如果仍然不行,就手动把用户添加到 wireshark
的用户组中,然后赋予执行权限
1 | sudo usermod -a -G wireshark $USER |
加入用户组后注销用户重新登录,使用命令行或者应用启动器启动软件,就可以顺利实现抓包
机器上有两块网卡,一块有线网卡,一块无线网卡,所以需要选择其中一块网卡用于捕获数据包.菜单栏选择 捕获-选项-管理接口
,选择需要捕获的网卡.
过滤器有两种,一种捕获过滤器一种是显示过滤器,捕获过滤器用来过滤需要什么数据包,用于控制捕获数据的数量,以避免产生过大的日志文件.显示过滤器用于在捕获的记录中过滤筛选出需要的数据包,相当于一个查找功能.其中捕获过滤器在 捕获-捕获过滤器
中编辑规则,然后在捕获开始时选择相应的规则.而显示过滤器则是在 分析-显示过滤器
中编辑.
首先打开软件,然后打开网页进行抓取如下:
查看 DNS
请求包,看到 https://web.wechat.com/
对应的 ip
为 183.232.103.154
,所以在过滤规则中写 ip.addr == 183.232.103.154
进行分析:
TCP
完成的建立连接到释放过程如下:
从上图可知建立连接需要三次握手,从抓到的数据中可以看到三次握手的数据包:
在 OSI
模型中 TLS/SSL
位于表现层,从抓到的数据中可以分析出 微信使用的 TLS/SSL
的工作原理
Client Hello
客户端建立 HTTPS
连接时会发送一个 ClientHello
消息:
Server Hello
服务器端收到 ClientHello
消息后,会回复客户端一个 ServerHello
消息,所带信息如下:
Change Cipher Spec(Client发送密钥改变通知)
客户端生成 master secret
之后即可发送密钥改变通知通知服务器,之后就使用对称密钥 master secret
来加密数据:
至此环境都OK,下面分析登录协议
在上面的过程中计算机已经和微信网页版的服务器建立了 TCP
连接,并且客户端每隔一段时间都向服务器发送数据保持链接不断开,而当用手机扫码登录后,服务器给客户端发送数据进行登录:
然后客户端发起了 DNS
查询请求,查询了 wx2.qq.com
的地址, DNS
服务器返回了两个地址,从之后的数据中可以看出客户端选择了第一个地址进行连接:
DNS
查询完成后,自然就是 TCP
三次握手建立连接,然后 SSL
交换密钥,最后客户端页面跳转进入微信网页程序对话框页面(https://wx2.qq.com)
进入界面之后,需要加载用户头像,各类会话信息等等,这时候又抓到了大量 DNS
查询请求,有 js.aq.qq.com
,res.wx.qq.com
,js.aq.tcdn.qq.com
等等,根据域名大概猜出是在查询 CDN
加速的静态 js
文件等,以及各种微信资源(其中应该包括用户头像,会话列表等等).由此也可见微信的不同功能是分给不同的服务处理的,这样看微信应该采用的是微服务的架构设计.
然后就是跟刚刚查询到的各种地址各种握手建立 TCP
连接,并且由于连接都是 HTTPS
协议的,所以每次连接建立都要执行 SSL
那一套东西:
建立连接以后,就是各类请求获得数据
至此登录也已经完成,网页已经显示出所有信息
为了方便,这里先把显示地址改为显示域名:
设置完成后,使用网页找好友给他发送四条消息,然后开始分析
首先是 DNS
查询,查询了 webpush.wx2.qq.com
的地址,然后根据客户端选择的地址 183.232.103.146
进行过滤,分析给好友发送消息的全过程:
第一步是进行 TCP
握手认证,建立连接,由客户端发送 SYN
请求,服务器收到后发送 SYN
和 ACK
进行确认,最后从客户端向服务器发送 ACK
完成认证.
第二步是 SSL
过程和之前打开网页的过程一样从 Client Hello
到最后发送 secret
第三步之后就是不断进行交互,不断进行连接.
其中客户端发送完消息后,还会发送 Fin
进行连接请求,如果这时候接着发送消息,由下图可见,客户端又发送了一个 ACK
恢复连接
使用网页版发送一些消息,然后抓取数据.
首先可以看到的是,交互的地址仍旧是 183.232.103.146
也就是说网页版的微信群聊和私聊用的是同一个服务器,根据这个地址进行过滤分析:
可以看到跟上面私聊的过程完全相同,并且由于上面已经建立了连接,群聊直接使用上面建立的连接发起 POST
请求发送数据.
了解Hibernate的基本工作原理;
理解Hibernate的配置;
掌握简单映射文件的编写;
已知表结构如下:
序 号 | 列 名 | 数据类型 | 长 度 | 小数位 | 主 键 | 字段说明 |
---|---|---|---|---|---|---|
1 | Class_id | int | 4 | 0 | √ | 班级编号 |
2 | Class_Name | varchar | 80 | 0 | 班级名称 |
序 号 | 列 名 | 数据类型 | 长 度 | 小数位 | 主 键 | 字段说明 |
---|---|---|---|---|---|---|
1 | Stu_no | varchar | 2 | 0 | √ | 学号 |
2 | Class_id | int | 4 | 0 | 班级编号 | |
3 | Stu_Name | varchar | 80 | 0 | 姓名 | |
4 | Stu_sex | varchar | 2 | 0 | 性别 |
根据表结构可以得出建表语句
1 | SET NAMES utf8mb4; |
java
工程引入 jar
包跟 eclipse
有点不同,选择工具栏上 File
—> Project Structure
—> Libraries
—> 点击 +
—> Attach Files or Directories
—> 选择自己需要的 jar
包:
首先需要配置并连接数据库,如下图所示使用 idea
连接数据库,根据表结构自动生成建立班级和学生的类,并编写 hbm
文件建立类和数据库表的映射:
生成的文件目录如下:
生成的代码如下:
hbm.xml
1 |
|
TblClass
1 | package xyz.zhzh.classes.model; |
TblStudent
1 | package xyz.zhzh.classes.model; |
Hibernate
主配置文件编写在 src
下新建 hibernate.cfg.xml
,然后写入以下内容:
1 |
|
新建 xyz.zhzh.classes.util
包,然后新建 HibernateUtil.java
类,写入:
1 | package xyz.zhzh.classes.util; |
可以看到终端打印出 sql
语句,并且数据库中已经存入相应信息,视为配置映射文件没有问题。
由于学生和班级之间为多对一的关系,所以需要在学生类增加一个班级的属性,班级类中增加一个 List
的学生属性,并且在 hbm.xml
配置文件中增加该映射关系。
首先在 TblStudent
中添加:
1 | private TblClass studentClass; |
在 TblClass
中添加:
1 | private Set<TblStudent> students; |
在 hbm.xml
中两个类里对应添加以下映射关系
1 | <set name="students"> |
1 | <many-to-one name="studentClass" class="xyz.zhzh.classes.model.TblClass" column="Class_id"/> |
至此配置完成,可以正式开始开发
]]>通过使用 Flare
美化游戏界面,下面是美化前和美化后的效果:
项目源码为 Flare官方Demo
首先下载 master
分支下的代码,切换到项目路径,安装相应的包文件:
1 | flutter packages get |
安装好项目所必须的依赖后,运行程序确认是否能够正常使用。
打开 pubspec.yaml
找到 dependencies
,在依赖中添加 flare
的包:
1 | dependencies: |
然后再次运行包安装命令,安装新添加的包:
1 | flutter packages get |
回到需要调用这个包的文件 main.dart
中,导入这个包:
1 | import 'package:flare_flutter/flare_actor.dart'; |
导入后编译器不报错即视为成功。
把需要使用的动画素材文件放到项目根目录下 assets
文件夹中,然后回到 pubspec.yaml
中,找到被注释掉的 assets
取消注释,添加一会需要用到的静态动画资源:
1 | flutter: |
首先在入口程序处找到 Scaffold
,这边需要用到背景的设置,所以采用 Stack
这种重叠层的布局方式,然后在 children
属性中传入 FlareActor
和 Scaffold
组件。FlareActor
这个组件首先需要传入一个 String
表示展示的动画素材的路径,还有一些其他的属性控制大小、位置显示动画等,使用示例:
1 | home: Stack( |
这里需要把 Scaffold
的背景颜色设置为透明色,这样才能够正常地显示出背景素材。完成之后保存代码使用 Flutter
的热重载, App
显示背景即可视为添加成功。
根据最终效果图片,需要在初始界面加上一个火箭的素材,首先在代码中找到相应的位置:
1 | Scaffold( |
可以看到在 Scaffold
的 body
属性根据 eG
的值是否为 1
会显示两种不同的组件,而第一个 Center
组件就是初始界面。分析这个界面由 Padding
、Text
等几个组件在 Column
中显示。可以分析出我们需要在 Padding
上面动画素材的组件 FlareActor
,该组件用法如下:
1 | FlareActor( |
但是由于素材尺寸比较大,所以需要使用 Container
组件包裹素材组件,通过控制 Container
的尺寸来控制素材的尺寸:
1 | Container( |
最后完整的代码如下:
1 | body: eG != 1 |
观察最终效果图,发现在初始界面需要替换的不仅背景还有开始的那个按钮,刚才已经找到了构成初始界面的那个组件,找到那个带有监听事件的组件 GestureDetector
,把组件的 child
属性按照上面添加背景素材那种方式改为添加开始按钮:
1 | GestureDetector( |
游戏界面中需要修改的只有三处,添加方式跟上面添加素材的方式一样,使用 FlareActor
组件调用素材,然后用 Container
包裹这个组件控制它的大小,完整的修改代码:
1 | : Column( |
通过使用 Flutter
构建 BW丨BBS
的静态页面,详细介绍 Flutter
的核心组件。
首先新建一个 Flutter
项目,可以看到下面的代码:
1 | void main() => runApp(MyApp()); |
这是程序的入口处,顺着程序的入口 MyApp
就可以看到遇到的第一个核心组件 MaterialApp
。
MaterialApp
表示为一个使用 Material
界面风格的应用程序,它封装了应用程序实现 Material Design
所需要的一些 widget
,大多数项目的界面都应该基于 MaterialApp
进行呈现。
该组件三个属性的基本用法如下:
1 | MaterialApp( |
home
属性指定程序的主界面,如果直接指定 Text
则为整个界面都为文字。但是在实际开发过程中不仅仅是显示文字,还会有顶部栏、主体、底部栏或者侧边栏的划分,并且显示的内容可能还会改变,这就需要用到需要认识的第二个核心组件 Scaffold
。
该组件是页面结构的脚手架,包含了页面的基本组成单元:
appBar
属性使用需要传入一个 AppBar
组件:
1 | Scaffold( |
title
属性传入一个 Text
组件作为 appBar
显示的内容;
centerTitle
属性传入的是一个布尔值,控制内容是否居中显示;
actions
属性指定右侧的行为按钮,传入了一个 IconButton
组件,这个组件传入 icon
作为显示的图标, onPressed
表示点击触发的事件。
侧边栏抽屉区域,用法比较简洁,主要代码示例如下:
1 | drawer: Drawer( |
在 Drawer
组件中,通过 child
传入一个 ListView
组件。这个组件相当于把自己 children
中得到的组件作为一个列表排列。列表的第一项传入 UserAccountsDrawerHeader
用来展示用户信息,之后使用 ListTile
展示选项和 icon
,示例代码如下:
1 | drawer: Drawer( |
滚动列表,可以很单纯中填充固定个数的内容,也可以循环渲染列表数据。将需要的内容节点,直接写入到 ListView
组件的 children
属性中即可,代码示例如下:
1 | ListView( |
组件表示把图片按照圆形展示,然后通过自己的 backgroundImage
属性传入一个 NetworkImage
组件,这个组件用来通过地址加载网络上的图片;
一般都用在侧边栏 Drawer
组件中:其中, accountName
和 accountEmail
是必选项。另外,头像区域使用 currentAccountPicture
进行指定;背景区域使用 decoration
属性进行指定;
组件这里还用到了一个属性 decoration
,很多组件都有这个属性,这个属性是用来美化当前控件的,这里通过这个属性给头像区域加图片。首先传入一个 BoxDecoration
组件,然后给它的 image
属性传入图片,这里直接传入是不可以的,需要传入的是 DecorationImage
组件。然后在 DecorationImage
组件中指定 image
。正常显示图片后会发现如果侧边栏的尺寸和导航栏尺寸不一致的话会有留白的情况,通过给 fit
属性指定 BoxFit.cover
使得图片填充整个组件所在的区域;
详细代码示例如下:
1 | UserAccountsDrawerHeader( |
这个属性用来控制边距。这里通过使用 EdgeInsets.all(0)
来消除侧边栏顶部的那一行空白。
1 | bottomNavigationBar: Container( |
一个常用的布局组件,常用的属性:
child【子节点】
padding【内容距离盒子边界的距离】
1 | padding: EdgeInsets.all(10) |
1 | margin: EdgeInsets.all(10) |
1 | decoration: BoxDecoration( |
1 | body: Center( |
虽在这个 Codelabs
中没有用到 floatingActionButton
,但是这个属性依旧是 Scaffold
中重要的属性,它控制的是右下角浮动按钮区域
在记录点击次数这个经典示例中,使用示例代码如下:
1 | int _count = 0; |
StatefulWidget
是有状态控件,这样的控件拥有自己的私有数据和业务逻辑,基本定义过程如下:
1 | // 定义一个 电影详情 控件,继承自 StatefulWidget |
1 | // 这个继承自 State<T> 的类,专门用来定义有状态控件的 业务逻辑 和 私有数据 |
1 | // 这个继承自 State<T> 的类,专门用来定义有状态控件的 业务逻辑 和 私有数据 |
本文基于以下环境:
首先 Node.js
是单进程的,我们可以通过多开 Node.js
并配合 Nginx
来实现多进程 Node.js
负载均衡,然后要做的项目需要用一些前端静态页面需要通过 Nginx
代理,提高性能。并且还需要通过 Nginx
给域名配置 HTTPS
。所以首先要安装 Nginx
1 | yum -y install nginx |
正常打印Nodej
在阿里云申请且下载 SSL证书
,然后使用 SFTP
上传到 /data/release/nginx
目录,也可以是别的目录
上传完证Nodejinx,进入服务器的
/etc/nginx/conf.d目录,新建一个
weapp.conf文件,打开编辑,写入如下配置(将配置里
wedolist.zhzh.xyz和
SSL证书` 路径修改为自己的):
1 | upstream app_weapp { |
配置完成之后,输入:
1 | nginx -t |
检测配置文件是否成功,配置成功后,输入 nginx
,启动 Nginx
。通过配置的域名访问域名,如果跳转到 HTTPS
,且显示 502 Bad Gateway
,则表示配置成功。
Wafer2小程序NodejMySQL 5.7
及以上版本,这里选择 5.7
版本安装
1 | yum localinstaNodejysql.com/get/mysql57-community-release-el7-11.noarch.rpm |
第一次安装,设置数据库开机自动启动:
1 | systemctl enable mysqld |
完成后打印出运行状态,确认 MySQL
是否正常启动:
1 | systemctl status mysqld |
当 MySQL
第一次启动时,会为 root
用户生成临时密码。通过以下命令找到密码:
1 | grep 'temporary password' /var/log/mysqld.log |
运行该 mysql_secure_installation
配置 MySQL
的安全性:
1 | mysql_secure_installation |
系统会要求输入刚才从 log
中打印出来的临时密码,然后根据提示和个人需求设置新密码、是否删除匿名用户等等
首先登录 MySQL
,系统会提示输入数据库 root
用户的密码,输入完成后即进入 mysql shell
1 | mysql -uroot -p |
进去 mysql shell
后给 root
用户或者其他你需要远程连接的用户权限:
1 | use mysql; |
修改完成后重启 MySQL
就可以使用 Navicat
等工具远程连接数据库了
phpMyAdmin
是一个基于 PHP
的开源工具,它允许用户通过 Web
与 MySQL
数据库交互,管理用户帐户和权限,执行SQL语句,以各种数据格式导入和导出数据等等。
CentOS 7
附带 PHP 5
,但是 phpMyadmin
需要 PHP 7
,所以需要自行安装 PHP 7
1 | yum install http://rpms.remirepo.net/enterprise/remi-release-7.rpm |
默认情况下,PHP FPM
使用 apache
在 9000
端口上以用户身份运行。需要将用户更改为 nginx
从TCP socket
切换到 Unix socket
。为此,需要编辑 /etc/php-fpm.d/www.conf
需要修改的内容如下:
1 | user = nginx |
配置 user
是为了控制权限, 读写站点文件.
配置 listen.owner/group
是为了 nginx
有权限和 php-fpm
通信.
然后使用一下命令,保证 /var/lib/php
目录具有权限:
1 | chown -R root:nginx /var/lib/php |
最后启用 PHP FPM
服务:
1 | systemctl enable php-fpm |
没有报错就说明启动成功,在 Linux
中没有消息就是好消息
之前已经启用 EPEL
软件包了,这里可以直接进行安装,当然安装完成后需要把根目录的用户组改为 nginx
,方便后面使用 nginx
配置
1 | yum install phpmyadmin |
跟之前配置小程序域名一样,上传访问 phpMyAdmin
的域名的证书;进入服务器的 /etc/nginx/conf.d
目录,新建一个 phpmyadmin.conf
文件,打开编辑,写入如下配置(将配置里 XXX.zhzh.xyz
、 phpmyadmin
安装根目录 和 SSL证书
路径修改为自己的):
1 | server { |
Wafer
的 Demo
需要 7.6
以上版本的 Node.js
才能运行,目前最新稳定版本为 10.x
,yum
本身不提供 Node.js
的源,所以首先我们得切换源:
1 | curl --silent --location https://rpm.nodesource.com/setup_10.x | sudo bash - |
接着就可以直接通过 yum
安装了:
1 | yum -y install nodejs |
同理,我们可以通过如下命令验证 Node.js
是否安装成功:
1 | node -v |
该命令会返回当前 Node.js
的版本号,如果你看到了版本号大于 7.6
,则 Node.js
安装成功。之后需要使用 npm
安装腾讯提供的 wafer2
,这里先把 npm
源切换到腾讯云镜像,防止官方镜像下载失败:
1 | npm config set registry http://mirrors.tencentyun.com/npm/ |
到 Wafer2-startup 仓库下载最新的 Demo 代码,修改 server/config.js
:
1 | const CONF = { |
安装 pm2
作为后台服务管理工具
1 | npm install -g pm2 |
把 server
上传到服务器,使用 npm install
安装程序需要使用的包。然后打开之前配置好的 phpMyAdmin
新建数据库,数据库名需要跟之前的配置文件写得一样,编码为 utf8mb
,然后使用以下命令初始化数据库:
1 | node tools/initdb.js |
最后使用 pm2
开启服务
1 | pm2 start app.js |
通过第一个 Codelabs
上手 Flutter
开发。以下是第一个项目的演示:
CodeLabs
分析水墨化效果
按照从内到外的顺序,分析应用程序需要的组件以及组合形式。这里可以把每一行封装为一个组件,然后复用组件代码,实现列表应用。
创建 category.dart
,新建一个 ItemRow
的类,用来表示一个 ICON
和一个 TEXT
。
Flutter
组件有两种: 有状态组件
和 无状态组件
。组件通过直接继承 StatelessWidget
和 StatefulWidget
这两个抽象类来创建(class ItemRow extends StatelessWidget
)。
和多数面向对象程序设计语言一样,组件中需要使用的参数都需要提前申明,每个参数对应一个 final
修饰的属性。组件构造方法里面的命名参数可以使用 @required
注解为必需参数。另外语法规范还规定了,第一个参数是 key
,最后一个参数是 child
、children
或其他类似参数。
通过使用 assert
来保证当所指定的参数没有被赋值时,出现报错。一来这就省去了在创建组件的时候,需要给参数赋初值,二来提醒使用组件者,必须传入必要参数。
通过覆盖 build
这个抽象方法描述由此控件所实现的那一部分用户界面。其中,抽象类 BuildContext
是该控件在控件树中的位置句柄。
1 | class ItemRow extends StatelessWidget { |
然后按照设计,用 Padding
、 InkWell
组件包裹上面创建的组件,使得构成一个完整的 Category
组件
1 | class Category extends StatelessWidget { |
组件开发完后,就要用自定义的组件,创建一个完整的页面了。在 Fluter
中,页面被称为 Route
。当然在 Flutter
中万物皆为组件,所以一个页面其实也就是创建一个组件。
1 | class CategoryRoute extends StatelessWidget { |
这里用到了一个 FLutter
布局组件 Scaffold
。官方示例
这里主要用到了 appBar
和 body
两个属性。appBar
显示在界面顶部的一个 AppBar
,body
用来在显示页面的主要内容。
首先创建 body
要显示的组件:
1 | Widget _buildCategoryWidgets(List<Widget> categories) { |
这里传入一个 List
用来存放所有要显示的我们之前自定义的 category
组件。
这里使用 OrientationBuilder
这个小部件,在设备的方向发生改变的时候,重新构建布局。主要是通过 orientation
这个参数是 Orientation.landscape
还是 Orientation.portrait
来区别横屏还是竖屏。具体参考这篇文章。
创建好 body
后,定义变量,然后在 Scaffold
内调用即可。
1 | static const _categoryNames = <String>[ |
最后在 main.dart
调用即可。
通过官网直接下载后解压
使用 vim
或者其他软件,打开目录下的 start_navicat
,修改其中的语言为
1 | export LANG="zh_CN.UTF-8" |
因为用到的破解工具只有 Windows
版本的
配置虚拟机共享文件夹为 Navicat
在 Ubuntu
下的路径
在 Windows
虚拟机中下载破解工具 https://github.com/DoubleLabyrinth/navicat-keygen/releases
在 Windows
中打开 cmd
运行以下命令
1 | navicat-patcher.exe "E:\Navicat" |
路径要修改为共享文件夹在虚拟机中的路径地址
然后会有一堆输出,看到最后显示 MESSAGE: Patch has been done successfully.
说明创建私钥并替换公钥成功
然后输入下面的命令生成激活码
1 | navicat-keygen.exe -text .\RegPrivateKey.pem |
然后会有一些选项选择 Navicat
的版本、语言等等
1 | Select Navicat product: |
等到出现序列号的时候,回到 Ubuntu
系统,断开网络并打开 Navicat
,点击注册-输入序列号-激活,稍等一会弹出一个提示离线激活的窗口,点击离线激活
复制里面的密钥,粘贴到 Windows
中的 cmd
中(出现序列号后按照要求继续输入姓名和组织后就会提示你输入注册码)
在 cmd
输入注册码后两次回车就可以得到激活码,将那个激活码粘贴到 Ubuntu
下的 Navicat
的离线激活对话框中,就激活了
复制下面的图片到软件根目录
在根目录下新建文件
1 | [Desktop Entry] |
然后赋予快捷方式运行权限,并添加到 Ubuntu
应用快捷文件的存放位置中,方便下次搜索并直接运行 navicat
1 | sudo chmod a+x navicat.desktop |
打开后发现很多地方出现了小框框的乱码,修改字体可以解决
工具-选项 会弹出配置窗口,分别修改常规、编辑器和记录三个栏目中的字体为 Noto Sans mono CJK SC Regular
JavaScript
中有两种不同的数据类型的值,分别是基本数据类型和引用数据类型
变量赋值的时候,基本数据类型时传值的,也就是直接访问。引用数据类型是按引用访问的,相当于 C
中的指针。
修改其中一个变量不会影响另一个变量的值
1 | var x = 1 |
如果从一个变量向另一个变量复制基本类型的值,会在变量变量对象上创建一个新值,然后把该值复制到为新变量分配的位置上
——《JavaScript高级程序设计》
也就是说,传值得过程经历了以下几步:
所以两个变量的值只是在数值上相等,其实在内存中是两个地址,是互相独立的存在
因此其中一个发生改变时并不会影响到另外一个
引用数据类型的赋值,是把传递存放数据的内存空间的地址
1 | var arr1 = [1,2,3,4]; |
当从一个变量向另一个变量复制引用的值时,同样也会将存储在变量对象中的值复制一份放到为新变量分配的空间中。不同的是,这个值的副本实际上是一个指针,而这个指针指向存储在堆中的一个对象。复制操作结束后,两个变量实际上引用同一个对象
——《JavaScript高级程序设计》
克隆实现原理:
for
循环这种方法在遇到复杂数组时比较无力,比如 arr = [1,[2,3],{a:1,b:2}]
1 | //for循环原数组,将每个参数分别赋值给空数组arr2 |
forEach
实现forEach
是 ES5
新引入的数组方法,可以用来循环数组及对象
1 | array.forEach(callback,[thisObject]); |
forEach
有两个参数:第一个参数是回调函数,第二个参数是执行回调是 this
的值
回调函数包含 3
个参数:
注意:ie浏览器
没有 forEach
方法,要兼容的话需要自己用原生给原型链上加这个方法
1 | var arr1 = [1,2,3,4]; |
JSON.stringify() / JSON.parse()
实现JSON
格式字符串值(无法转换 DOM/BOM
对象)JSON
字符串转换成对象(属性名称必须有引号)用法举例:
1 | var arr = [1,2,[3,4],{a:'abc',b:true}]; |
克隆实现方法:
1 | var arr1 = [1,2,[3,4],{a: 'abc',b: true}]; |
定义且训练一个合适的深度网络,能够识别一张全新的图像,归入到 CIFAR-10
的 10
个类别中的一个
该项目是一个从实践出发,学习
Keras
、Tensorflow
的项目中的一部分 项目地址
这是一个包含 10
个类别的 RGB
三通道彩色图片数据集,具有以下特点:
3
通道图像而不是灰度图像32 × 32
50000
张训练图片和 10000
张测试图片Tensorflow 官方示例提供了一份代码 https://www.tensorflow.org/tutorials/images/deep_cnn
文 件 | 用 途 |
---|---|
cifar10_input.py | 在 TensorFlow 中加载 CIFAR-10 数据集 |
cifar10.py | 建立 CIFAR-10 模型 |
cifar10_train.py | 使用单个 GPU 或 CPU 训练模型 |
cifar10_multi_gpu_train.py | 使用多块 GPU 训练模型 |
cifar10_eval.py | 使用测试集测试模型的性能 |
源码详见项目目录下的 simple.ipynb
项目地址
可以直接通过内置的 cifar10
模块导入数据集,然而下载数据集慢,所以考虑用离线下载。 keras
数据集的位置在 ~/.keras/datasets
下,下载数据集后放到该目录下,再调用 keras
的时候就会自动使用本地的数据集。
1 | (x_train, y_train), (x_test, y_test) = cifar10.load_data() |
对标签进行 one-hot
编码处理
1 | # 转换为 one-hot 编码 |
将图像数据转换为 float
类型,并且归一化处理
1 | # float 类型归一化处理 |
网格采用 32
个卷积滤波器,每个大小是 3 × 3
。输出的维度和输入的形状相同 padding='same'
所以也应该是 32 × 32
,并且激活函数是 ReLU
。之后进行 2 × 2
大小的最大池化运算,并关闭 25%
的神经元。
1 | model.add(Conv2D(32, (3, 3), padding='same', |
接着使用 展平层(Flatten)
接 512
个单元构成全连接网络,并用 ReLU
激活。
1 | model.add(Flatten()) |
关闭一半的神经元,加上作为输出的有 10
个类的 softmax
层,每个类对应一个类别。
1 | model.add(Dropout(0.5)) |
训练前先把训练集分为两部分,一部分训练,另一部分验证。训练集用来构建模型,校验集用来选择选择表现最好的方法,测试集是为了在新的未见过的数据上检验模型的性能。
1 | model.fit(x_train, y_train, batch_size=BATCH_SIZE, epochs=NB_EPOCH, |
源码见项目目录下
v2.ipynb
项目地址
上面的准确度只有 0.6658
通过定义一个更深的、有更多卷积操作的网络,能够提高模型的准确率
所以这里定义这样一个模型,同样训练 20
轮能够达到 80%
以上的准确率
1 | _________________________________________________________________ |
源码见项目目录下
v3.ipynb
项目地址
深度学习通常要求拥有充足数量的训练样本。一般来说,数据的总量越多,训练得到的模型的效果就会越好。在数据集有限的情况下,通常通过 数据增强(Data Augmentation)
来改善模型的训练效果。 数据增强
就是对输入的图像进行一些简单的平移、缩放、颜色等颜色变化,这些不会影响图像类别的操作,人工增大训练集样本的个数,从而获得更充足的训练数据,使模型训练的效果更好。
keras
已经提供了图像增强的 API
直接调用就可以
1 | from keras.preprocessing.image import ImageDataGenerator |
rotation_range
表示图片旋转的值, width_shift_range
和 height_shift_range
是对图片做随机水平或垂直变化时的范围, zoom_range
是随机缩放图片的变化值, horizontal_flip
是对选中的一般图片进行随机的水平翻转, fill_mode
表示填充新像素的策略
1 | datagen = ImageDataGenerator( |
给上面设置好的图片生成器匹配数据
1 | datagen.fit(x_train) |
在训练的时候,调用图片生成器,使生成器针对模型并发运行。可以让图像在 CPU
上拓展时, 在 GPU
上并行训练。注意,此时训练非常耗时。
1 | model.fit_generator(datagen.flow(x_train, y_train, batch_size=BATCH_SIZE), |
关于卷积神经网络的相关内容还可以通过复现一个实战项目练手 人脸表情识别与卡通化
]]>使用 rdesktop 连接 Windows 远程桌面
1 | rdesktop 58.87.74.163 -r disk:LinuxFiles=/home/zhou/Documents -g 1080*1080 -u Administrator |
rdesktop 的常用参数有:
使用下面这个命令,会让 jupyter 在 C:/User/Adminitrator 下生成.jupyter 的文件夹,并且在文件夹中生成 jupyter_notebook_config.py 的配置文件
1 | jupyter notebook --generate-config |
编辑生成的配置文件,在文件中修改、激活以下配置
1 | c.NotebookApp.ip = '0.0.0.0' # 允许访问此服务器的 IP,星号表示任意 IP, 0.0.0.0 或者 127.0.0.1 表示本地 |
在终端输入
1 | jupyter notebook password |
通过提示,输入密码,并且密码的哈希值会保存在配置文件相同目录下的 jupyter_notebook_config.json 中
打开 Nginx 的配置文件 nginx.conf ,配置域名代理到Jupyter Notebook的服务,并且启用 SSL
SSL 证书可以在阿里云、腾讯云免费申请,也可以自己生成
1 | server { |
配置中启用了 websocket ,否则 Jupyter 中的 terminals 和 kernels 服务无法启用
最后就可以通过 https://notebook.zhzh.xyz 访问 Jupyter 服务
]]>双系统,三块硬盘,机械盘 NTFS 格式,两个系统公用。两块固态硬盘分别存放了两个系统。常用系统是 Ubuntu,所以希望能让 Ubuntu 开机自动挂载机械硬盘。
1 | zhou@zhou-son:~$ sudo blkid |
获取到当前系统下所有硬盘的信息。sda 装着 Windows,sdc 装着 Ubuntu,sdb 的两个分区是要挂载的机械硬盘。
1 | /dev/sda1: UUID="0B1B-14FF" TYPE="vfat" PARTLABEL="EFI system partition" PARTUUID="4ccb74e1-c447-11e8-8b97-da9208650b40" |
/etc/fstab 是在开机引导的时候自动挂载到 Linux 文件系统,根据官方论坛修改
首先打开 /etc/fstab 查看格式
1 | sudo vim /etc/fstab |
设备文件名称
设备文件名称(即/dev/xxx),或者是设备的label或uuid。由于硬盘接口位置顺序都不改变,偏向使用设备文件名称
挂载目录
要把硬盘挂载到 Linux 文件系统的位置。通常选择/media
文件系统类型
Linux 上用的最多的是 ext4,Windows 上用的最多的是 NTFS。Windows 下要通过例如 Ext2Read 的软件读取 ext4 格式的硬盘
挂载选项
一般设置为defaults,就是自动挂载
是否备份
一般设置为0,即不备份
开机时是否对文件系统进行自检
不自检,设置为0;挂载点为根目录的设备,设置为1;其它需要自检的设备,设置为2
根据分析,设置好要挂载的硬盘,重启即可(每一个选项之间,使用 Tab 而不是空格)
1 | /dev/sdb1 /media/zhou/PROGRAM ntfs-3g defaults 0 2 |
外接显示器的时候,由于显示器质量比较好,总是显示供电不足,用两套鼠标键盘操作也麻烦转而使用ssh或者vnc
树莓派内置VNC、SSH等等多种通信协议,只需要打开
1 | sudo raspi-config |
选择第5个选项Interfacing Options,进去后可以看到有相机、SSH、VNC、SPI、I2C、串口通信等等。VNC有图形化界面,SSH也很舒服,就选这两个了。(不过既然有这么多通信协议,以后有需求的时候应该可以考虑到用树莓派做双机通信)
一路是和确定后结束,就可以看到右上角VNC服务开启了。如果没有的话,就用vncserver命令打开一下
树莓派在VNC下分辨率爆炸,所以需要对VNC登录的分辨率进行设置。还是上面那条命令,进入树莓派设置里面的“Advanced Options > Resolution”,选择合适的分辨率,重启一下就好。
到VNC Viewer,选择相应的版本,下载安装。Ubuntu 18.04直接下载deb的版本,用dpkg安装一下即可。
出于某些不可描述却众所周知的原因,装完Ubuntu要换,装完Anaconda要换,装完树莓派也要换,这年头装完什么好像都要换一下源。。。
1 | sudo nano /etc/apt/sources.list |
树莓派的操作跟Ubuntu的差不多,用相同的方法更新下系统,树莓派就算配好了
]]>在opencv的GitHub官方地址下载源码,并且解压
进入OpenCV的文件夹,进行编译
1 | mkdir build |
make是一段非常漫长的过程
cd进opencv文件夹下的samples/cpp/example_cmake文件夹
运行make命令
1 | cmake . |
看到摄像头打开并且左上角有一个“hello opencv”
这里选用Clion,下载后解压,在工具里生成启动方式
1 | aria2c -s 64 https://download.jetbrains.8686c.com/cpp/CLion-2018.3.1.tar.gz |
每次需要连接opencv的时候,在cmake下面添加两行即可
1 | find_package(OpenCV REQUIRED) |
通过opencv-python识别出人脸
然后用fer2013的数据集训练深度卷积神经网络构建的模型识别人脸表情
使用训练好的模型识别人脸的表情情绪
根据识别结果,匹配合适的emoji遮住人脸
训练模型的数据集选用了kaggle挑战赛上的fer2013数据集
下载得到的csv格式可以通过Excel看到格式为:
Emotion | Pixels | Usage |
---|---|---|
0 | 4 0 170 118 101 88 88 75 78 82 66 74 68 59 63 64 65 90 89 73 80 80 85 88 95 117 … 129 | Training |
2 | 200 197 149 139 156 89 111 58 62 95 113 117 116 116 112 111 96 86 99 113 120 1 … 116 | Training |
所以首先打开csv文件,根据usage把数据集分为:训练集、测试集和验证集
1 | with open(csv_file) as f: |
如果直接用当前数据是一个扁平的向量,没有空间局部性。用这样的数据直接进行训练,就会失去空间结构和图像关系信息。卷积神经网络可以保留空间信息,并且更适合图像分类问题,所以要把数据转为图片方便下面采用卷积神经网络进行训练
1 | num = 1 |
顺便把图片灰度化处理(防止黑人和白人的肤色对模型造成影响 O(∩_∩)O哈哈哈)
替代人脸的卡通表情采用了Android 9的Emoji
这里用到了很多神经网络层
这里图像使用tf(tensorflow)顺序,它在三个通道上的形状为(48,48),正常图片可以表示为(48, 48, 3)。只不过在刚刚生成图片的时候,已经做过灰度化处理,所以这个时候,只有一个通道了。
使用keras添加一层二维滤波器,输出维度是32并且每个二维滤波器是1 * 1的卷积层
1 | self.model.add(Conv2D(32, (1, 1), strides=1, padding='same', input_shape=(img_size, img_size, 1))) |
padding=’same’表示保留边界处的卷积计算结果。总共只有两种设置,这种表示输出和输入的大小相同,输入的区域边界填充为0;padding=’valid’表示只对输入和滤波器完全叠加的部分做卷积运算,因而输出将会少于输入。不过讲道理,这里strides这个处理步幅已经是1了,不管设置什么都不会超过边界
使用ReLU激活函数
1 | self.model.add(Activation('relu')) |
然后给网络学习32个5 * 5的滤波器,也用ReLU激活。并且紧接着一个最大池化层方法
1 | self.model.add(Conv2D(32, (5, 5), padding='same')) |
之后第二层卷积阶段和第三层卷积阶段都是用ReLU激活函数,后面再次跟着最大池化层方法。第二层仍然是32个3 * 3大小的滤波器,第三层滤波器增加到64个5 * 5,在更深的网络层增加滤波器数目是深度学习中一个普遍采用的技术
1 | self.model.add(Conv2D(32, (3, 3), padding='same')) |
首先用Flatten()获得一个扁平的网络
1 | self.model.add(Flatten()) |
用ReLU激活一个有2048个神经元的隐藏层,用Dropout丢弃到一半的网络,再添加一个1024个神经元的隐藏层,跟着一个关闭50%神经元的dropout层
1 | self.model.add(Activation('relu')) |
添加作为输出7个类的softmax层,每个类对应一个类别
1 | self.model.add(Dense(num_classes)) |
1 | _________________________________________________________________ |
这里选择随机梯度下降算法作为优化器
1 | sgd = SGD(lr=0.01, decay=1e-6, momentum=0.9, nesterov=True) |
通常提高性能有两种方法,一种是定义一个更深、有更多卷积操作的网络,另一种训练更多的图片。这里用keras自带的ImageDataGenerator方法扩展数据集
1 | # 自动扩充训练样本 |
考虑到效率问题,keras提供了生成器针对模型的并发运行。我的理解就是CPU处理生成图像,GPU上并行进行训练
1 | # 归一化验证集 |
把结构保存为JSON字串,把权重保存到HDF5文件
1 | model_json = self.model.to_json() |
1 | # 从json中加载模型 |
用opencv打开摄像头,使用opencv提供的一个训练好的模型识别人脸人类器
1 | # 创建VideoCapture对象 |
根据识别出的脸部特征点,裁剪出脸部图像,然后调用模型预测情绪
1 | if len(faceLands) > 0: |
根据识别结果,用cv的rectangle在视频流上框出脸部并且用putText打上标签
1 | # 框出脸部并且写上标签 |
先在第一次获取视频画面的时候就copy一个没有灰度化处理的视频画面
1 | # 呈现用emoji替代后的画面 |
直接把emoji图片遮盖人脸会出现emoji背景变为黑色盖上去了。所以这里要蒙版处理一下,也就是保持emoji透明背景的特性,当然,这里所有图像都要归一化处理
1 | def face2emoji(face, emotion_index, position): |
1 |
|
1 |
|
在许多方面看来,消息队列类似于有名管道,但是却没有与打开与关闭管道的复杂关联。然而,使用消息队列并没有解决我们使用有名管道所遇到的问题,例如管道上的阻塞。消息队列提供了一种在两个不相关的进程之间传递数据的简单高效的方法。与有名管道比较起来,消息队列的优点在独立于发送与接收进程,这减少了在打开与关闭有名管道之间同步的困难。消息队列提供了一种由一个进程向另一个进程发送块数据的方法。另外,每一个数据块被看作有一个类型,而接收进程可以独立接收具有不同类型的数据块。消息队列的好处在于我们几乎可以完全避免同步问题,并且可以通过发送消息屏蔽有名管道的问题。更好的是,我们可以使用某些紧急方式发送消息。坏处在于,与管道类似,在每一个数据块上有一个最大尺寸限制,同时在系统中所有消息队列上的块尺寸上也有一个最大尺寸限制。尽管有这些限制,但是 X/Open 规范并没有定义这些限制的具体值,除了指出超过这些尺寸是某些消息队列功能失败的原因。
1 |
|
与信息号和共享内存一样,头文件 sys/types.h 与 sys/ipc.h 通常也是需要的。
使用 msgget 函数创建与访问一个消息队列:
1 | int msgget(key_t key, int msgflg); |
与其他 IPC 工具类似,程序必须提供一个指定一个特定消息队列的 key 值。特殊值IPC_PRIVATE 创建一个私有队列,这在理论上只可以为当前进程所访问。与信息量和共享内存一样,在某些 Linux 系统上,消息队列并不是私有的。因为私有队列用处较少,因而这并不是一个严重问题。与前面一样,第二个参数,msgflg,由 9 个权限标记组成。要创建一个新的消息队列,由 IPC_CREAT 特殊位必须与其他的权限位进行或操作。设置 IPC_CREAT
标记与指定一个已存在的消息队列并不是错误。如果消息队列已经存在,IPC_CREAT 标记只是简单的被忽略。如果成功,msgget 函数会返回一个正数作为队列标识符,如果失败则会返回-1。
msgsnd 函数允许我们将消息添加到消息队列:
1 | int msgsnd(int msqid, const void *msg_ptr, size_t msg_sz, int msgflg); |
消息结构由两种方式来限定。第一,他必须小于系统限制,第二,必须以 long int 开始,这在接收函数中会用作一个消息类型。
通常在使用消息时,是以如下形式来定义消息结构:
1 | struct my_message { |
因为 message_type 用于消息接收,所以不能简单的忽略他。必须定义我们自己的数据结构来包含并对其进行初始化,从而他可以包含一个可知的值。
函数如果成功,函数会返回 0,并且系统就会复制一份消息数据并将其放入消息队列;如果失败,则会返回-1。
msgrcv 函数由一个消息队列中收取消息:
1 | int msgrcv(int msqid, void *msg_ptr, size_t msg_sz, long int msgtype, int msgflg); |
msgctl()这与共享内存中的控制函数类似
1 | int msgctl(int msqid, int command, struct msqid_ds *buf); |
如果成功则会返回 0,如果失败则会返回-1。当进程正在 msgsnd 或是 msgrcv 函数中等待时如果消息队列被删除,发送或接收函数就会失败。
]]>hexo -s的时候出现了报错
1 | INFO Start processing |
根据提示,去官方文档查了一下ENOSPC
ENOSPC 错误 (Linux)
运行 $ hexo server 命令有时会返回这样的错误:
1 | Error: watch ENOSPC ... |
它可以用过运行 $ npm dedupe 来解决,如果不起作用的话,可以尝试在 Linux 终端中运行下列命令:
1 | $ echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p |
也就是用来提高监视(watch)的文件数量上限
构建网站的时候用到了gulp.js,gulp的watch需要监听很多文件的改动,但是ubuntu系统的文件句柄其实是有限制的,因此提高上限就OK了,之前没有出现这个问题,可能是因为之前文件还少,现在文章多了图片也多了,就出现了这样的问题
]]>初始化基础信息
1 | class PlannedCourseSpider: |
通过新建一个课程类,来方便管理获取到的课程信息
其中code被用作选课时的提交代码,构造数据包时要用到
1 | class Lesson: |
常规操作,逆向工程查看请求的URL和发送的数据
1 | def hello_zf(self): |
分析网页源码,课程信息的存放大同小异,都是在一张table里面,第一项是抬头,获取抬头的兄弟节点。这里要注意这张网页的table最后有一个无效的tr标签,根据那个判断是否读完整张表。读取的信息可以存在之前用过的Lesson类里。这里用了正则表达式匹配课程代码,这个代码用于之后获取该课程的所有开课信息
1 | def get_all_lesson(selector): |
把所有获取到的课程显示初恋,调用Lesson类的自带方法,选课也可以用到相同的方法,于是封装成一个类
1 | def show_and_select_lessons(lesson_list): |
根据选择的计划内课程,获取到那门课程的所有开课信息。
常规操作遇到坑,直接模拟获取信息的时候会在新窗口中打开。所以要按住Ctrl新建标签页面打开开课信息页面,然后在新打开的窗口中F12抓包。此时需要再发一次请求才能抓包,所以回到所有计划内课程页面,按住Ctrl再重新点击一次课程,之前打开抓包的那个页面就会刷新一次,抓到的数据也是需要的数据了
没啥新鲜的,Headers的referer改为发送请求的url,xkkh就是之前获取所有计划内课程信息里面的code,xh就是学号
1 | def hello_lesson(self, xkkh): |
这里用到了set_view_state修改Headers里面的__VIEWSTATE,分析源码发现,这个值总是出现在table的第三个隐藏input里面,后面选课的时候,要发送这个值
1 | def set_view_state(self, selector, from_name): |
查看网页源码,跟之前完全类似的table结果,获取信息后储存到Lesson类中即可
1 | def get_all_information_of_lesson(selector, lesson): |
手动模拟选课,查看POST的数据,比公选课简单的数据少一点
herders的referer还是要更新为当前发送请求的页面URL
1 | def select_lesson(self, lesson): |
用到的show_error是Lesson模块的方法,用Xpath定位页面的script,再用正则表达式匹配aleret信息,给出“现在不是选课时间”、“选课时间冲突”这样的提示
1 | def show_error(selector): |
1 | import Login as Lg |
1 | import re |
执行sudo apt-get update出现错误:
1 | 仓库'http://dl.google.com/linux/chrome/deb stable Release'将其'Origin'值从'Google, Inc.'修改到了'Google LLC' |
这个错误导致无法升级系统,错误说明了原因:是由于 Google Chrome 的 Origin 改变引起的,也给出了解决思路:手动接受这个改变。
所以修复这个错误就很容易,执行:
1 | sudo apt update |
执行后会看到
1 | E: 仓库'http://dl.google.com/linux/chrome/deb stable Release'将其'Origin'值从'Google, Inc.'修改到了'Google LLC' |
输入y后,正常更新操作就完成了
]]>把一些基础的信息初始化,方便在之后的方法中增加、删除、修改、使用这些数据。这些信息,在搜索课程和选课时都会用到。
1 | class LessonSpider: |
Chrome F12 进行逆向工程(俗称抓包),查看正常浏览(网上选课——公选课)时,会发出哪些数据
1 | def hello_zf(self): |
用lxml解析get到的公选课课程页面,通过xpath定位到已经选择的课程列表。另外计数已经选择的课程,用于之后判断选课是否成功
1 | def already(selector): |
通过新建一个课程类,来方便管理获取到的课程信息
其中code被用作公选课选课时的提交代码,构造数据包时要用到
1 | class Lesson: |
抓取了搜索课程的包,忽略值为空的键
1 | def set_view_state(self, selector): |
ddl_ywyl: ‘%D3%D0’ 中文“有”的gb2312编码,表示查询还有余量的公选课;’%CE%DE’表示“无”;为空表示查询所有
ddl_xqbs: 1 相传这个是校区代码,每个学校都不一样。这里就保持1就OK
TextBox1: 要查找的课程名的GB2312编码
dpkcmcGrid:txtChoosePage: 1 表示选择的页数,默认1
dpkcmcGrid:txtPageSize: 200 为一页显示多少数据,经过测试,服务器最多响应200条,公选课也就一百多门,这里写200也就保证了上面那个值写1也能抓到所有课程。如果有一天,超过200门公选课了,就需要修改代码,循环抓取每一页的课程了
Button2: 确定的GB2312编码,相当于查询时的那个确定按钮
1 | def search_lessons(self, lesson_name=""): |
lxml解析查询得到的页面,xpath定位所有课程信息的位置,转换为Lesson的对象存入元组中方便选课
1 | def get_lessons(selector): |
这里post的数据包比起基础包要多两个
1 | def select_lesson(self, lesson_list): |
这里还用到一个显示错误的函数,原理也是解析页面后查看html>head>script的值,用正则表达式查找是否有alert的弹窗,显示alert中的内容
1 | def show_error(selector): |
1 | import re |
1 | import copy |
正方教务系统的二维码字符颜色都是蓝色。二维码转为RGB通道,遍历像素点,蓝色改为黑色,其他颜色改为白色。再转为灰度图片,方便后面识别
1 | def stay_blue2gray(image): |
验证码的字符总是出现在相同的位置,定长暴力切割
1 | def split_image(image): |
字模的文件名切片第一个就是字模对应的字符
1 | def ocr(images, model_path='./zfgetcode/data/model'): |
遍历像素点,返回匹配度最高的结果,匹配算法还是大一程序设计课上,找最小数的穷举算法
1 | def single_char_ocr(image, models, file_names): |
1 | import os |
总是需要用到的一些量放入这个类,方便调用和修改。
1 | class MySetting { |
先给项目导入HttpClient
通过HttpClient建立连接,Chrome审查元素可知验证码链接为教务系统链接后面加上“/CheckCode.aspx”
用Java文件输入输出流保存图片到本地
1 | class getCodeIMG { |
思路是用现有的验证码生成OCR字模。再抓取一批验证码,使用字模打标。人工处理一遍,把错误的验证码找出来重新手工打标。打标用再生成字模。如此循环多次,模型不断完善,准确率达到85%以上即可
ZUCC_ZhenFangHelper下zf_train.tar.xz里面有32283张已经打好标签的验证码,选择1000张放入result
观察验证码文本都是蓝色。遍历图片所有像素点,所有蓝色点变为黑色,其他为白色
1 | private static BufferedImage removeBackgroud(String picFile) throws Exception { |
观察验证码,发现每次文本位置不变。直接等快分割,不借助切割算法。高度在0-23像素之间,宽度在5-53像素之间。每个字符占据1/4的像素,因此规定第一个字符的像素在5-17之间,第二个在17-29之间,第三个在29-41之间,第四个在41-53之间。
1 | private static List<BufferedImage> splitImage(BufferedImage img){ |
传统的验证码识别中“9”,“0”,“o”出错率很高。但正方教务系统不会生成“9”,“o”,提高了准确率。只不过对于“l”,“i”,“1”这三个字符上出错率依旧很高,可以通过扩充字模降低
1 | /* |
ZUCC_ZhenFangHelper/zfgetcode/data下有已经训练好的模型“model”和“model1”。其中“model1”是用三万多张那个数据集生成出来的,虽然识别率高,但大大降低了识别速率。“model”内的字模已经够用使用上已经够了
先去背景,二值化,分割成单个图片后,与字模库分别对比获得结果
1 | /* |
二值化后的验证码,文本都变为黑色,比对黑色像素不同个数,不同像素点个数最少的作为识别结果
1 | /* |
用JavaFX制作人工校验图形化界面。校验时结果正确则回车下一个验证码,并删除图片。结果错误则输入框内输入正确值,把修改后的验证码移到WRONG目录下,校验完成后去该目录确认一遍,无误后移入result文件夹,调用字模生成接口,重新生成字模
1 | public void start(Stage primaryStage) { |
不断重复上述步骤扩充字模库可以不断提高识别准确率,但随之而来的就是识别速率的下降
完整代码较长,可参见github
拿到了3000多张数据集,文件名却异常诡异,用Bash批量操作
1 |
|
1 |
|
1 | # 直接运行 |
Cookie指某些网站为了辨明用户身份、进行session(会话)跟踪而储存在用户本地终端上的数据。辨明身份,自然要保存用户的的相关信息了,所以可以通过Cookie来模拟登录网站。
1 | import requests |
Cookie由于各种原因,大概率无效,尝试POST表单交互
查看表单的网页源代码,确定提交方式是POST
观察表单源码中input标签,获取表单提交字段
逆向工程(抓包)确认表单的提交字段
(1)进入ZUCC选课网,打开chrome开发者工具,选择network选项
(2)手动输入账号和密码后登录
(3)在“default2.aspx”的文件中,“From Data”里面就是需要提交的字段
1 | __VIEWSTATE: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX |
__VIEWSTATE: 相传这个是.Net框架特有产物,会变化,在html源码的form标签下的第一个hidden的input标签里可以看到值
1 | <input type="hidden" name="__VIEWSTATE" value="dDwxNTMxMDk5Mzc0Ozs+HjPJ2APgB+73zYSyRsZkjFxlx0A="> |
txtUserName: 学号参数
Textbox1: 用来实现记住密码的功能的(讲道理chrome的记住密码比这个功能好用多了)
TextBox2: 用来传输密码的(抓包的时候看到,密码没有一点点加密,直接明文传送)
txtSecretCode: 验证码
RadioButtonList1: 通过view source查看源码,可以看到
1 | __VIEWSTATE=dDwxNTMxMDk5Mzc0Ozs%2BHjPJ2APgB%2B73zYSyRsZkjFxlx0A%3D&txtUserName=XXXXXXXX |
审查元素,验证码地址是“CheckCode.aspx”
1 | def get_information(): |
1 | class LoginSpider: |
获取__VIEWSTATE的值,并抓取验证码到本地
1 | def hello_world(self): |
1 | def down_code(self): |
每提交一次选课,VIEWSTATE就会更新一次。所以后面会一直用到获取VIEWSTATE的信息,直接封装成一个函数
1 | def get_view_state(response): |
定义一个判断是否成功登录的函数。对于登录成功和登录失败的页面,用etree解析页面,xpath定位title。根据title的不同就可以判断
1 | def login_status(response): |
1 | def login_manual(self): |
1 | def login_ocr(self): |
Cookie登录功能已经废弃,OCR代码识别代码参见OCR验证码识别(Python)
1 | import json |
依据正则表达法这套标准,来用几个符号组成的正则表达式表示一定规则的字符串。通过正则表达式,方便地让计算机理解我们想要表达的一长串字符串。
对于一些查找、删除、替换等等支持正则表达式的工具,可以用正则表达式来理解我们想要告诉计算机的字符串,并根据工具或者接口本身的功能和作用来对我们选定的字符串进行处理。
1 | # 收录,方便查表 |
-a :将 binary 档案以 text 档案的方式搜寻数据(一般可执行的比如shell脚本就是binary档案)
-c :计算找到 ‘搜寻字符串’ 的次数
-i :忽略大小写的不同,所以大小写视为相同
-n :顺便输出行号
-v :反向选择,即显示出没有 ‘搜寻字符串’ 内容的那一行
1 | grep -n 'the' test.txt # 查找有“the”的字符串 |
1 | grep -n 't[ea]st' test.txt # 查找包含test或者tast的字符串 |
[]表示包含这里面的所有字符中的一个,有且只有一个。
[n1-n2]表示搜索指定的字符串范围,例如[0-9]、[a-z] 、[A-Z]等
[^…]表示这里面所有的字符都不要
1 | grep -n 'Mr.Zhou' zhzh.txt # 查找“Mr.Zhou”为开头的字符串 |
1 | # *(星号):代表重复前面 0 个或者多个字符。 |
{ }可限制一个范围区间内的重复字符数。举个例子,若要找出 2~5 个 o 的连续字符串,此时便要用到{}了。由于 { 与 } 在 shell 中有特殊意义,需要用到转义字符\。
1 | grep -n 'z\{5\}h' zhzh.txt # 查找字符串“z” + “5个z” + “h” |
元字符 | 正则表达式中的写法 | 意 义 |
---|---|---|
. | . | 代表任意一个字符 |
\d | \\d | 0~9任意一个数字 |
\D | \\D | 任意一个不是数字的字符 |
\s | \\s | 空白符号“\t”、“\n”(把字符串根据空格分割为字符串数组) |
\S | \\S | 任意一个非空白符号 |
\w | \\w | 代表可以用作标识符的字符,除了“$”(看示例程序) |
\W | \\W | 代表不可以用作标识符的字符 |
\p{Lower} | \\p{Lower} | 小写字母a~z |
\p{Upper} | \\p{Upper} | 大写字母A~Z |
\p{ASCLL} | \\p{ASCLL} | ASCLL字符 |
\p{Alpha} | \\p{Alpha} | 字母字符 |
\p{Digit} | \\p{Digit} | 十进制数字 |
\p{Alnum} | \\p{Alnum} | 数字或者字母 |
\p{Punct} | \\p{Punct} | 标点符号 |
未完待续……………… |
[a-zA-z] 任意一个大写或者小写字母
[a-e[g-z]] a~e或者g~z中的任意一个字母
[a-o&&[def]] a~o和[def]的交集,也就是d、e、f
[a-d&&[^bc]] a~d和[^bc]的交集,a~d减去[bc]的差运算,即a、d
|限定修饰符|意 义|示 例|
|—|—|—|—|
|?|0次或者多次|A?|
||0次或者多次|A|
|+|一次或者多次|A+|
|{n}|正好出现n次|A{2}|
|{n,}|至少出现n次|A{3,}|
|{n,m}|出现n~m次|A{2,6}|
1 | package xyz.zhzh; |
源码来自斗大的熊猫。使用captcha生成验证码,作为后面训练模型生成能够识别验证码模型的数据集。captcha是一个能够生成图片验证码和语音验证码的库。
1 | from captcha.image import ImageCaptcha # pip install captcha |
将序列中的元素以指定的字符连接生成一个新的字符串。
‘sep’.join(seq)
sep:分隔符。可以为空
seq:要连接的元素序列、字符串、元组、字典
返回通过指定字符连接序列中元素后生成的新字符串。
figure(num=None, figsize=None, dpi=None, facecolor=None, edgecolor=None, frameon=True)
num:图像编号或名称,数字为编号 ,字符串为名称
figsize:指定figure的宽和高,单位为英寸;
dpi:参数指定绘图对象的分辨率,即每英寸多少个像素,缺省值为80 1英寸等于2.5cm,A4纸是 21*30cm的纸张
facecolor:背景颜色
edgecolor:边框颜色
frameon:是否显示边框
1 | import matplotlib.pyplot as plt |
subplot可以规划figure划分为n个子图,但每条subplot命令只会创建一个子图
subplot(nrows,ncols,sharex,sharey,subplot_kw,**fig_kw)
nrows:subplot的行数
ncols:subplot的列数
sharex:所有subplot应该使用相同的X轴刻度
sharey:所有subplot应该使用相同的Y轴刻度
subplot_kw:创建各subplot的关键字字典
1 | import numpy as np |
这个问题在16.04已经有了,当时是挂载报错。18挂载依旧报错,但是会自动以只读的方式挂载。主要原因就是Windows的快速启动。关于Windows的快速启动,主要就是在关机的时候把内核直接全部写到硬盘里,下次开机的时候直接读进来。快是快了,但是会出现一些bug(功能刚出来的时候特别明显),而且总是擦写对固态也不好,固态硬盘已经够快了,没必要开快速启动。
1 | sudo ntfsfix /dev/sdb1 |
Windows-控制面板-电源选项-快速启动
]]>d: 删除命令
这里的删除并没有真正操作文件,仅仅局限于显示
1 | sed '3d' filename # 使用数字表示第几行 |
s: 替换命令
用法:s/old/new/标签
标签后面加p,显示影响满足条件的行
n 不在屏幕上显示
1 | sed 's/tom/TOM/' filename # 标签默认为1 |
打开Juypter Notebook,发现居然在用CPU训练,检查发现是安装了tensorflow的cpu版本
1 | pip ubinstall tensorflow |
接着import tensorflow出错了,ImportError:no module named tensorflow.python。应该是卸载掉了什么依赖,或者某些需要的依赖没有达到要求
1 | pip install --ignore-installed --upgrade tensorflow-gpu |
突然想到如果把tensorflow-gpu和tensorflow如果装在两个环境是不是能解决问题
]]>官网下载deb,运行即可
deb安装的软件,大概率出现缺少依赖而打不开
1 | zhou@zhou-son:~$ whereis gitkraken |
1 | zhou@zhou-son:~$ /usr/bin/gitkraken |
上面的Error处在libgnome-keyring
1 | sudo apt-get install libgnome-keyring-common libgnome-keyringev |
1 | sudo snap install wps-office |
安装完成后打开wps会出现字体缺失的报错,因为wps用到的一些字体在Ubuntu下没有
字体找了个百度云链接,下载好后解压出来
1 | sudo mkdir /usr/share/fonts/wps-office |
打开wps,不报字体缺失即可视为OK。然而由于换了fcitx,上次的坑还没有填满,不能再用临时方案了,必须彻底去解决这个问题……
]]>
注意保证显卡驱动版本在384及以上,因为之后需要安装的CUDA 9.0对显卡驱动的最低支持到384(2018.10.23)
记得勾选所有用户
和勾选自动添加环境变量
打开cmd,换清华源
1 | conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/ |
导入环境
开始菜单 - 打开Anaconda Navigator - 侧边栏选择Environments - 选择文件environment.yaml - 点击import
安装Jupyter Notebook(由于地域问题导致导入失败的,直接看下一条)
侧边栏切换回Home - 安装Jupyter Notebook
找到jupyter notebook,点击install
环境导入出错的,先确认一下conda有没有切换到清华源,还是不行的尝试自己配置一个虚拟环境:
1 | conda create -n tf python=3.5 |
双击运行安装,记住安装位置后一直下一步就好
https://developer.nvidia.com/cuda-toolkit-archive
注意:一要按照对应文件夹拷贝;二是把解压出来的文件夹里面的文件拷贝进CUDA目录的对应文件夹例如lib,就把lib/x64下面的cudnn.lib放到CUDA\v9.0\lib\x64下面
具体表现在jetbrains套装上和开始的搜索框切换不了任何输入法,进行中文输入
分析和验证后发现是gnome给亲儿子ibus写了点东西,但是ibus早就被卸了……
1 | sudo vim /etc/profile |
1 | gsettings set org.gnome.settings-daemon.plugins.xsettings overrides "{'Gtk/IMModule':<'fcitx'>}" |
分析来分析去,还是因为gnome支持ibus,上述方法能临时生效,但不能永久生效,问题还需要研究,先给自己挖个坑,等空下来了再来填吧
终于在装了WPS之后,不得不来填坑了
1 | #Fcitx |
卵子用都没有哦,于是接着尝试,在WPS和pycharm.sh里面加入配置命令,还是卵子用都没有。reboot后发现不用上面的gsettings,重启fcitx之后就可以了,但是每一次开机都需要重启fcitx。
用脚本自动重启fcitx
1 |
|
然后尝试开机自动启动脚本,结果依旧凉凉。意识到还是因为gnome和fcitx不对付,也许事情是先启动fcitx,然后gnome启动出现了一些骚操作(还不知道是什么)破坏了fcitx的运行,于是导致几遍安装好qt,配置好环境变量依旧不能使用。所以就让脚本随gnome自启动
1 | [Desktop Entry] |
Desktop文件都是存放在 /usr/share/applications下
1.tweak tool可以直接设置
2.放软链接
1 | ln -s /usr/share/applications/fixfictx.desktop ~/.config/autostart/fixfictx.desktop |
Snap是Ubuntu母公司Canonical于2016年4月发布Ubuntu16.04时候引入的一种安全的、易于管理的、沙盒化的软件包格式,与传统的dpkg/apt有着很大的区别。Snap可以让开发者将他们的软件更新包随时发布给用户,而不必等待发行版的更新周期;其次Snap应用可以同时安装多个版本的软件,所以snap可以解决兼容性和依赖性的问题,当然带来的就是速度下降,特别是启动速度。
Ubuntu18.04的发行说明中提到,默认支持snap应用。还有,例如计算器这个系统应用竟然也是snap应用
1 | sudo snap list |
ex:snap安装idea
1 | sudo snap install intellij-idea-ultimate --classic --edge |
1 | # 通过指定包名来指定要更新的软件 |
1 | sudo snap revert intellij-idea-ultimate --classic --edge |
1 | sudo snap remove intellij-idea-ultimate --classic --edge |
1 | from tensorflow.examples.tutorials.mnist import input_data |
TF提供了方便的封装,可以直接加载MNIST数据为我们期望的格式
1 | mnist = input_data.read_data_sets('MNIST_data', one_hot = True) |
多分类任务,通常使用softmax regression模型。工作原理就是讲某一类的特征相加,然后把这些特征转换为判定是这一类的概率。
1 | import tensorflow as tf |
这里采用梯度下降法
1 | train_step = tf.train.GradientDescentOptimizer(0.5).minimize(cross_entropy) |
1 | # 开始训练之前,首先要构建图,InteractiveSession可以被注册为默认的session,这样之后运算会方便 |
1 | correct_prediction = tf.equal(tf.argmax(prediction, 1), tf.argmax(y_, 1)) |
1.tensorflow中定义为变量类型,才能成为一个变量,有一点点奇怪
2.定义了变量之后,一定要初始化变量才能用
1 | import tensorflow as tf |
1 | x_data = np.random.rand(100).astype(np.float32) |
1 | #weights and biases是需要训练的变量 |
1 | loss = tf.reduce_mean(tf.square(y-y_data)) |
这里用的误差传递方法是梯度下降法: Gradient Descent ,
“tf.train.GradientDescentOptimizer(0.5)”被用作参数的更新.
1 | # 梯度下降算法的优化器 |
1 | init = tf.global_variables_initializer() |
1 | sess = tf.Session() |
下载sh,用普通用户权限执行安装,会自动~/.source里添加Anaconda变量
1 | vim ~/.condarc |
1 | conda create -n tf python=3.5 |
1 | conda activate tf |
起床,还是忍不住,打算装一下驱动程序和cuda
1 | sudo vim /etc/modprobe.d/blacklist.conf //添加blacklist nouveau来把这个开源驱动加入黑名单 |
1 | sudo apt-get purge nvidia-* //删除可能存在的已有驱动 |
目前TF支持最好的就是CUDA9.0了
Ubuntu18.04默认版本不是CUDA要求的4.8,有两种办法一种是修改CUDA绕过版本验证,另一种是GCC降级。
1 | sudo apt-get install gcc-4.8 |
下载run,然后执行安装就好,当然会提示安装NVIDIA384的显卡驱动,选NO就行。关于CUDA的软链接还是选YES,后期如果需要多个cuda版本再说。安装完成后,就会出现下面的提示:
PATH includes /usr/local/cuda-9.0/bin
LD_LIBRARY_PATH includes /usr/local/cuda-9.0/lib64, or, add /usr/local/cuda-9.0/lib64 to /etc/ld.so.conf and run ldconfig as root
1 | sudo vim /etc/profile |
添加CUDA的环境变量
最后跑两个示例程序就OK
download cudnn然后tar开压缩包
1 | sudo cp cuda/include/cudnn.h /usr/local/cuda/include |
1 | pip3 install tensorflow-gpu |
想要做一个web版的猜画小歌,先去爬数据集。
1 | from urllib import request |
然而,这网络……简直无力吐槽了,或者试一下多线程下载
]]>1 | .post { |
1 | li |
顺便还学了下 pug
1 | npm i --save hexo-wordcount |
这款插件还能显示文章字数、阅读时长等等,具体使用看官方文档
1 | .footer |
在style.css里面加入滚动条样式代码,用的还是诌给自己的滚动条渐变代码
1 | /*定义滚动条高宽及背景 高宽分别对应横竖滚动条的尺寸*/ |
1 | if theme.weibo |
安装RSS插件
1 | npm install hexo-generator-feed |
Blog工程目录下的_config.xml
1 | # Extensions |
生成RSS
1 | hexo g |
在theme下的文件下设置路径
1 | # Social Accounts |
修改下链接打开方式
1 | if theme.rss |
首先是可能会出现打不开,我是在Anaconda用sudo安装的时候出现的,也以chmod来给权限。顺带一提,Anaconda最好还是别sudo,所以卸载干净,普通权限
1 | bash ./Anaconda.sh |
另一种需要检查~/.code的用户组,如果是root,那即便连上网也只能是一直Installing而无法安装上插件的
1 | sudo chown -R zhou:zhou ~/.code |