内容简介:通过智能锁保护您的环境,第 2 部分将 NodeMCU 开发板连接到电动锁,并使用一个基于云的简单 IoT 应用程序打开或关闭该锁在上一篇文章中,您学习了如何构建一个在未联网环境中使用的智能锁,在该环境中,锁本身无法与互联网进行通信,用户需要使用一次性密码执行身份验证,才能打开智能锁。
通过智能锁保护您的环境,第 2 部分
为一个联网环境构建一个智能锁
将 NodeMCU 开发板连接到电动锁,并使用一个基于云的简单 IoT 应用程序打开或关闭该锁
系列内容:
此内容是该系列 2 部分中的第 # 部分: 通过智能锁保护您的环境,第 2 部分
https://www.ibm.com/developerworks/cn/library/?series_title_by=**auto**
敬请期待该系列的后续内容。
此内容是该系列的一部分: 通过智能锁保护您的环境,第 2 部分
敬请期待该系列的后续内容。
在上一篇文章中,您学习了如何构建一个在未联网环境中使用的智能锁,在该环境中,锁本身无法与互联网进行通信,用户需要使用一次性密码执行身份验证,才能打开智能锁。
在本文中,我将介绍如何使用相同的硬件在 联网 环境中实现智能锁,在该环境中,中央服务器可以确定人们何时有权打开锁。您需要完成上一篇文章中的步骤来设置 NodeMCU 开发板,配置该开发板,并搭建电路。
构建这个智能锁需要做的准备工作
- 来自上一篇教程的硬件:一个电力控制锁、一个 9V 电池、一个电池连接器,以及一个数字继电器。您还需要 NodeMCU 开发板、一个搭建电路的电路试验板,以及一些连接所有元件的电线。
要进一步了解 NodeMCU 开发板,请查阅“ 了解 NodeMCU 和它的 DEVKIT 开发板 ”。
- 按照上一篇教程中的描述设置 NodeMCU 开发板。为该开发板构建固件时,请选择以下模块: crypto 、 file 、 GPIO 、 HTTP 、 net 、 node 、 SJSON 、 timer 和 WiFi 。因为这个智能锁要连接到互联网,所以请确保包含了 TLS/SSL 支持。同样地,您可以获取整数版本。最后,将 NodeMCU 闪存到您构建的固件中。
- IBM 云平台 IBM Cloud 上的一个帐户。
1
创建一个打开或关闭该智能锁的简单 IBM Cloud Functions 操作。
IBM Cloud Functions (基于 Apache OpenWhisk)是一个功能即服务 (FaaS) 平台。它支持以非常低的成本在互联网上运行持续可用的服务(因为没有持续运行应用程序的资源需求,所以功能仅在需要时激活,并在随后的几分钟内运行)。
在这个应用程序中,IBM Cloud Functions 为锁提供了需要打开还是关闭的决定权。第一步是编写一个随机打开或关闭该锁的非常简单的操作。
- 转到 IBM Cloud 控制台 。
- 从汉堡包形状的菜单中,选择 Functions 。
- 单击 Start Creating 。
- 单击左侧边栏中的 Develop 。
- 单击 Create an Action 。
- 将该操作命名为
lock_query
,保留默认执行运行时 (Node.js),并单击 Create Action 。 - 将 main 函数替换为此代码:
function main(params) { return { open : Math.random() > 0.5 }; }
- 单击 Run this Action 。
备注:如果没有看到 Run this Action 按钮,请拉大您的浏览器窗口。 - 单击 Run with this Value 。如果您修改了一个现有操作,则需要先单击 Make It Live 。请注意,输入无关紧要,可以在这里放入任何 JSON 结构。
- 在调用控制台打开时,可以多次单击 Run Again 来查看结果的变化。
- 单击 Close 离开调用控制台。
2
以 API 形式对外提供该操作
您已经能从 IBM Cloud 外部访问该操作(在 Develop 视图中,单击 View REST Endpoint , 下滚到 cURL 示例,单击 Show Full Example ) ,但是将该操作用作对外 API 的一部分会更简单、更灵活。
- 单击左侧边栏上的 APIs (假设您仍在 IBM Cloud Functions 中)。
- 单击 Create Managed API 。
- 将该 API 命名为
SmartLock
,
并指定基础路径/smartlock
。 - 单击 Create operation , 使用以下参数创建一个操作:
参数 值 路径 /lock_query 动词 GET 包含操作的包 默认值 操作 lock_query 响应内容类型 application/json - 单击 Save 。然后,向下滚动并单击 Save & expose 。
- 单击该图标来复制路径:
- 将该路径作为 URL 粘贴到浏览器窗口中,在 URL 末尾输入
/lock_query
。重新加载几次,以了解您获得的两种响应。
3
编写 Lua 程序来锁住或打开智能锁
该操作返回的值(open 或 locked)是没有意义的,除非该锁的 NodeMCU 会检索它并执行相应的操作。我编写了一个实现此操作的 Lua 程序。
- 运行 esplorer.bat,您已在上一篇文章中安装它。
- 复制 此代码 ,将它粘贴到 ESPlorer 上的左侧文本区域。
- 修改 Lua 程序顶部的参数:SSID、wifi 密码和 API 的 URL。
- 单击 Send to ESP 按钮。
让我们看看为该锁运行的 Lua 代码。您可以看到,配置参数位于顶部,这使得它们很容易管理。
-- Configuration parameters -- change these values for your environment ssid = "<<redacted>>" wifi_pwd = "<<redacted>>" openwhisk_url = "https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api/ec74d9ee76d47d2a5f9c4dbae2510b0b8ae5912b542df3e2d6c8308843e70d59/smartlock/lock_query" lock_pin = 2 -- The GPIO pin connected to the lock wait_time = 1 -- How long to wait until -- we ask OpenWhisk again
我无法让 NodeMCU http.get 函数 兼容 HTTPS,所以我编写了自己的函数。这个函数有两个参数 - 要获取的 URL 和一个在获取它后运行的回调函数。
-- Get an HTTPS response. -- According to the docs, http.get should support https -- URLs.However, I couldn't get that working. function getHttpsResponse(url, cb)
Lua 函数可以有 多个返回值 。 string.match 函数 为模式中的每个括号返回一个值。在本例中,第一个括号是主机名,以一个斜杠结尾。第二个括号是路径,包含任何查询字符串。
host, path = string.match(url, "https://([^/]+)/(.+)")
此连接需要是 TLS 连接,而不是普通的 TCP 连接。
conn = tls.createConnection()
IBM Cloud Functions 与 NodeMCU 之间的通信受到 HTTPS 的保护。通常,这足以让人信任服务器的身份。但是,因为 NodeMCU 的资源非常受限,所以它没有 CA 证书,这意味着如果黑客能控制 DNS 服务器,他们就能将该服务器 ( service.us.apiconnect.ibmcloud.com
) 重定向到自己的服务器并打开锁。
NodeMCU 有一个针对此问题的解决方案。 tls.cert.verify()
函数允许存储一个 CA 证书并要求证书经过该 CA 签名。要获取证书,请在浏览器中访问 https://service.us.apiconnect.ibmcloud.com (忽略 404 错误)。对于不同浏览器,从此站点获取该文件的过程可能有所不同。
如果不想通告某个锁已打开或关闭的事实,可以在该锁发送到 IBM Cloud Functions 操作的请求中包含一个共享密钥作为身份验证字符串。
我们建立连接后,发送该请求。请注意,这里对包含换行符的字符串使用了 [[<string>]]
,这种语法比 "\r\n"
更清晰。不幸的是,此语法无法识别空行 - 而 HTTP 协议需要这些行。
-- Don't send the request before we are connected conn:on("connection", function(sck, c) req = "GET /" .. path ..[[ HTTP/1.1 Host: ]] .. host .."\r\n" ..[[ Connection: close Accept: */*]] .."\r\n\r\n" sck:send(req) end) -- of conn:on("connection") callback
收到应答后,使用 string.match
忽略标头字段(两个换行符之前的所有内容),保留实际的响应。此代码假设会将响应放在一个包中,所以应合理地考虑来自操作的响应长度。
conn:on("receive", function(sck, c) resp = string.match(c, ".+\r\n\r\n(.+)")
使用 sjson 包
解码从操作中收到的 JSON 结构。
decoder = sjson.decoder({}) decoder:write(resp)
使用该结果来调用回调函数。
cb(decoder:result()) end) -- coon:on("receive") callback
到此刻,我们已添加了两个我们需要的事件处理函数(用于连接和接收响应)。现在可以实际连接到服务器了。
conn:connect(443,host); end -- of getHttpsResponse
此函数调用了 IBM Cloud Functions 上的操作,使用响应,然后设置一个要再次运行的计时器。
-- Call the action, and open or close the lock -- based on the response function openOrCloseLock() getHttpsResponse(openwhisk_url,
回调函数执行了大部分工作。
function(t) print(t.open) if (t.open) then gpio.write(lock_pin, 1) else gpio.write(lock_pin, 0) end -- if then else -- Call again in wait_time seconds tmr.create():alarm(wait_time*1000, tmr.ALARM_SINGLE, openOrCloseLock) end) -- getHttpsResponce callback end -- openOrCloseLock
请注意,这个方法的效率很低,每隔几秒就会检查一次服务器上的锁的状态。NodeMCU 上需要的额外处理能力实际上是免费的(毕竟 NodeMCU 没有执行任何操作),但是网络带宽和服务器级处理不是免费的。让锁仅在用户要求它们打开时询问自己的状态,这样做会更高效。可以使用一个按钮或 Web 界面实现此特性。
要使用按钮,请将它连接在 NodeMCU 上的一个未用的数据引脚(D0、D1、D3 等)与接地引脚之间。将它配置为生成一个调用 openOrCloseLock
的中断:
pin = 1 gpio.mode(pin, gpio.INT, gpio.PULLUP) gpio.trig(pin, "down", function(level, time) openOrCloseLock() end )
要使用 Web 界面,可以对为智能锁提供其 IP 地址的接入点进行配置,每次都为它提供相同的 IP 地址,并放置一个可供用户从智能手机使用的 HTTP 服务器。它们需要访问一个 URL(在下面的代码中,它是该服务器上的任何 URL)。准确的路径和响应无关紧要。
httpServer = net.createServer(net.TCP) httpServer:listen(80, function(conn) conn:on("receive", function(conn, payload) print(payload) conn:send("Querying the server about the lock") openOrCloseLock() end) -- of the conn:on function end) -- of the httpServer:listen function
如果锁处于打开状态, openOrCloseLock
函数需要定期检查它是否能再次关闭。如果锁处于关闭状态,那么在用户请求之前不需要检查它。将该函数的结尾处的代码替换为此代码:
-- If the lock is open, call again in -- wait_time seconds if (t.open) then tmr.create():alarm(wait_time*1000, tmr.ALARM_SINGLE, openOrCloseLock) end -- if t.open end) -- getHttpsResponce callback end -- openOrCloseLock
在 NodeMCU 启动时,无法立即使用互联网。首先需要与一个接入点关联,然后获取一个 IP 地址。 wifi.eventmon.register
函数 允许我们在获取该 IP 地址时运行一个函数,并可以开始使用互联网。
-- There's no point doing anything until we get an -- IP address from the access point wifi.eventmon.register(wifi.eventmon.STA_GOT_IP, function(t) openOrCloseLock() end) -- wifi connected function
只要设备启动,就会执行此代码(将程序写入 init.lua 后)。它会设置 wifi 模式、连接,然后设置控制锁的引脚的模式。
-- Actually connect wifi.setmode(wifi.STATION) wifi.sta.config({ ssid = ssid, pwd = wifi_pwd }) gpio.mode(lock_pin, gpio.OUTPUT)
如果我们在多个位置都拥有智能锁,那么区分它们来避免开错门就很重要。IBM Cloud Functions 有一个简单的解决方案。如果我们将查询字符串附加到该 API 的 URL,该操作会在其输入结构中获得查询字符串值。
为了在我们的程序中添加此特性,我们修改了 URL 参数:
openwhisk_url = "https://service.us.apiconnect.ibmcloud.".. "com/gws/apigateway/api/ec74d9ee76d47d2a5f9c4dbae2510" .. "b0b8ae5912b542df3e2d6c8308843e70d59" .. "/smartlock/lock_query_2?chip=" .. node.chipid()
该定义中唯一的新的部分是 node.chipid()
调用。此调用提供了一个唯一的芯片标识符。
可以 在这里看到针对 NodeMCU 的完整程序 。将它保存为 init.lua,让它在设备启动后自动运行。
4
使用 Cloudant 存储智能锁信息
随机打开和关闭的锁并没有用。下一步是建立一个 Cloudant 数据库来存储锁、锁的位置和状态。
4a
创建 Cloudant 数据库来存储锁信息
- 在 IBM Cloud 控制台中,从汉堡包形状的菜单中,单击 Data & Analytics 。
- 单击 Create Data & Analytics service , 并选择 Cloudant NoSQL DB 。
- 将该服务命名为
SmartLock-System
并单击 Create 。 - 创建该服务后,打开它。然后单击 Service credentials 。 单击 New credential 按钮。
- 将新凭证命名为
SmartLockAction
,并单击 Add 。 - 单击 View credentials , 并单击复制图标将凭证复制到一个文本文件中。
- 在左侧边栏中,单击 Manage 。 然后单击 LAUNCH 按钮。
- 在左侧边栏中单击 Databases 菜单。
然后,单击右上角的 Create Database 。 - 将该数据库命名为
smartlocks
。 - 不要创建任何文档。我们的操作会自动创建它们来支持注册。
4b
配置该操作来使用 Cloudant
除非 IBM Cloud Functions 实际使用 Cloudant 数据库,否则该数据库对我们没有任何用。创建一个新操作,或者将现有的 query_lock
操作替换为 此代码 。记得将 Cloudant 凭证替换为您的值,如果您创建了一个新操作,请记得将它添加到 API 中,并修改 Lua 代码来访问新操作。
此刻,您应该能通过更改 Cloudant 数据库来开门和关门(在智能锁第一次连接时,Cloudant 数据库会获得该锁的一个文档):
- 从控制台的汉堡包形状的菜单中,单击 Data & Analytics 。
- 单击 SmartLock-System , 然后单击 LAUNCH 。该数据库将在一个单独的选项卡中打开。
- 单击 smartlocks 数据库。
- 从列表中选择您的锁(它应该是唯一的列表项)。
- 将 open 值从 false 更改为 true,并单击 Save Changes 。
- 可以看到该锁打开了。可以再次更改该值来关闭它。
让我们看看这段与 Cloudant 进行通信的操作代码。它首先包含数据库凭证。它复制了完整的凭证,因为这样更容易且存储很便宜,但从技术上讲,您只需要 URL 字段:
var cloudantCred = { "username": "<<redacted>>", . . . "url": "https://<<user name redacted>>:<<password redacted>>@4d1cded5-56a3-4ad9-a59a-9c68c192995c-bluemix.cloudant.com" };
此操作非常简单,可以放在一个 JavaScript 函数中。
function main(params) {
以下行连接到凭证 URL(其中包含用户名和密码)上的 smartlocks
Cloudant 数据库。
var cloudant = require("cloudant")(cloudantCred.url); var mydb = cloudant.db.use("smartlocks");
数据库查找是一个异步过程,所以该操作无法立即返回结果。如果遇到这种情况,系统会返回一个 Promise 对象来指定如何操作。对象构造函数有一个参数 - 为了获得结果而要运行的函数。此函数接收两个参数,一个是要在成功时调用的函数,另一个是要在失败时调用的函数。
return new Promise(function(success, failure) {
第一步是搜索数据库,使用芯片 IP 作为键来查找是否有一个针对该锁的文档。
mydb.get(params.chip, function(err, body) {
如果没有文档,那么这是第一次注册。在这种情况下, err.statusCode
为 404。但是,我们不能假设 err
实际存在。如果没有错误,它将为 null
。
// If there is no document, this is a new // smartlock to register if (err != null && err.statusCode == 404) {
创建新智能锁的文档,并通过芯片 ID 进行索引。默认位置是未知的,该锁的默认状态是关闭的。
mydb.insert( { "_id": params.chip, location: "unknown", open: false }, function() {
创建智能锁的条目后,返回它的当前状态(关闭)。
success({open: false}); }); // mydb.insert call
上面的函数调用是异步的。它发出一个请求,然后将一个条目添加到一个将在响应到达时调用的表中。然后,此函数将继续运行,但是运行它的剩余部分没有意义 – return
可以让我们在此刻结束运行该函数。
return ; } // end of a new smartlock to register
如果文档已存在,则返回其中的值。
// Return the read value success({open: body.open}); }); // mydb.get call }); // new Promise call }
5
构建一个用户界面,供锁管理员用于锁住或打开该智能锁
锁系统的用户界面 使管理员能够指定新注册的锁的位置,并查看和修改现有锁的状态。可以在 我的 Smart Connected Lock GitHub 存储库中查看它的源代码 。
创建该操作后,通过路径 /ui
将它添加到 API 中。确保将响应内容类型设置为 text/html ,以便浏览器将它作为网页处理。
让我们看看如何实现此操作。 main
函数将会检查是否有要执行的操作。如果没有,则会返回一个 Promise 对象,该对象调用 returnHtml
来创建要返回给用户的 HTML。
// If we get here, there is no action to do, // just return the HTML return new Promise(function(success, failure) { returnHtml(success); }); // new Promise object
如果有一个操作(打开锁、关闭锁或设置锁的位置),则返回一个 Promise
对象来指定新字段值,并调用 modifyEntry
。该函数会修改合适的条目,然后调用 returnHtml
。
if (params.action == "closeLock") return new Promise(function(success, failure) { modifyEntry(params.id, {open: false}, success); });
modifyEntry
函数首先从 Cloudant 获取该锁的现有条目的一个副本。
var modifyEntry = function(id, newVals, success) { mydb.get(id, function(err, res) {
接下来,它会迭代新值并创建或替换现有结果中的值。
Object.keys(newVals).forEach( function(key) {res[key]=newVals[key];});
我们需要保留时间戳,原因会在下一步中进行解释。
res.lastChange = Date.now();
我们将把修改后的版本插回数据库中,然后返回 HTML,与没有要执行的操作时一样。
mydb.insert(res, function(err, body) { returnHtml(success); }); // mydb.insert callback }); // mydb.get callback }; // end of modifyEntry
returnHTML
函数会检索整个 Cloudant 数据库。
var returnHtml = function(callback) { mydb.list({include_docs:true}, function(err, res) {
该数据库包含内部信息,所以我们使用一个 map
函数来仅保留我们需要的信息。 map
函数 在每个列表项上运行它作为参数接收的函数(在本例中为 res.rows
,表示数据库中的所有行),并在一个列表中返回结果。 箭头函数 (=>) 是定义一个函数的更简短的表示法。
var data = res.rows.map((entry) => { return { id: entry.id, location: entry.doc.location, open: entry.doc.open }; });
下一步是将锁条目按类型进行拆分:具有已知位置的锁(可从界面打开或关闭的锁)和具有未知位置的锁(需要在处理前为其分配一个位置)。该程序使用 filter
函数 来拆分这些条目。该函数返回一个列表,其中仅包含原始列表中该参数函数为其返回 true 的项。
var unknownLoc = data.filter( (entry) => {return entry.location == "unknown";}); var knownLoc = data.filter( (entry) => {return entry.location != "unknown";});
接下来,我们再次使用 map
,然后使用 reduce
将条目列表转换为一个 HTML 表的内容。我们为该 HTML 使用了一个 模板字面变量 ,所以会计算 ${<expression>}
并将它放在字符串中的该位置。例如, ${entry.location}
被当前条目中的 location 字段取代。
var knownLocRows = knownLoc.map((entry) => { return `<tr> <td> ${entry.id} </td> <td> ${entry.location} </td> <td> ${entry.open ? "Open" : "Locked"} <button class="btn ${entry.open ? "btn-danger" : "btn-success"}" type="button" onClick="window.location.href='ui?id=${entry.id}&action=${ entry.open ? "close" : "open"}Lock'"> ${entry.open ? "Lock" : "Unlock"} </button> </td> </tr>`; }); var knownLocTable = ""; if (knownLocRows.length > 0) knownLocTable = knownLocRows.reduce((a,b) => {return a+b;});
最后,将表内容嵌入到 HTML 表中(一个表用于具有未知位置的锁,另一个用于其他所有锁)。为了能使用 location 字段的内容,该 HTML 使用了 Angular 库 。
请注意,对于这个样本应用程序,我没有添加身份验证,但实际的应用程序需要它,或许可以使用 API 定义中指定的 OAuth 来实现此操作。
6
创建一个处理自动重新锁定的 IBM Cloud Functions 操作。
如果一扇门在需要打开时被锁住,人们将无法完成工作。用户将呼叫锁管理员来纠正这种情形。但是当一扇门意外地一直处于打开状态,这可能是一种长期未纠正的静默故障(安全问题)。
要解决此问题,可以使用 此代码 创建一个操作。它寻找目前已打开且其条目上次修改超过 5 分钟的锁,然后关闭它们。我假设 5 分钟时间对开一扇门已足够用;如果不够,您可以增加该时间。
要每隔 5 分钟运行此操作一次,可以执行以下步骤:
- 在 Develop 视图中打开该操作,然后单击 Automate this Action 。
- 单击 PERIODIC 磁贴,然后单击 NEW ALARM 。
- 单击 :MM ,以便每隔几分钟运行该操作一次,并输入
5
。将该触发器命名为every-five-min
,并单击 Create Periodic Trigger 。 - 单击 Next ,然后单击 This Looks Good 和 Save Rule 。
- 尝试将一个锁保持打开 10 分钟,看看它是否会自动锁住。如果重新加载浏览器页面,请记得删除查询(URL 中从问号到结尾部分)。
此操作中的大部分代码都与用户界面中使用的代码类似。但是,可能需要在一次调用中锁住多个锁。只有到达所有数据库修改的回调时,我们才应该运行 success 函数,它会报告我们已完成的工作。
我们将使用全局变量 leftToLock
和要修改的剩余锁的数量。如果从 mydb.list
回调中获取它的值,该值为修改列表的大小。
var leftToLock; … function main(params) { return new Promise(function(success, failure) { mydb.list({include_docs:true}, function(err, res) { … // We only care about entries that haven't // been changed in the last five minutes var now = Date.now(); data = data.filter((entry) => {return now-entry.lastChange > 5*60*1000;}); // We only care about those entries that // have an open lock data = data.filter((entry) => {return entry.open}); // Lock the entries in data. leftToLock = data.length; data.map((entry) => {lock(entry.id, success)}); }); // mydb.list }); // new Promise } // main
lock
函数的每次调用都有一个对 leftToLock
执行递减的回调。当递减到 0 时,最终的 lock
函数调用回调将会调用 success 函数。
var lock = function(id, success) { mydb.get(id, function(err, res) { res.open = false; res.lastChange = Date.now(); mydb.insert(res, function(err, body) { leftToLock --; if (leftToLock == 0) { // We're done success({}); } // leftToLock == 0 }); // mydb.insert }); // mydb.get }; // end of lock
请注意,当 5 分钟时间不足以修改 Cloudant 数据库中所有需要重新锁住的锁时,此算法将失败。但这种情况几乎不可能出现。
结束语
在本文中,您学习了如何使用 IBM Cloud Functions 实现一个连接到互联网的智能锁。除了这个用例之外,您现在应该能够:
- 使用 NodeMCU 作为 HTTPS 客户端,包括解析服务器响应
- 创建 IBM Cloud Functions
- 创建 API 来访问这些 IBM Cloud Functions
- 使用 Cloudant 数据库来存储信息
- 从 IBM Cloud Functions 访问并修改 Cloudant 数据库中的信息
- 创建一个用户界面作为 IBM Cloud Function
- 定期运行一个 IBM Cloud Function 来执行各种维护操作
希望您在未来开发自己的 IoT 产品和应用程序时,这些技能会派上用场。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 3D 环境训练智能体时代来临?Facebook 开源仿真 3D 环境平台 Habitat
- 涂鸦智能的 Istio 企业级生产环境的实践
- 十一贝:航延险智能判定,公平消费环境惠及消费者
- 开源智能手机 Librem 5 将使用 GNOME 3.32 桌面环境
- 开源智能手机 Librem 5 将使用 GNOME 3.32 桌面环境
- 我们如何只用太阳能电池收集的环境能量为人工智能提供动力?
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
人类思维如何与互联网共同进化
[美] 约翰·布罗克曼 / 付晓光 / 浙江人民出版社 / 2017-3 / 79.90元
➢人类是否因互联网的诞生进入了公平竞争的场域? “黑天鹅事件”频频发生,我们的预测能力是否正在退化? 智人的第四阶段有哪些特征? 全球脑会使人类成为“超级英雄”吗? 虚拟现实技术会不会灭绝人类的真实体验? 还有更多不可预知答案的问题,你将在本书中找到属于自己的答案! ➢ 我们的心智正和互联网发生着永无止境的共振,人类思维会因此产生怎样的进化效应?本书编者约翰•布......一起来看看 《人类思维如何与互联网共同进化》 这本书的介绍吧!