问候,亲爱的!
在他们生活的某个阶段,每个顽固的DIY盒子都不再像
他们无法做到的 “
自己的东西”一样错过Kantian Arduino
! :闪烁LED,从传感器获取数据并通过电线将其传输到PC肯定很有趣,但是圣杯却在于移动性,从“铜键”的解放,在通用以太波中的真正自由。
这就是不稳定的通信渠道,传输错误,未传递的消息的严峻现实。
上帝禁止在这一领域宣称独创性:人类长期以来在各种场合都使用了一系列协议。
但是我们的目标是学习,并且由于我是战斗侦察的热心支持者,因此我们将通过发明自己的“自行车”协议进行学习。
今天,我提议开发一种协议,以确保两个订户(点对点,点对点)之间的消息传递,完整性和消息顺序有保证,知道如何使用
Nagle算法和
协议流水线 ,无论这意味着什么。 同时,它应该具有最小的
开销 ,甚至可以挤入狭窄的Arduino UNO中。

我问所有在船上感兴趣的人,我们关闭舱口,打开国王石,填充压载舱。 我们游览了过去的目的地:1974年!
根据良好的传统,至少有两位该领域的知名专家参与了加密算法和协议的描述,如果没有其他人不认识它们,请结识:
和
首先我们描述一个简单的任务
爱丽丝(Alice)和鲍勃(Bob)坐在相邻的战es中,无法抬起头来见彼此。 他们只能说声音,旁边有子弹和哨子爆裂,淹没了他们的尖叫声,此外,当其中一个人讲话时,您必须尖叫,以至于根本听不到任何声音。
由于它们被敌人窃听了,因此情况变得复杂-而且由于某种原因,您必须使用一种由长数字序列组成的编码语言。
由于爱丽丝(Alice)和鲍勃(Bob)都是人,因此他们必须定期外出吃饭或上厕所,而且他们是如此不耐烦,以至于在最不适当的时刻不耐烦!
如何以及为什么建立连接?
在一切似乎注定要失败的情况下,我们如何才能在这种令人沮丧的情况下安排可靠的数据传输?
想到的第一个解决方案是使用
停用词代码短语来开始和结束传输。
好吧,假设爱丽丝要发送消息,那么她需要大喊“开始发送!”,然后等到鲍勃回答“开始接收!”。
如果爱丽丝不等待鲍勃的回应,她只需重复她的请求即可开始转移。 自然,您不应该经常这样做,否则,据我们所知,您只是听不到鲍勃的回答。
太好了 但是,如果作为响应的爱丽丝从下一个沟槽“开始传输!”中听到声音,会发生什么?
事实证明,鲍勃还决定立即传输一些重要信息。 爱丽丝性格温和,她可能会想:“好吧,我等一下,从原则上讲,我的信息并不紧迫,让鲍勃先把它传递出去。” 考虑到这一点,她回答:“开始接待!”。
由于
在战争时期正弦值可以达到4,所以声音
的速度是有限的,并且需要一些时间来理解爱丽丝和鲍勃所听到的声音,甚至鲍勃作为绅士也可以决定让位给这位女士,他耸耸肩大喊“我开始接受!” ...
为了说明这种愤怒,我们将使用时间表。 时间落在他们身上。
爱丽丝和鲍勃不同时的情况:

消息丢失的情况:

这是一场惨败。 一切变得太混乱了,并且由于收件人可以听到或不听到任何短语的事实而变得更加复杂,并且在每种情况下,对话者都不知道收件人是否听到了他的信息。
现在,爱丽丝和鲍勃都期待着受到欢迎。 意识到发生了冲突,并且有人需要恢复传输是合乎逻辑的。 但是,如果一切都以新的方式再次发生怎么办? 在这里,我们再次回到了起点。
如果您认为这种情况极为罕见,请记住您上一次通过语音与某人通话时,您的订户或您(或两者)的互联网连接速度很慢。 “你好,你好,你消失了。” “你听不到你好。”
同时,在战es中,局势正在升温,指挥官要求传递报告。
现在该
转向主要资源了:研究马克思,恩格斯将在40多年前回来,看看
DEC工程师在设计
DDCMP协议时如何解决这些问题。
根据DDCMP的开发人员所说,爱丽丝和鲍勃需要拒绝情绪,变得像
有限状态机 。
这意味着从现在开始,我们的爱丽丝和鲍勃将只有几个固定状态,当某些事件发生时,这些状态之间的转换可以严格按照某些规则进行。
首先,我们只列出状态:
如您所见,其中只有四个。 而现在,无论发生什么情况,每个订户至少都可以肯定地知道他相对于这些国家中的一个。 实际上,向前看,我会说几乎所有一个订户都会知道第二个订户所处的状态,
但这并不准确 。
让我们分别详细考虑状态
HALTED是最简单的状态,没有人到任何地方,每个人都留在自己的地方,没有任何东西被传输和接收,任何外部刺激都被忽略。 除了一个以外的所有-上级政府的意愿。 在原始DDCMP协议中,从
HALTED状态的过渡只能应用户的请求处于
INITIAL START状态-Alice或Bob收到建立连接的命令。
当爱丽丝或鲍勃收到这样的命令时会怎样?
他们立即向自己指出,状态已从“
暂停”更改为
“ 初始启动” ,此转换与其他任何转换一样,都包含严格定义的操作顺序。 在这种情况下,您需要大喊“ DO IT!”,然后将时钟设置为时钟。 仅此而已。
因此,爱丽丝大喊大叫,并按下秒表上的一个按钮。 现在,为了了解Bob的期望,我们将弄清楚当Alice处于
INITIAL START状态时会发生什么。
-从爱丽丝注意到时间已经过去的那一刻起,比如说10秒钟,她没有听到鲍勃的任何回应(请注意,我并不是说鲍勃没有对她大喊大叫-这是未知的,只是爱丽丝一无所知在这段时间里听到过,爱丽丝是一个睿智而理性的女人,完全依靠事实。 我们称此事件为超时-已超过等待间隔。 在这种情况下,该协议告诉我们重复:喊“ DO IT ONCE!”,然后再次计时。 还不厚。
-如果Alice听到Bob大喊大叫同一件事-“一次做一次!”,则Alice
选择性地进入
确认开始状态,对此她应立即大喊“做两次!”并重新设置时钟。
-再次,如果爱丽丝从鲍勃那里听到“做两件事!”,那么她立即进入“
正在运行”状态(!),大喊“接受否!”。 如果秒表已经启动,她会从谨慎的角度将其关闭。
重要的是不要进行当前状态未提供的任何不必要的移动。 无论鲍勃哭泣什么,无论诅咒或乞讨,爱丽丝都只会按照同意的方式做出反应。
以表格的形式呈现这些东西很方便。 因此,让我们从已描述的
HALTED和
INITIAL START状态开始,然后再补充表。
我有意识地从DDCMP的原始描述中忽略了一些要点-我们不需要它们,我们不仅希望重复DDCMP,而且希望
在相同的基础上构建
,只是另一个新协议。
但是回到状态和过渡的描述。 下一个状态是
ACKNOWLEDGED START 。
处于这种状态下,让Alice或Bob担心的是:
-与之前一样,等待时间已到期,在这种情况下,您需要保持相同的状态,尖叫“做两遍!”并再次启动计时器
-听到“ DO TWO!”的声音,然后喊“ ACCEPTED NOOOOL!”并转换为
RUNNING状态,并停止计时器;
-听到的“ DO IT!”保持不变,您需要喊“ DO TWO!2”并启动计时器。
-听到“ NOOOL ACCEPTED!”的声音-过渡到
RUNNING状态,停止计时器。
我们将以上所有内容放入表格中。
握手后,几乎所有东西都准备就绪-仅需考虑一种
正在运行状态,因为其中一个订户已经可以进入其中,而第二个订户则立即冲上厕所,等到他回来时,忘记一切并尝试建立新的连接。
从握手过程的角度来看(我们尚未处理已开始一切的数据传输-这是一个单独的故事),在
RUNNING状态下,我们对两个事件感兴趣:
-如果他们对我们大喊“立即做!”-一切都非常糟糕,完全不同步,需要重新开始一切。 原始协议
指导您仅进入
HALTED状态。 但这不会以任何方式对我们有帮助-如果由于某种原因在自主Arduino上发生这种情况,该Arduino从某些传感器传输了一些数据,那么对我们来说这是一个彻底的失败。 众所周知,您只能在政府指示下才能从
HALTED进入
INITIAL START 。
因此,我们在此处修改协议:“暂停”命令在
HALTED状态下的接收应像来自授权机构的命令一样工作-即
切换到INITIAL START状态,喊“ DO IT ONCE!”,启动计时器。 此外,在某些情况下,给自己供电后立即下达建立通讯的命令很方便。
因此,现在,在最不方便的情况下,我们将简单地重置连接。
-在“
运行”状态下有必要对之进行的第二个事件-如果我们从相邻的沟槽中听到“两声!”。 这已经更加有趣了。 在这种情况下,您需要大声喊“ ACCEPTED ER!”。其中,ER表示在当前通信会话中成功接收到的消息数。 这是一个新概念。 下面,我们将更详细地考虑所有内容,但是暂时,我们会将当前所学到的所有内容都放在表中:
现在,如果爱丽丝和鲍勃严格遵守该协议,那么他们只是没有任何选择
进入令人
费解的事情,除了如何建立连接,共同切换到
RUNNING状态或者在糟糕的情况下试图在
单击获胜之前建立连接。
一个有进取心的读者可以尝试对所有选项进行分类,得出结论:一系列状态和转换实际上是封闭且严格确定的。 现在,我们(在DEC工程师的帮助下)将爱丽丝和鲍勃与一套规则捆绑在一起,只需遵循这些规则即可建立联系,如果在当前条件下,原则上通常是可行的。
现在如何传输数据?
好的,那是一个很好的锻炼。 Candy-bouquet时期中两个网络节点的关系。 回想一下我们开始做生意:我们需要在保证交付和优先的情况下传输数据! 随着灾难恢复。 在一定程度上,硬件资源允许这样做(毕竟,Alice和Bob可能都被证明是具有2 KB RAM的弱8位控制器!)。
DEC工程师告诉我们,我们需要对邮件进行编号,我们需要计算发送的邮件数量,收到的邮件数量以及发送给收件人的邮件数量。
是时候离题了!承认吧 当我在DDCMP协议的描述中看到变量的名称时,我认为这绝非偶然:美国人非常喜欢用耳朵听精美的缩写。
为了我们的方便,甚至有几种资源可供那些感兴趣的人接触。
我最喜欢的是这个
-Dumb or Overly Forced Astronomical Acronyms Site(或DOOFAAS)这些制作物价值多少!
这是一个例子:
WASP-宽带模拟光谱仪(但根本不像您想的那样!)
SAURON-光学星云的光谱
地物单位
CISCO-用于OHS的冷却红外光谱仪和照相机(这就是它的意思!)
在这里,开火:
喷水 (是的,第18条以上!)-Satettile QUick Research测试台
SHIT (无论
多多还是少!)-超大型干涉望远镜,上面刻有“为自己寻找”字样,同名文章上附有摘要链接。
因此,在协议的原始描述中指示节点上已接收,已发送和已发送数据包数量的变量称为
RNA 。
啊,他们为什么不这样命名协议-RNA! 一种RNA网络。 如果情况有所不同,DECnet协议就有机会成为Internet协议。
但是回到我们的战es
原始协议标准定义所有计数器均为8位且以256为模递增。这意味着最多可以发送256条尚未收到确认的已发送消息。
并且,如果未收到确认,则可能需要重新传输它们;如果可能需要,则必须将它们存储直到确认。 毕竟,我们保证交货!
爱丽丝和鲍勃的身体参数决定了我们的不同情况。 在8位Arduino中,这些数据量根本无处可存放,我们必须妥协。 我并不是在谈论这样一个事实,即标准的数据包(消息)的长度以字节为单位限制为16位数字,即 64 KB是不可接受的奢侈!
因此,建立了连接。 接下来是什么?
当Alice或Bob
进入RUNNING状态时,计数器将重置。
正如我已经提到的,原始协议涉及以256为模的消息编号,但是我们必须减少该数量以适应类似Arduino的事物中的少量内存。
为了立即能够限制计数器的所有增量,我们将引入一定的常量UMCP_PACKETS_NUMBER,现在所有增量将在此模块中发生。
如果您使UMCP_PACKETS_NUMBER = 8,并且最大数据包大小为UMCP_PACKET_DATA_SIZE-一次传输的数据部分被限制为64个字节,那么一切都将适合Arduino UNO,并且仍会满足用户需求。
重要的是要记住,这两个参数对于双方都必须相同。
显然,现在,如果Alice和Bob成功建立了连接,并且其中一个需要传输数据,则必须首先将数据分为不超过64个字节的部分,其次,每个数据包还必须包含一个状态两个发送方计数器:已接收和已发送消息的数量(R和N)。
看看现在组织所谓的 流水线处理错误情况有多容易!
如果爱丽丝在建立连接后立即连续发送3个数据包,则所有这些数据包的计数器R都将设置为0(她尚未接收到任何数据包),并且计数器N将随每个新数据包增加1。
如果Bob成功地接受了所有数据包,那么为了确认收到所有三个数据包,仅对最后一个数据包发送确认就足够了,事实上,如果他只是简单地发回其计数器的状态R = 3和N = 0,那么Alice将立即理解所有已发送的数据包她的消息到达了收件人。
当没有不可抗力发生时,这是一个理想的情况。 现在,让我们看一下可能出问题的地方以及如何处理。
如果鲍勃出于某种原因跳过了第一个数据包并接受了下一个数据包,那么他会立即引起注意以下事实:其中的计数器N(爱丽丝发送的数据包数量)明显超过了鲍勃一侧的计数器R,而鲍勃很容易意识到自己错过了第一个数据包。 在这种情况下,他只需要打出最平庸的队长证据,然后将接收到的数据包的计数器状态告诉爱丽丝(R = 0)。 爱丽丝同时了解到她是N = 3,而鲍勃的R = 0,也就是说,有必要从第一种方法开始以新的方式传输数据包。
如果仔细看一下该方案,您会发现任何订户的任何计数器状态传输都会立即通知他或她数据包传输的结果,并且在一侧发送的计数器与在另一侧接收的计数器之间的差异表明丢失了多少个数据包,并且它从哪个号码开始。
也就是说,在最坏的情况下,传输会完全重传,在平均情况下,发送器侧的计数器A会增加到接收侧的计数器R的值,然后“发送”丢失的数据包。容易理解,通过这种方式可以保持计数器增量的连续性,这意味着可以保证消息(数据包)的传输。除RNA变量外,每个用户都有两个标志SACK和SPEP。如果安装了第一个,则需要发送确认(发送确认),如果安装了第二个,则需要发送确认请求(发送答复给消息)。顺便说一句,原始DDCMP中暗示了另一个标志-SNAK(发送负确认)。它的安装涉及发送带有某种代码的错误消息。但是在我们的协议版本中,我们将仅使用超时机制来解决所有错误,因为该协议可以用于例如通用频段的声纳或无线电通信中-用错误代码阻塞通用环境是没有意义的。如果收到的消息中存在完整性错误,则严格来说,这是不可接受的消息。在这一点上,腐蚀性的读者应该有某种缺失的感觉。这种苗条的方案出了点问题。这是真的。
稍后再详细介绍。同时,我建议按照连接建立过程的示例,收集有关将数据传输到表的所有零碎想法。由于我们现在只有一个状态,因此该表将仅包含两列-事件和要执行的操作。为了避免变量所属的变量之间的混淆,我们用L索引标记本地变量,并使用R索引标记已删除变量(包含在接收到的消息中的那些变量)。现在仔细看一下,在头上滚动整个电路。我们意识到这里缺少什么。在DDCMP的原始描述中,我们已经脱离了DDCMP,这被称为SELECT标志-一个节点(Alice或Bob)可能被“选择”,也可能未被“选择”。让我们感到困惑的是,没有任何机制被允许允许或禁止转移。好吧,这是:这是SELECT标志。它的应用非常简单:如果设置了标志,则可以传输,如果没有,则不可能。所有控制消息(例如ACK和REP)都必须包含此标志。队列中的最后一个数据包也必须包含此标志。如果节点将标志“缝制”到数据包中,则它“将其放弃”,因此不再安装。相反,在包中检测到此标志的节点必须自己安装它。这类似于传递警棍或玩百果馅(还记得吗?)。使用此标志最重要的是,默认情况下,其中一个节点必须具有此标志,而另一个节点则没有。那是另一个非常重要的计时器-SELECT标志返回计时器。现在我们有了一套完整的规则来建立连接并通过它传输数据。我们不仅触及这套规则的具体实施。好吧,解决它!包装形式和格式
这称为消息框架-用于分析和生成消息的规则以及格式。让我们计算一下我们需要多少。1.至少,我们需要每个消息都包含发送方计数器R和N的状态。对于Arduino,我们同意最多可以发送8条但未确认的消息。但是,由于我们传输字节,因此将两个计数器都压入一个字节,使其为4位。该字节的格式如下: = (RL & 0x0F) | (NL << 4);
我们将像这样读取计数器的状态:
NR = (c >> 4) & 0x0F; RR = c & 0x0F;
c-消息中的相应字节
2.我们还记得,每个消息都必须包含SELECT标志的状态。 消息本身的不同类型将是:
也就是说,只有6种不同类型的消息。 除DTA之外的所有消息都“释放” SELECT标志-它们需要远程订户的立即响应,没有该标志,他将无法传输它。 DTA消息不返回标志以使流水线成为可能。
通常,对于消息类型,我们有足够的3位,但是为了不干扰这些位,我们为该类型分配了一个完整的字节-如果进行修订,我们将有一定的操作自由度。
如果消息中包含数据,则我们需要传输其数量和校验和。 由于最大数据包大小为64个字节,因此我们还将为校验和和长度取一个字节-突然,您将不得不增加数据包大小。
3.我们还需要消息开头的一些签名和标头的单独校验和。
考虑到所有这些,标头(又称控制消息)如下所示:
数据块是这样的:
仅此而已。 这是我们从DDCMP获得的协议的完整描述。
现在您可以完成实施。
它是如何排列的以及如何使用?
首先,介绍一下存储库的结构。
正如我在一开始所提到的,项目代码位于github上:
uMCPIno为了查看一切工作原理,您可以在PC上运行
测试应用程序 。
在存档中,运行uMCPIno_Test.exe,选择所需的COM端口,然后尝试其工作方式。
您可以检查一对虚拟COM端口(我通常这样做)。
为什么可以运行该应用程序的两个副本。 只是不要忘记以一个副本打开“默认选择”(这将是主副本,而在另一个副本中)将其关闭。 顺便说一句,如果有兴趣,您可以看到如果不遵守此规则会发生什么情况=)
EXTRAS选项使您可以查看协议大脑内部的所有思想运动。 将显示SELECT标志状态的所有变化,计时器的事件,节点状态的变化以及已发送和已接收消息中的变量R和N的值。
我通过UART <-> USB转换器将Arduino UNO连接到笔记本电脑。 引脚连接器使您可以随时模拟换行符:

如果现在在笔记本电脑上运行该应用程序,则在按下“ CONNECT”按钮之后,arduina将建立连接:

这是系统对尝试通过“破损”线路发送邮件的反应:

要将uMCPIno嵌入到PC的应用程序中:
- 该存储库有一个uMCPIno库。 将其连接到项目的参考
- 它包含uMCPInoPort类。 我们声明其实例:
uMCPInoPort port; port = new uMCPInoPort("COM1", UCNLDrivers.BaudRate.baudRate9600, true, 8100, 2000, 64, 8);
参数的顺序为:端口名称,然后是端口速度,默认的SELECT状态,SELECT的间隔,超时间隔,数据包大小以及未确认消息的最大数目。
- 订阅事件:
当SELECT-port.Select标志更改时:
OnSelectChangedEventHandler
状态更改时-port.State:
OnStateChangedEventHandler
远程主机确认收到代码:
OnDataBlockAcknowledgedEventHandler
数据包何时到达:
OnDataBlockReceivedEventHandler
- 在工作之前,打开端口
port.Open();
- 要发送数据,我们调用方法:
port.Send(byte[] data);
- 完成后,关闭端口:
port.Close();
只需发送两个字节!
现在让我们继续进行Arduino的实现。
github.com/AlekUnderwater/uMCPIno/tree/master/Arduino文件夹中有两个示例
第一个只是往返于uMCP的转换器。 第一个串行用于与主机通信,而串行1(如果在您的主板上)或针2和3上的SoftwareSerial用于与另一个uMCPIno节点通信。 您可以在此处连接蓝牙或无线电模块。
第二个是支持uMCPIno协议的项目模板
这两个项目都有您可以并且应该攀爬的设置。 它们是:
SELECT标志的默认状态。 如果设置为(true),则即使远程节点不返回标志,计时器也会将其设置为true。
#define CFG_SELECT_DEFAULT_STATE (false)
要设置此计时器的时间,请进行以下设置:返回SELECT标志的间隔(以毫秒为单位)
#define CFG_SELECT_DEFAULT_INTERVAL_MS (4000)
等待响应的时间间隔(以毫秒为单位),最好使其稍小于返回SELECT标志的时间间隔。
#define CFG_TIMEOUT_INTERVAL_MS (3000)
线路的实际波特率。 需要此参数来确定传输何时结束。
#define CFG_LINE_BAUDRATE_BPS (9600)
Nagle算法的数据累积间隔。 毫不客气地将其取为100毫秒。 在此期间,我们正在等待一组软件包,如果未键入,则将其照原样发送。 Nagle算法的任务是清除网络中一堆小包,大小从一个字节到几个字节。
#define CFG_NAGLE_DELAY_MS (100)
这些设置设置与控制系统(主机)和线路进行通信的端口速度。 不要将端口速度与具有物理传输速率的线路混淆。
#define CFG_HOST_CONNECTION_BAUDRATE_BPS (9600)
如果启用此设置,则当为控制器供电时,协议本身将命令自身开始建立连接。
#define CFG_IS_AUTOSTART_ON_POWERON (true)
这是传入数据包的缓冲区大小(以字节为单位)。
#define CFG_IL_RING_SIZE (255)
接下来,让我们看看主要的草图循环是什么样的:
void loop() { uMCP_ITimers_Process(); DC_Input_Process(); DC_Output_Process();
现在让我们看看该协议是如何工作的。 主要逻辑包含在uMCP_Protocol_Perform()函数中; 这是她的代码:
void uMCP_Protocol_Perform() { if (state == uMCP_STATE_RUNNING) {
功能中的数据包解析器
On_NewByte_From_Line
也根据有限状态机的原理进行排列,并且“逐字节”工作。 这样做是为了节省内存。
其余的实现没有特别的意义。 我们将更好地分析用户如何与协议进行交互。 在此示例中,有四个“联系点”。
第一个是在uMCPIno行上发送数据的功能:
bool uMCPIno_SendData(byte* dataToSend, byte dataSize);
这里的一切都很简单-您有一个字节缓冲区dataToSend,其大小为dataSize。 如果可以发送(有添加数据的空间),则该函数返回true,否则返回false。
为了避免徒劳,您可以使用以下功能立即检查是否有足够的空间:
bool uMCP_IsCanSend(byte dataSize);
为了分析传入的数据包,您需要将代码添加到功能主体中
void USER_uMCPIno_DataPacketReceived();
传入数据被写入il_ring环形缓冲区。 从中读取可以像这样组织字节:
while (il_Cnt > 0) { c = il_ring[il_rPos]; il_rPos = (il_rPos + 1) % CFG_IL_RING_SIZE; il_Cnt--;
为了提供愉悦的娱乐,这里有一个功能
void USER_uMCP_OnTxBufferEmptry();
成功发送所有数据时调用该方法。 也可以在其中放入某种代码。
为什么是这一切?在哪里?
我主要是为了好玩。 另外,我需要一些简单且最重要的“轻量级”协议来通过我们的
uWAVE声纳调制解调器发送数据。 由于它们通过水以80 bps的速度传输数据,并且最大通信距离为1000米,并且水中的声速约为1500 m / s,因此传输具有明显的延迟,并且只有一个声纳通道(如果不是!)最吵,最慢和最不稳定。
很大程度上由于这个原因,我不得不放弃否定确认(NAK)的机制-如果可能无法传播-在水中最好不要传播100%。
实际上,该协议在使用
DORJI模块和
arduinoes熟知的
NS-012通过无线电信道传输数据时派上了用场。
接下来是什么?
如果有时间,我计划增加寻址的可能性(顺便说一句,它在DDCMP中)。 由于此协议的主要任务是为我们的声纳调制解调器和其他传感器网络的各种测试提供便利,因此存在(字面上!)陷阱。 我只能说,仅添加“发件人”和“目标”字段并不能解决问题。
也许它将涉及到
地理路由和所有爵士乐。
聚苯乙烯
传统上,我将非常感谢建设性的批评,希望和建议。 了解您是在做对人有用的事情还是在浪费时间,始终很重要。
也许,在试图避免将这本长篇小说过渡到小说《战争与和平》时,我错过了一些细节-请不要犹豫。
PPS
非常感谢我对文盲的羞辱,指出了错误(语法和逻辑上的错误):
该项目最初是开源的,但是现在文章也是开源的。