目标
了解MMKV
MMKV的基本应用
MMKV的原理概念
多进程设计思想
性能对比
源码解读
简介
MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。
官方文档:https://github.com/Tencent/MMKV/blob/master/README_CN.md
了解MMKV
MMKV的基本应用
MMKV的原理概念
多进程设计思想
性能对比
源码解读
MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强。
官方文档:https://github.com/Tencent/MMKV/blob/master/README_CN.md
Flutter项目是能运行的,打开Flutter里面的Android项目才会报下面错误。
1 2 3 4 5 6 |
8:34 Gradle sync started 8:35 Gradle sync failed: Could not create task ':image_picker:generateDebugUnitTestConfig'. this and base files have different roots: D:\Pensoon\flutter_property_check_gd\build\image_picker and C:\Users\XXX\AppData\Roaming\Pub\Cache\hosted\pub.flutter-io.cn\image_picker-0.8.3+2\android. (52 s 230 ms) 8:35 Gradle sync started 8:35 Gradle sync failed: Could not create task ':image_picker:generateDebugUnitTestConfig'. this and base files have different roots: D:\Pensoon\flutter_property_check_gd\build\image_picker and C:\Users\XXX\AppData\Roaming\Pub\Cache\hosted\pub.flutter-io.cn\image_picker-0.8.3+2\android. (2 s 588 ms) |
1 2 3 4 5 6 7 8 9 |
$ .\gradlew clean build Configuration on demand is an incubating feature. FAILURE: Build failed with an exception. * What went wrong: Could not determine the dependencies of task ':url_launcher_android:test'. > Could not create task ':url_launcher_android:testDebugUnitTest'. > this and base files have different roots: D:\Source\xxxx\build\url_launcher_android and C:\Users\Administrator\AppData\Local\Pub\Cache\hosted\pub.flutter-io.cn\url_launcher_android-6.0.25\android. |
报错的项目配置信息如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
buildscript { ext.kotlin_version = '1.8.0' repositories { maven { url "https://maven.aliyun.com/nexus/content/groups/public/" } maven { url "https://maven.aliyun.com/nexus/content/repositories/google" } google() mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:7.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } allprojects { repositories { maven { url "https://maven.aliyun.com/nexus/content/groups/public" } maven { url "https://maven.aliyun.com/nexus/content/repositories/google" } google() mavenCentral() } } rootProject.buildDir = '../build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" } subprojects { project.evaluationDependsOn(':app') } task clean(type: Delete) { delete rootProject.buildDir } |
Flutter一开始Android build是没问题的,开发着突然就报这个下面的错误,开始怀疑是不是有什么缓存啥的,然后各种排除都没找到什么原因,后面想着降版本吧,kotlin降了没用,后面尝试最后一个Gradle降版本竟然成功了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 报错版本 classpath 'com.android.tools.build:gradle:7.0.0' // 报错版本 classpath 'com.android.tools.build:gradle:7.3.1' // 报错版本 classpath 'com.android.tools.build:gradle:7.4.2' // 解决版本 classpath 'com.android.tools.build:gradle:4.1.3' // 解决版本 classpath 'com.android.tools.build:gradle:4.2.2' |
在 Flutter 里 TextField
是一个比较复杂的控件,而在整个 TextField
里嵌套了许多不同实现的控件,它们组成了我们常用的输入框效果,如下图所示是关于 TextField
的主要构成部分,也是本篇主要讲解的内容。
在 Android Studio Electric Eel | 2022.1.1 Patch 2 中构建项目的时候,出现了
1 |
removeContentEntry: removed content entry url 'file://*****' still exists after removing |
这样的报错。
经过多次尝试,发现直接删除 .idea目录是有效的。
关于该问题详细的讨论可以参考 https://stackoverflow.com/questions/66214555/gradle-sync-failed-removecontententry-removed-content-entry-url-still-ex
解决idea下removeContentEntry: removed content entry url ‘file://*****‘ still exists after removing的问题
以前在 Windows系统上的 Android Studio 上编辑 gradle.properties 的时候增加了不少的中文注释,现在升级到 Android Studio Electric Eel | 2022.1.1 Patch 2 结果发现中文都变成乱码了。
原因是升级到 Android Studio Electric Eel | 2022.1.1 Patch 2 之后,Windows系统上 gradle.properties 的默认编码格式被设置成了 ISO-8859-1 ,导致文件显示乱码。
现在调整回 UTF-8 格式即可解决问题。如下图:
继续阅读解决Android Studio Electric Eel | 2022.1.1 Patch 2系统下gradle.properties中文注释乱码
构建时间太长会拖慢您的开发过程。本页将介绍一些有助于解决构建速度瓶颈的技巧。
提高应用构建速度的一般过程如下:
开发应用时,您应尽可能将其部署到搭载 Android 7.0(API 级别 24)或更高版本的设备中。较新版本的 Android 平台有更出色的机制来向您的应用推送更新,例如 Android 运行时 (ART) 以及对多个 DEX 文件的原生支持。
注意:您完成首次干净构建后,可能会注意到后续构建(干净和增量)的执行速度明显加快了(即使您没有使用本页面介绍的任何优化措施)。这是因为 Gradle 守护程序有一个性能提升“预热”期,类似于其他 JVM 进程。
按照下面的提示操作,以提高 Android Studio 项目的构建速度。
几乎每次更新时,Android 工具都会获得构建方面的优化和新功能,本页介绍的一些提示假设您使用的是最新版本。为了充分利用最新的优化措施,请确保以下工具始终是最新版本:
避免编译和打包不测试的资源(例如,其他语言本地化和屏幕密度资源)。您可以仅为“dev”变种的版本指定一个语言资源和屏幕密度,如下面的示例中所示:
1 2 3 4 5 6 7 8 9 10 11 12 |
android { ... productFlavors { dev { ... // The following configuration limits the "dev" flavor to using // English stringresources and xxhdpi screen-density resources. resourceConfigurations "en", "xxhdpi" } ... } } |
在 Android 中,所有插件都位于 google()
和 mavenCentral()
代码库中。不过,build 可能需要使用 gradlePluginPortal()
服务解析的第三方插件。
Gradle 会按照声明的顺序搜索代码库,因此,如果先列出的代码库包含大多数插件,build 性能就会得到提升。因此,您可以尝试将 gradlePluginPortal()
条目放置在 settings.gradle
文件的代码库中最靠后的位置。在大多数情况下,这样可以最大限度地减少冗余插件搜索次数,并提高构建速度。
如需详细了解 Gradle 如何导航多个代码库,请参阅 Gradle 文档中的声明多个代码库。
始终为会进入调试 build 类型的清单文件或资源文件的属性使用静态值。
您每次想运行更改时,都需要完整的应用 build 才能使用动态版本代码、版本名称、资源或可更改清单文件的任何其他构建逻辑,即使实际更改可能仅需要 1 次热交换也是如此。如果您的 build 配置需要此类动态属性,请将这些属性隔离到您的发布 build 变体中,并使相应值对您的调试 build 保持静态,如下例所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
... // Use a filter to apply onVariants() to a subset of the variants. onVariants(selector().withBuildType("release")) { variant -> // Because an app module can have multiple outputs when using multi-APK, versionCode // is only available on the variant output. // Gather the output when we are in single mode and there is no multi-APK. val mainOutput = variant.outputs.single { it.outputType == OutputType.SINGLE } // Create the version code generating task. val versionCodeTask = project.tasks.register("computeVersionCodeFor${variant.name}", VersionCodeTask::class.java) { it.outputFile.set(project.layout.buildDirectory.file("versionCode${variant.name}.txt")) } // Wire the version code from the task output. // map will create a lazy Provider that: // 1. Runs just before the consumer(s), ensuring that the producer (VersionCodeTask) has run // and therefore the file is created. // 2. Contains task dependency information so that the consumer(s) run after the producer. mainOutput.versionCode.set(versionCodeTask.flatMap { it.outputFile.map { it.asFile.readText().toInt() } }) } ... abstract class VersionCodeTask : DefaultTask() { @get:OutputFile abstract val outputFile: RegularFileProperty @TaskAction fun action() { outputFile.get().asFile.writeText("1.1.1") } } |
如需了解如何在项目中设置动态版本代码,请参阅 GitHub 上的 setVersionsFromTask 配方。
在 build.gradle
文件中声明依赖项时,请避免使用动态版本号(以加号结尾的版本号,例如 'com.android.tools.build:gradle:2.+'
)。使用动态版本号可能会导致意外的版本更新和难以解析版本差异,并会因 Gradle 检查有无更新而减慢构建速度。 请改用静态版本号。
在应用中查找可以转换成 Android 库模块的代码。以这种方式将您的代码模块化,可以让构建系统仅编译您修改的模块,并缓存输出以用于未来的构建。此外,这种方式也会让并行项目执行更有效(当您启用该优化时)。
创建构建性能剖析报告后,如果性能剖析报告显示相当长的一部分构建时间用在了“配置项目”阶段,请检查 build.gradle
脚本并查找您可以添加到自定义 Gradle 任务中的代码。将某些构建逻辑移到任务中后,您可以确保它仅在需要时运行,可以缓存结果以用于后续构建,并且该构建逻辑将可以并行运行(如果您已启用并行项目执行)。如需详细了解自定义构建逻辑的任务,请参阅官方 Gradle 文档。
提示:如果您的构建包含大量自定义任务,您可能需要通过创建自定义任务类来整理 build.gradle
文件。将您的类添加到 project-root/buildSrc/src/main/groovy/
目录中;Gradle 会自动将这些类添加到项目中所有 build.gradle
文件的类路径中。
WebP 是一种既可以提供有损压缩(像 JPEG 一样)也可以提供透明度(像 PNG 一样)的图片文件格式,不过与 JPEG 或 PNG 相比,WebP 格式可以提供更好的压缩。
减小图片文件大小可以加快构建速度(无需在构建时进行压缩),尤其是当应用使用大量图片资源时。不过,在解压缩 WebP 图片时,您可能会注意到设备的 CPU 使用率有小幅上升。通过使用 Android Studio,您可以轻松地将图片转换为 WebP 格式。
即使您不将 PNG 图片转换为 WebP 格式,仍然可以在每次构建应用时停用自动图片压缩,以加快构建速度。
如果您使用的是 Android Gradle 插件 3.0.0 或更高版本,则系统会在默认情况下针对“调试”编译类型停用 PNG 处理。如需针对其他 build 类型停用此优化,请将以下代码添加到 build.gradle
文件中:
1 2 3 4 5 6 7 8 |
android { buildTypes { release { // Disables PNG crunching for the "release" build type. crunchPngs false } } } |
由于 build 类型或产品变种不定义此属性,因此在构建应用的发布版本时,您需要手动将此属性设置为 true
。
通过配置 Gradle 所用的最佳 JVM 垃圾回收器,可以提升构建性能。虽然 JDK 8 默认配置为使用并行垃圾回收器,JDK 9 及更高版本已配置为使用 G1 垃圾回收器。
为提高构建性能,我们建议您使用并行垃圾回收器测试 Gradle 构建。在 gradle.properties
中设置以下内容:
1 |
org.gradle.jvmargs=-XX:+UseParallelGC |
如果此字段中已设置了其他选项,请添加一个新选项:
1 |
org.gradle.jvmargs=-Xmx1536m -XX:+UseParallelGC |
如需衡量采用不同配置时的构建速度,请参阅对构建进行性能剖析。
如果您发现构建速度较慢(尤其是在 Build Analyzer 结果中发现构建时间超时 15% 的情况),则应增加 Java 虚拟化机器 (JVM) 堆大小。 在 gradle.properties
文件中,将限制设置为 4 GB、6 GB 或 8 GB,如以下示例所示:
1 |
org.gradle.jvmargs=-Xmx6g |
然后测试构建速度是否有提升。确定最佳堆大小最简单的方法是增加限额,然后测试是否有足够的构建速度提升效果。
如果您还使用 JVM 并行垃圾回收器,则整行命令应如下所示:
1 |
org.gradle.jvmargs=-Xmx6g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:+UseParallelGC -XX:MaxMetaspaceSize=1g |
您可以通过开启 HeapDumpOnOutOfMemoryError 标记分析 JVM 内存错误。这样,JVM 会在内存耗尽时生成堆转储。
使用非传递 R
类可为具有多个模块的应用构建更快的 build。这样做有助于确保每个模块的 R
类仅包含对其自身资源的引用,而不会从其依赖项中提取引用,从而帮助防止资源重复。这样可以获得更快的 build,以及避免编译的相应优势。这是 Android Gradle 插件 8.0.0 及更高版本中的默认行为。
从 Android Studio Bumblebee 开始,新项目的非传递 R
类默认处于开启状态。 对于使用早期版本的 Android Studio 创建的项目,请依次前往 Refactor > Migrate to Non-transitive R Classes,将项目更新为使用非传递 R
类。
如需详细了解应用资源和 R
类,请参阅应用资源概览。
您可在应用和测试中使用非常量 R
类字段,以提高 Java 编译的增量并允许进行更精准的资源缩减。 对库而言,R
类字段始终不是常量,因为在为依赖于相应库的应用或测试打包 APK 时,资源会进行编号。 这是 Android Gradle 插件 8.0.0 及更高版本中的默认行为。
由于大多数项目都直接使用 AndroidX 库,因此您可以移除 Jetifier 标志,以便获得更好的构建性能。如需移除 Jetifier 标志,请在 gradle.properties
文件中设置 android.enableJetifier=false
。
Build Analyzer 可以执行一项检查,确认是否可以安全移除该标记,使您的项目能够具有更好的构建性能,并不再使用未加维护的 Android 支持库。如需详细了解 Build Analyzer,请参阅排查构建性能问题。
配置缓存是一项实验性功能,可让 Gradle 记录有关构建任务图的信息,并在后续 build 中重复使用该任务图,而不必再次重新配置整个 build。
如需启用配置缓存,请按以下步骤操作:
使用 Build Analyzer 检查项目是否与配置缓存兼容。Build Analyzer 会运行一系列测试 build,以确定是否可以为项目启用该功能。请参阅问题 13490,查看受支持的插件列表。
将以下代码添加到 gradle.properties
文件:
1 2 3 4 |
org.gradle.unsafe.configuration-cache=true # Use this flag carefully, in case some of the plugins are not fully compatible. org.gradle.unsafe.configuration-cache-problems=warn |
启用配置缓存后,当您首次运行项目时,build 输出应该会显示 Calculating task graph as no configuration cache is available for tasks
。 在后续运行期间,构建输出应该会显示 Reusing configuration cache
。
如需详细了解配置缓存,请参阅配置缓存深度解析这篇博文和有关配置缓存的 Gradle 文档。
首先 串口 的流控大家应该都有所了解,通常是硬件 CTS/RTS 或软件 XON/XOFF 这两种流控方式,然而因为 RS485 是总线形式,所以传统的方法都不再适用。
有人会觉得奇怪,貌似从来没有考虑过 RS485 流控的问题,没错,传统 RS485 都是一收一发,用不着考虑流控,然而这种一收一发的效率比较低,譬如在 IoT 火热的今天,如果用 RS485 来传输网络数据,那么传统的做法就很低效了。
然后,针对数据完整性确保的问题,很多同行都没有留意到一个细节问题,他们通常判断是否收到回复 OK 的数据包,如果没收到数据包就超时重发一次。 这种做法大多情况都没有问题,但是某些场景,譬如发送一个命令让滑轨左移 10mm, 滑轨成功接收命令并返回 OK, 然而主机因为干扰等各种问题没有收到滑轨的回复,那么重发命令就会导致滑轨错误左移 20mm. 当然你可以说目前用到的设备都是绝对位置控制,不会有影响,但万一哪天新做一个设备,到那时再改协议,难道就不考虑兼容自己以往的产品了吗?
当然还是有很多朋友有注意到这个问题,本文使用的解决方法原理上跟这些朋友也是相同的。
我接下来提出的方案最大的亮点是共用同一套机制,同时解决了流控、数据完整性确保、大数据分包等功能,而且比较高效和简单。
同样,最底层的协议我们依然使用 CDBUS, 因为它比较简单,又支持硬件增强(可以主动避让冲突,实现多主机、对等通讯、主动上报数据等功能),能最大程度体现出本文方法的性能优势。
你可能没有听过 CDBUS 这个名字,但你可能曾经或正在使用相似的协议,它的组成包含 3 个部分: - 3 个字节的头:「源地址,目标地址,用户数据长度」 - 0~255 字节的用户数据(因为数据长度用 1 个字节表示) - 2 个字节的 CRC 校验,涵盖整个数据包,校验算法同 ModBus.
数据包与数据包之间要有一定的空闲时间,来隔开不同的数据包,详细请参见 CDBUS 的协议定义: https://github.com/dukelec/cdbus_ip
譬如地址 0x00 为主机,0x01 为 1 号从机,那么主机发送两个字节数据 0x10 0x11 给 1 号从机的完整数据为:
1 |
[00 01 02 10 11 49 f0] |
然后 1 号从机回覆单个 0x10 给主机:
1 |
[01 00 01 10 04 b8] |
然而 CDBUS 只是最底层的协议,接下来我们要定义上述用户数据的格式,最简单常用的方式就是首字节为命令号,然后后面跟可选命令参数; 回覆数据第一个字节通常为状态,然后是返回的数据。
这种方式完善之后也有一个名字,叫 CDNET, 它的定义在:https://github.com/dukelec/cdnet
(本文的内容这个连接都有包含,但本文会更加通俗的讲解一下关键细节。)
CDNET 协议有 3 个级别,由首字节的高两位决定:
1 2 3 4 |
Bit7 Bit6 描述 0 x Level 0: 上述最简单的形式 1 0 Level 1: 支持跨网、组播、流控等高阶功能 1 1 Level 2: 裸数据模式,支持大数据拆包,譬如传输 IPv4/v6 数据包 |
实际使用根据情况自由选择某一个或某几个来用就好。
Level 0 格式
请求
首字节:
1 2 3 4 |
位 描述 [7] 等于 0: Level 0 [6] 等于 0: 请求 [5:0] dst_port, 范围 0~63 |
CDNET 的端口号可以看做类似电脑的 UDP 端口,也可以看做是一个命令号。
第二字节及其后:命令参数
回复
首字节:
1 2 3 4 5 |
位 描述 [7] 等于 0: Level 0 [6] 等于 1: 回复 [5] 1: [4:0] 存放用户数据;0: 不使用 [4:0] 不使用或用户数据,数据必须 ≤ 31 |
例如: 回复 [0x40, 0x0c] 和回复 [0x6c] 是相同的意思。
1 2 3 4 |
0x40: 'b0100_0000 0x0c: 'b0000_1100 ---- 0x6c: 'b0110_1100 |
首字节的用户数据(如果有)、第二个字节及其后:回复的状态 和/或 数据。
Level 1 格式
首字节:
1 2 3 4 5 6 7 |
位 描述 [7] 等于 1 [6] 等于 0 [5] MULTI_NET (跨网) [4] MULTICAST (组播) [3] SEQUENCE (序列号) [2:0] PORT_SIZE (端口大小设置) |
MULTI_NET & MULTICAST
1 2 3 4 5 |
MULTI_NET MULTICAST 描述 0 0 Local net: 本地网络,不追加数据 0 1 Local net multicast: 追加 2 字节 [multicast-id] 组播号 1 0 Cross net: 追加 4 字节: [src_net, src_mac, dst_net, dst_mac] 1 1 Cross net multicast: 追加 4 字节: [src_net, src_mac, multicast-id] |
这个与本文主题无关,就不展开了。
SEQUENCE
0: 无序列号;
1: 追加 1 字节序列号 SEQ_NUM, 这个是重点,稍后会主要说明。
PORT_SIZE:
1 2 3 4 5 6 7 8 9 |
Bit2 Bit1 Bit0 SRC_PORT DST_PORT 0 0 0 Default port 1 byte 0 0 1 Default port 2 bytes 0 1 0 1 byte Default port 0 1 1 2 bytes Default port 1 0 0 1 byte 1 byte 1 0 1 1 byte 2 bytes 1 1 0 2 bytes 1 byte 1 1 1 2 bytes 2 bytes |
注: - 默认端口通常定为 0xcdcd, 所以不用额外追加字节. - 追加的字节按顺序,先是 src_port 再是 dst_port.
Level 2 格式
首字节:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
位 描述 [7] 等于 1 [6] 等于 1 [5:4] FRAGMENT(大数据分包) [3] SEQUENCE(序列号) [2:0] User-defined flag FRAGMENT: Bit5 Bit4 DST_PORT 0 0 Not fragment 0 1 First fragment 1 0 More fragment 1 1 Last fragment |
注: - 使用分包功能的时候必须同时选择使用 SEQUENCE. - 开始分包的时候 SEQ_NUM 不需要归零.
一般情况下,要求不高,使用最简单的 Level 0 格式就好了,如果命令比较多,那么就可以用 Level 1 格式,用不到的功能不用理会即可。
Level 1 没有大数据分包功能,因为通常 MCU 也用不到那么大的数据包,即使是烧录代码这种要传大数据的功能,也是可以在命令内部定义地址和数据长度的,譬如我的 STM32 总线代码升级的命令定义:
1 2 3 4 |
// flash memory manipulation, port 11: // erase: 0xff, addr_32, len_32 | return [] on success // read: 0x00, addr_32, len_8 | return [data] // write: 0x01, addr_32 + [data] | return [] on success |
而 Level 2 譬如可以用来传 IPv4/v6 数据包,那么就不得不加入拆包的功能了。因为 Level 1 和 Level 2 的序列号部分是一样的,所以接下来就混在一起讲了。
CDBUS 协议将前 0~9 保留专用,10 及其后的用户可以随便用,保留的部分目前也就用了 4 个,而且也不是强制的,用户愿意实现就实现,不用或者自己想怎么用就怎么用也没问题。 上篇文章说了端口或命令 0x01 是用来查询设备信息的,命令 0x03 是用来设置地址的,还详细说了如何使用这两个端口来实现地址自动分配,剩下两个端口其中 0x02 是用来设置波特率的, 对于本文最关键的端口 0x00 是用于流控、完整性确保、大数据拆包的了,其定义如下:
Port 0
配合 Level 1 和 2 头中的 SEQUENCE 字段使用。
命令启用 SEQUENCE 后追加的对应字节 SEQ_NUM[6:0] 的低 7 位会每次自动加 1.
而 SEQ_NUM 的第 7 位用来指示接收方是否要报告状态。
Port 0 本身的命令不可以启用 SEQUENCE.
Port 0 命令定义:
1 2 3 4 5 6 7 8 9 10 11 |
主动读目标的 SEQ_NUM: Write [] Return: [SEQ_NUM] (如果没有记录 bit 7 置 1) 主动设置目标的 SEQ_NUM: Write [0x00, SEQ_NUM] Return: [] 目标回复 SEQ_NUM: Write [SEQ_NUM] Return: None |
实际示例:
(-> 和 <- 是端口层的数据流, >> 和 << 是 CDNET 数据包层面的数据流,不含最低层的 CDBUS 的部分)
1 2 3 4 5 6 7 8 9 10 11 12 |
设备 A 设备 B 描述 [0x00, 0x00] -> Port0 首次通讯设置对方的 SEQ_NUM Default port <- [] 设置成功返回 [0x88, 0x00, ...] >> 开始发送数据 [0x88, 0x01, ...] >> [0x88, 0x82, ...] >> 这次的数据标注了需要回复 SEQ_NUM @2 [0x88, 0x03, ...] >> [0x88, 0x04, ...] >> Port0 <- [0x03] 回复 SEQ_NUM @2 (每成功接收一个包计数加 1, 回复当前计数 0x03) [0x88, 0x85, ...] >> 标注了需要回复 SEQ_NUM @5 Port0 <- [0x06] 回复 SEQ_NUM @5 |
效率提升的重点就在这里,我们可以自行选择多久回复一次,而不是每次都要回复状态,如果最后一次数据包没有标注需要回复,那么会引发超时,然后主动读一次目标的 SEQ_NUM 以做同步。 之所以引发超时,是因为所有发出的数据包都不能立刻释放,要等确认对方收到才会释放,以防需要出错重传。 因为有 SEQ_NUM 号,所以即使同一个命令重复发送,对方也会只执行一次。
流控的功能也包含在内,譬如发送方时刻只允许最多 6 个数据包没有释放,那么等收到回复,释放掉 3 个,再发送 3 个数据包,这样可以最大化的利用总线带宽。 而且万一有多方发送数据至同一个节点,发送方也可以因频繁超时,来动态降低最大允许 pending 的数据包数量。
再来说大包拆分,也是很简单,拆分包有 3 个标记,分别是起始、继续、结束,譬如一个大包拆开了 4 个小包,且如果当前 SEQ_NUM 为 23,那么这四个小包的 SEQ_NUM 和标记对应关系就是:
1 2 3 4 |
23: 拆分启始标志 24: 继续 25: 继续 26: 结束 |
这样接收方也就很容易的把四个小包还原成原始的大包,万一出问题,也只是重新传输错掉或丢掉的包(及其后的包)。
为了简便,对于 CDNET 协议,并不是丢一个包就只重传一个包,其后传的包也需要重传,因为接收方只是简单判断序号,不对便拒绝接收,这么做是为了保持简单,毕竟错包、丢包的概率很低。
最后,想说的是,这篇文章的内容都是经过实践检验的,我有用来传输摄像头视频,DEMO 可以在这篇介绍文章中看到:https://github.com/dukelec/cdbus_doc/blob/master/intro_zh.md
协议的实现部分代码也是开源的,就是上面的 CDNET 连接,另外有一些使用 CDNET 的示例代码,譬如这个 STM32F103 的步进电机控制器:https://github.com/dukelec/stepper_motor_controller
当然,这些代码、库我也会进一步优化改善。
由IEEE制定的新型单对以太网(SPE)或10BASE-T1L物理层标准,为传输设备运行状况信息实施状态监测(CbM)应用提供了新的连接解决方案。SPE提供共享电源和高带宽数据架构,可通过低成本双线电缆在超过1000米的距离实现10 Mbps数据和电源的共享。
ADI公司设计了业界首款10BASE-T1L MAC-PHY(ADIN1110),这是一款集成MAC的单对以太网收发器。ADIN1110使用简单的SPI总线与嵌入式微控制器通信,从而可降低传感器的功耗并减少固件开发时间。
在本文中,您将了解如何设计一款体型小巧但功能强大的传感器,如图1所示。本文将介绍:
● 如何设计小型共享数据和电源通信接口
● 如何为传感器设计超低噪声电源
● 微控制器和软件架构选择
● 选择合适的MEMS振动传感器
● 集成数字硬件设计和机械外壳
● 电脑上的数据采集UI示例
ESP32芯片有三个UART控制器(UART0
, UART1
和UART2
),其中UART0
(GPIO3
用于U0RXD
,GPIO1
用于U0TXD
)用作下载、调试串口,引脚不可改变;
UART1
和UART2
的引脚是可以设置的。 UART1
默认引脚是GPIO9
用作U1RXD
,GPIO10
用作U1TXD
,但是这两个引脚也是用于外接flash的,因此在使用UART1
的时候需要设置其他引脚;
UART2
默认引脚是GPIO16
用作U2RXD
,GPIO17
用作U2TXD
。
在components/driver/include/driver/uart.h
中可以查看api;
在examples/peripherals/uart中也可以参考官方的各种串口例程。
1 |
esp_err_t uart_driver_install(uart_port_t uart_num, int rx_buffer_size, int tx_buffer_size, int queue_size, QueueHandle_t* uart_queue, int intr_alloc_flags); |
这里要注意参数:uart_queue
属于freertos里面的队列句柄,在这里表示用于指示来自串口底层中断的队列消息。
1 |
esp_err_t uart_param_config(uart_port_t uart_num, const uart_config_t *uart_config); |
1 |
esp_err_t uart_set_rx_full_threshold(uart_port_t uart_num, int threshold); |
1 |
esp_err_t uart_set_pin(uart_port_t uart_num, int tx_io_num, int rx_io_num, int rts_io_num, int cts_io_num); |
1 |
int uart_read_bytes(uart_port_t uart_num, uint8_t* buf, uint32_t length, TickType_t ticks_to_wait); |
1 |
int uart_write_bytes(uart_port_t uart_num, const char* src, size_t size); |
1 2 3 4 5 6 7 8 |
#define EX_UART_NUM UART_NUM_1 //串口1 #define TXD_PIN (GPIO_NUM_17) //txd使用gpio17 #define RXD_PIN (GPIO_NUM_16) //rxd使用gpio16 #define BUF_SIZE (128) //缓冲区定义 static QueueHandle_t uart_queue; //队列句柄 static uint8_t uartbuf[BUF_SIZE]; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
void uart_comm_init(void) { /* Configure parameters of an UART driver, * communication pins and install the driver */ uart_config_t uart_config = { .baud_rate = 115200, .data_bits = UART_DATA_8_BITS, .parity = UART_PARITY_DISABLE, .stop_bits = UART_STOP_BITS_1, .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, .source_clk = UART_SCLK_APB, }; //Install UART driver, and get the queue. uart_driver_install(EX_UART_NUM, BUF_SIZE * 2, BUF_SIZE * 2, 20, &uart_queue, 0); uart_param_config(EX_UART_NUM, &uart_config); uart_set_rx_full_threshold(EX_UART_NUM,126); //Set UART pins (using UART0 default pins ie no changes.) uart_set_pin(EX_UART_NUM, TXD_PIN, RXD_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); //Create a task to handler UART event from ISR xTaskCreate(uart_event_task, "uart_event_task", 2048, NULL, 12, NULL); //创建串口任务 } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
static void uart_event_task(void *pvParameters) { uart_event_t event; for(;;) { //阻塞接收串口队列, //这个队列在底层发送,用户只需在应用层接收即可 if(xQueueReceive(uart_queue, (void * )&event, (portTickType)portMAX_DELAY)) { switch(event.type) {//各种串口事件 case UART_DATA: ESP_LOGI(TAG, "[UART DATA]: %d", event.size); uart_read_bytes(EX_UART_NUM, uartbuf, event.size, portMAX_DELAY); //阻塞接收 ESP_LOGI(TAG, "[DATA EVT]:"); uart_write_bytes(EX_UART_NUM, uartbuf, event.size);//原样发送 break; //Event of HW FIFO overflow detected case UART_FIFO_OVF: //硬件fifo溢出 ESP_LOGI(TAG, "hw fifo overflow"); // If fifo overflow happened, you should consider adding flow control for your application. // The ISR has already reset the rx FIFO, // As an example, we directly flush the rx buffer here in order to read more data. uart_flush_input(EX_UART_NUM); xQueueReset(uart_queue); break; //Event of UART ring buffer full case UART_BUFFER_FULL: //环形缓冲区满 ESP_LOGI(TAG, "ring buffer full"); // If buffer full happened, you should consider encreasing your buffer size // As an example, we directly flush the rx buffer here in order to read more data. uart_flush_input(EX_UART_NUM); xQueueReset(uart_queue); break; //Event of UART RX break detected case UART_BREAK: ESP_LOGI(TAG, "uart rx break"); break; //Event of UART parity check error case UART_PARITY_ERR: ESP_LOGI(TAG, "uart parity error"); break; //Event of UART frame error case UART_FRAME_ERR: ESP_LOGI(TAG, "uart frame error"); break; //UART_PATTERN_DET case UART_PATTERN_DET: break; //Others default: ESP_LOGI(TAG, "uart event type: %d", event.type); break; } } } vTaskDelete(NULL); } |
在实际产品开发过程中,在线升级可以远程解决产品软件开发引入的问题,更好地满足用户需求。
OTA(空中)更新是使用 Wi-Fi 连接而不是串行端口将固件加载到 ESP 模块的过程。
在三种升级情况下,必须通过串行端口完成第一个固件上传。
OTA 进程没有强加的安全性,需要确保开发人员只能从合法/受信任的来源获得更新。更新完成后,模块将重新启动,并执行新的代码。开发人员应确保在模块上运行的应用程序以安全的方式关闭并重新启动。
模块必须以无线方式显示,以便通过新的草图进行更新。 这使得模块被强行入侵并加载了其他代码。 为了减少被黑客入侵的可能性,请考虑使用密码保护您的上传,选择某些OTA端口等。
可以提高安全性的 ArduinoOTA 库接口:
1 2 3 |
void setPort(uint16_t port); void setHostname(const char* hostname); void setPassword(const char* password); |
1 2 3 4 |
void onStart(OTA_CALLBACK(fn)); void onEnd(OTA_CALLBACK(fn)); void onProgress(OTA_CALLBACK_PROGRESS(fn)); void onError(OTA_CALLBACK_ERROR (fn)); |
已经内置了某些保护功能,不需要开发人员进行任何其他编码。ArduinoOTA和espota.py使用Digest-MD5来验证上传。使用MD5校验和,在ESP端验证传输数据的完整性。
ESP32 连接 HTTP 服务器,发送请求 Get 升级固件;每次读取1KB固件数据,写入Flash。
ESP32 SPI Flash 内有与升级相关的(至少)四个分区:OTA data、Factory App、OTA_0、OTA_1。其中 FactoryApp 内存有出厂时的默认固件。
首次进行 OTA 升级时,OTA Demo 向 OTA_0 分区烧录目标固件,并在烧录完成后,更新 OTA data 分区数据并重启。
系统重启时获取 OTA data 分区数据进行计算,决定此后加载 OTA_0 分区的固件执行(而不是默认的 Factory App 分区内的固件),从而实现升级。
同理,若某次升级后 ESP32 已经在执行 OTA_0 内的固件,此时再升级时 OTA Demo 就会向 OTA_1 分区写入目标固件。再次启动后,执行 OTA_1 分区实现升级。以此类推,升级的目标固件始终在 OTA_0、OTA_1 两个分区之间交互烧录,不会影响到出厂时的 Factory App 固件。
从 Arduino IDE 无线上传模块适用于以下典型场景:
在固件开发过程中,通过串行加载更快的替代方案 - 用于更新少量模块,只有模块在与 Arduino IDE 的计算机相同的网络上可用。
参考实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
#include <WiFi.h> #include <ESPmDNS.h> #include <WiFiUdp.h> #include <ArduinoOTA.h> const char* ssid = ".........."; const char* password = ".........."; void setup() { Serial.begin(115200); Serial.println("Booting"); WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); while (WiFi.waitForConnectResult() != WL_CONNECTED) { Serial.println("Connection Failed! Rebooting..."); delay(5000); ESP.restart(); } // Port defaults to 3232 // ArduinoOTA.setPort(3232); // Hostname defaults to esp3232-[MAC] // ArduinoOTA.setHostname("myesp32"); // No authentication by default // ArduinoOTA.setPassword("admin"); // Password can be set with it's md5 value as well // MD5(admin) = 21232f297a57a5a743894a0e4a801fc3 // ArduinoOTA.setPasswordHash("21232f297a57a5a743894a0e4a801fc3"); ArduinoOTA .onStart([]() { String type; if (ArduinoOTA.getCommand() == U_FLASH) type = "sketch"; else // U_SPIFFS type = "filesystem"; // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end() Serial.println("Start updating " + type); }) .onEnd([]() { Serial.println("\nEnd"); }) .onProgress([](unsigned int progress, unsigned int total) { Serial.printf("Progress: %u%%\r", (progress / (total / 100))); }) .onError([](ota_error_t error) { Serial.printf("Error[%u]: ", error); if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed"); else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); else if (error == OTA_END_ERROR) Serial.println("End Failed"); }); ArduinoOTA.begin(); Serial.println("Ready"); Serial.print("IP address: "); Serial.println(WiFi.localIP()); } void loop() { ArduinoOTA.handle(); } |
该方案使用场景:
直接从 Arduino IDE 加载是不方便或不可能的
用户无法从外部更新服务器公开 OTA 的模块
在设置更新服务器不可行时,将部署后的更新提供给少量模块
参考实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 |
#include <WiFi.h> #include <WiFiClient.h> #include <WebServer.h> #include <ESPmDNS.h> #include <Update.h> const char* host = "esp32"; const char* ssid = "xxx"; const char* password = "xxxx"; WebServer server(80); /* * Login page */ const char* loginIndex = "<form name='loginForm'>" "<table width='20%' bgcolor='A09F9F' align='center'>" "<tr>" "<td colspan=2>" "<center><font size=4><b>ESP32 Login Page</b></font></center>" "<br>" "</td>" "<br>" "<br>" "</tr>" "<td>Username:</td>" "<td><input type='text' size=25 name='userid'><br></td>" "</tr>" "<br>" "<br>" "<tr>" "<td>Password:</td>" "<td><input type='Password' size=25 name='pwd'><br></td>" "<br>" "<br>" "</tr>" "<tr>" "<td><input type='submit' onclick='check(this.form)' value='Login'></td>" "</tr>" "</table>" "</form>" "<script>" "function check(form)" "{" "if(form.userid.value=='admin' && form.pwd.value=='admin')" "{" "window.open('/serverIndex')" "}" "else" "{" " alert('Error Password or Username')/*displays error message*/" "}" "}" "</script>"; /* * Server Index Page */ const char* serverIndex = "<script src='https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js'></script>" "<form method='POST' action='#' enctype='multipart/form-data' id='upload_form'>" "<input type='file' name='update'>" "<input type='submit' value='Update'>" "</form>" "<div id='prg'>progress: 0%</div>" "<script>" "$('form').submit(function(e){" "e.preventDefault();" "var form = $('#upload_form')[0];" "var data = new FormData(form);" " $.ajax({" "url: '/update'," "type: 'POST'," "data: data," "contentType: false," "processData:false," "xhr: function() {" "var xhr = new window.XMLHttpRequest();" "xhr.upload.addEventListener('progress', function(evt) {" "if (evt.lengthComputable) {" "var per = evt.loaded / evt.total;" "$('#prg').html('progress: ' + Math.round(per*100) + '%');" "}" "}, false);" "return xhr;" "}," "success:function(d, s) {" "console.log('success!')" "}," "error: function (a, b, c) {" "}" "});" "});" "</script>"; /* * setup function */ void setup(void) { Serial.begin(115200); // Connect to WiFi network WiFi.begin(ssid, password); Serial.println(""); // Wait for connection while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(""); Serial.print("Connected to "); Serial.println(ssid); Serial.print("IP address: "); Serial.println(WiFi.localIP()); /*use mdns for host name resolution*/ if (!MDNS.begin(host)) { //http://esp32.local Serial.println("Error setting up MDNS responder!"); while (1) { delay(1000); } } Serial.println("mDNS responder started"); /*return index page which is stored in serverIndex */ server.on("/", HTTP_GET, []() { server.sendHeader("Connection", "close"); server.send(200, "text/html", loginIndex); }); server.on("/serverIndex", HTTP_GET, []() { server.sendHeader("Connection", "close"); server.send(200, "text/html", serverIndex); }); /*handling uploading firmware file */ server.on("/update", HTTP_POST, []() { server.sendHeader("Connection", "close"); server.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK"); ESP.restart(); }, []() { HTTPUpload& upload = server.upload(); if (upload.status == UPLOAD_FILE_START) { Serial.printf("Update: %s\n", upload.filename.c_str()); if (!Update.begin(UPDATE_SIZE_UNKNOWN)) { //start with max available size Update.printError(Serial); } } else if (upload.status == UPLOAD_FILE_WRITE) { /* flashing firmware to ESP*/ if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) { Update.printError(Serial); } } else if (upload.status == UPLOAD_FILE_END) { if (Update.end(true)) { //true to set the size to the current progress Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize); } else { Update.printError(Serial); } } }); server.begin(); } void loop(void) { server.handleClient(); delay(1); } |
ESPhttpUpdate 类可以检查更新并从 HTTP Web 服务器下载二进制文件。可以从网络或 Internet 上的每个 IP 或域名地址下载更新,主要应用于远程服务器更新升级。
参考实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 |
/** AWS S3 OTA Update Date: 14th June 2017 Author: Arvind Ravulavaru <https://github.com/arvindr21> Purpose: Perform an OTA update from a bin located in Amazon S3 (HTTP Only) Upload: Step 1 : Download the sample bin file from the examples folder Step 2 : Upload it to your Amazon S3 account, in a bucket of your choice Step 3 : Once uploaded, inside S3, select the bin file >> More (button on top of the file list) >> Make Public Step 4 : You S3 URL => http://bucket-name.s3.ap-south-1.amazonaws.com/sketch-name.ino.bin Step 5 : Build the above URL and fire it either in your browser or curl it `curl -I -v http://bucket-name.ap-south-1.amazonaws.com/sketch-name.ino.bin` to validate the same Step 6: Plug in your SSID, Password, S3 Host and Bin file below Build & upload Step 1 : Menu > Sketch > Export Compiled Library. The bin file will be saved in the sketch folder (Menu > Sketch > Show Sketch folder) Step 2 : Upload bin to S3 and continue the above process // Check the bottom of this sketch for sample serial monitor log, during and after successful OTA Update */ #include <WiFi.h> #include <Update.h> WiFiClient client; // Variables to validate // response from S3 int contentLength = 0; bool isValidContentType = false; // Your SSID and PSWD that the chip needs // to connect to const char* SSID = "YOUR-SSID"; const char* PSWD = "YOUR-SSID-PSWD"; // S3 Bucket Config String host = "bucket-name.s3.ap-south-1.amazonaws.com"; // Host => bucket-name.s3.region.amazonaws.com int port = 80; // Non https. For HTTPS 443. As of today, HTTPS doesn't work. String bin = "/sketch-name.ino.bin"; // bin file name with a slash in front. // Utility to extract header value from headers String getHeaderValue(String header, String headerName) { return header.substring(strlen(headerName.c_str())); } // OTA Logic void execOTA() { Serial.println("Connecting to: " + String(host)); // Connect to S3 if (client.connect(host.c_str(), port)) { // Connection Succeed. // Fecthing the bin Serial.println("Fetching Bin: " + String(bin)); // Get the contents of the bin file client.print(String("GET ") + bin + " HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "Cache-Control: no-cache\r\n" + "Connection: close\r\n\r\n"); // Check what is being sent // Serial.print(String("GET ") + bin + " HTTP/1.1\r\n" + // "Host: " + host + "\r\n" + // "Cache-Control: no-cache\r\n" + // "Connection: close\r\n\r\n"); unsigned long timeout = millis(); while (client.available() == 0) { if (millis() - timeout > 5000) { Serial.println("Client Timeout !"); client.stop(); return; } } // Once the response is available, // check stuff /* Response Structure HTTP/1.1 200 OK x-amz-id-2: NVKxnU1aIQMmpGKhSwpCBh8y2JPbak18QLIfE+OiUDOos+7UftZKjtCFqrwsGOZRN5Zee0jpTd0= x-amz-request-id: 2D56B47560B764EC Date: Wed, 14 Jun 2017 03:33:59 GMT Last-Modified: Fri, 02 Jun 2017 14:50:11 GMT ETag: "d2afebbaaebc38cd669ce36727152af9" Accept-Ranges: bytes Content-Type: application/octet-stream Content-Length: 357280 Server: AmazonS3 {{BIN FILE CONTENTS}} */ while (client.available()) { // read line till /n String line = client.readStringUntil('\n'); // remove space, to check if the line is end of headers line.trim(); // if the the line is empty, // this is end of headers // break the while and feed the // remaining `client` to the // Update.writeStream(); if (!line.length()) { //headers ended break; // and get the OTA started } // Check if the HTTP Response is 200 // else break and Exit Update if (line.startsWith("HTTP/1.1")) { if (line.indexOf("200") < 0) { Serial.println("Got a non 200 status code from server. Exiting OTA Update."); break; } } // extract headers here // Start with content length if (line.startsWith("Content-Length: ")) { contentLength = atoi((getHeaderValue(line, "Content-Length: ")).c_str()); Serial.println("Got " + String(contentLength) + " bytes from server"); } // Next, the content type if (line.startsWith("Content-Type: ")) { String contentType = getHeaderValue(line, "Content-Type: "); Serial.println("Got " + contentType + " payload."); if (contentType == "application/octet-stream") { isValidContentType = true; } } } } else { // Connect to S3 failed // May be try? // Probably a choppy network? Serial.println("Connection to " + String(host) + " failed. Please check your setup"); // retry?? // execOTA(); } // Check what is the contentLength and if content type is `application/octet-stream` Serial.println("contentLength : " + String(contentLength) + ", isValidContentType : " + String(isValidContentType)); // check contentLength and content type if (contentLength && isValidContentType) { // Check if there is enough to OTA Update bool canBegin = Update.begin(contentLength); // If yes, begin if (canBegin) { Serial.println("Begin OTA. This may take 2 - 5 mins to complete. Things might be quite for a while.. Patience!"); // No activity would appear on the Serial monitor // So be patient. This may take 2 - 5mins to complete size_t written = Update.writeStream(client); if (written == contentLength) { Serial.println("Written : " + String(written) + " successfully"); } else { Serial.println("Written only : " + String(written) + "/" + String(contentLength) + ". Retry?" ); // retry?? // execOTA(); } if (Update.end()) { Serial.println("OTA done!"); if (Update.isFinished()) { Serial.println("Update successfully completed. Rebooting."); ESP.restart(); } else { Serial.println("Update not finished? Something went wrong!"); } } else { Serial.println("Error Occurred. Error #: " + String(Update.getError())); } } else { // not enough space to begin OTA // Understand the partitions and // space availability Serial.println("Not enough space to begin OTA"); client.flush(); } } else { Serial.println("There was no content in the response"); client.flush(); } } void setup() { //Begin Serial Serial.begin(115200); delay(10); Serial.println("Connecting to " + String(SSID)); // Connect to provided SSID and PSWD WiFi.begin(SSID, PSWD); // Wait for connection to establish while (WiFi.status() != WL_CONNECTED) { Serial.print("."); // Keep the serial monitor lit! delay(500); } // Connection Succeed Serial.println(""); Serial.println("Connected to " + String(SSID)); // Execute OTA Update execOTA(); } void loop() { // chill } /* * Serial Monitor log for this sketch * * If the OTA succeeded, it would load the preference sketch, with a small modification. i.e. * Print `OTA Update succeeded!! This is an example sketch : Preferences > StartCounter` * And then keeps on restarting every 10 seconds, updating the preferences * * rst:0x10 (RTCWDT_RTC_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT) configsip: 0, SPIWP:0x00 clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00 mode:DIO, clock div:1 load:0x3fff0008,len:8 load:0x3fff0010,len:160 load:0x40078000,len:10632 load:0x40080000,len:252 entry 0x40080034 Connecting to SSID ...... Connected to SSID Connecting to: bucket-name.s3.ap-south-1.amazonaws.com Fetching Bin: /StartCounter.ino.bin Got application/octet-stream payload. Got 357280 bytes from server contentLength : 357280, isValidContentType : 1 Begin OTA. This may take 2 - 5 mins to complete. Things might be quite for a while.. Patience! Written : 357280 successfully OTA done! Update successfully completed. Rebooting. ets Jun 8 2016 00:22:57 rst:0x10 (RTCWDT_RTC_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT) configsip: 0, SPIWP:0x00 clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00 mode:DIO, clock div:1 load:0x3fff0008,len:8 load:0x3fff0010,len:160 load:0x40078000,len:10632 load:0x40080000,len:252 entry 0x40080034 OTA Update succeeded!! This is an example sketch : Preferences > StartCounter Current counter value: 1 Restarting in 10 seconds... E (102534) wifi: esp_wifi_stop 802 wifi is not init ets Jun 8 2016 00:22:57 rst:0x10 (RTCWDT_RTC_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT) configsip: 0, SPIWP:0x00 clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00 mode:DIO, clock div:1 load:0x3fff0008,len:8 load:0x3fff0010,len:160 load:0x40078000,len:10632 load:0x40080000,len:252 entry 0x40080034 OTA Update succeeded!! This is an example sketch : Preferences > StartCounter Current counter value: 2 Restarting in 10 seconds... .... * */ |
refer: