Navigation

    蓝鲸ROS机器人论坛

    • Register
    • Login
    • Search
    • Categories
    • Tags
    • Popular
    1. Home
    2. weijiz
    3. Posts
    ROS交流群
    ROS Group
    产品服务
    Product Service
    开源代码库
    Github
    官网
    Official website
    技术交流
    Technological exchanges
    激光雷达
    LIDAR
    ROS教程
    ROS Tourials
    深度学习
    Deep Learning
    机器视觉
    Computer Vision
    • Profile
    • Following 0
    • Followers 11
    • Topics 248
    • Posts 705
    • Best 11
    • Groups 1

    Posts made by weijiz

    • 视觉导航第一次应用于生产环境了

      最近接到了用户的一个机器人订单。用户需要做一个展览用机器人,放在他的展厅里面。整个展厅长大概有三十多米,用户展览的产品在展厅一侧依次排开。用户需要机器人在每个展览的产品处停下并介绍这个产品。目前这个用户的机器人采用的是循迹的方法,在地板上贴上黑色的路线,并在上面标明这时第几个产品。但是用户对这些黑线很不满意,觉得破坏了整个展厅的氛围。

      这种情况就必须用到我们的视觉定位系统了。然而视觉我们的视觉定位系统尚未成熟,程序能否准确稳定的运行也没有把握。但是用户既然有这样的需求也就只能试试了。

      上面的视频就是视觉导航的效果展示。可以看到视觉定位的精度很高,工作起来也非常稳定。程序设置小车按照和黑线一致的方向运行。无论人为如何干扰,小车都能回到原来的方向。这个是利用陀螺仪和编码器的惯性导航无法做到的。惯性导航一旦轮子发生打滑,定位就会产生无法修复的误差。

      上面是一个范围更加广的测试视频。至此小强的视觉导航系统终于被用于生产环境了。

      posted in 最新公告
      weijiz
      weijiz
    • 来退个火吧

      模拟退火算法是一个非常好用且简单的算法。它的思路也非常简单,下面就介绍一下模拟退火算法。

      什么是模拟退火算法呢?
      先从这个算法要解决的问题说起。实际上所有的算法都是为了一个目的——从解空间中把解给找出来。最好的当然是找到全局最优解,但是有时候局域最优解也是可以接受的。现在以一个简单的函数求极值的问题作为例子。
      如下图这样一个函数,我们要找到其中的最小值。
      0_1467440642581_func.png

      最直接的方法是遍历所有的点。找到其中的最小点。但是很多时候由于解空间太大我们根本没有办法去遍历。这时候就需要高效的算法了。
      比较直接的一种算法就是从图上任意一点出发,向两边开始移动,如果新位置的值小于之前位置的值,那么就移动到新的位置。在新位置再重复这个过程,直到无论怎么移动都没有当前的位置的值小的点。可以想象一个小球在曲线上滚动,最终会滚到一个很低的位置。这样我们就不需要遍历所有的点了。但是很有可能我们停在了一个极小值,这个值并不是全局的最有解。

      上面这个算法就是贪心算法,也叫做爬坡算法。而模拟退火算法就是从这个算法中改出来的。

      退火就是金属的冷却过程,刚开始温度比较高,原子运动比较剧烈,随着时间的推移,温度逐渐下降,原子运动也逐渐稳定。而模拟退火就是在模拟这个过程。整体的算法过程和贪心算法一样。但是移动的步长会随着时间在减小。刚开始移动比较剧烈,随着时间的推移每次的移动范围逐渐减小。这样我们就更大的可能性能落在全局的最优点上。而且搜索的速度也要快很多。

      下面就是模拟退火算法在小强中的具体应用。
      小强的摄像头能够通过SLAM算法得到小强的位置信息,但是这是在摄像头坐标系下的坐标。通常摄像头坐标系和小车坐标系只是差了一个平动和绕竖直轴的转动。但是如果摄像头放歪了,比如向上歪或向下歪。那么小车移动的最终轨迹在摄像头坐标系下就是一个倾斜的平面。我们要在这个平面内画出目标的导航轨迹,首先就要把这个轨迹放平。那就要知道轨迹平面的法向量。怎么计算这个法向量呢?
      我们对这个问题进行数学抽象。三维空间有很多点,这些点大致的分布在一个平面上,我们要找到这个平面的法向量。
      最直接的方法就是,利用三点确定一个平面的方式,计算出所有点在平面的组合,然后计算这些平面的平均法向量, 类似于最小二乘法的过程。但是如果有上千个点的话,这个计算量就能达到100010001000。
      如果采用模拟退火算法的话就很容易了。
      我们开始随便取一个法向量方向,然后计算所有点和法平面的距离。如果我们法向量取得好的话,那么所有点到法平面的距离应该差不多。我们可以通过计算这些距离的方差来描述这个差别。
      计算一次之后,我们对当前的法向量进行一次随机的转动,然后用新的法向量再次计算这个方差。如果新的方差更小,我们就用新的法向量进行下一次计算。直到计算出比较满意的结果。

      下面就是具体的计算代码

      var getDirection = (pointsList) => {
        var noUpdateCount = 0;
        var direction = null;
        var planePoint = [0,0,0];
        // 计算这些点的中心点
        for(let pointIndex in pointsList){
          planePoint[0] += pointsList[pointIndex][0];
          planePoint[1] += pointsList[pointIndex][1];
          planePoint[2] += pointsList[pointIndex][2];
        }
        planePoint[0] = planePoint[0] / pointsList.length;
        planePoint[1] = planePoint[1] / pointsList.length;
        planePoint[2] = planePoint[2] / pointsList.length;
      
        // 计算评价函数
        var res = 0;
        var planeDirection = [Math.random(), Math.random(), Math.random()]; // 刚开始的随机方向
        planeDirectionLength = distance3d(planeDirection, [0,0,0]);
        planeDirection = [planeDirection[0] / planeDirectionLength,
          planeDirection[1] / planeDirectionLength,
          planeDirection[2] / planeDirectionLength]; // 归一化
        let scale = 0;
        let calcCount = 0
        while(noUpdateCount < 5  || scale < 1000){ // 结束条件
          scale += 1;
          // 小转动
          var rotation = new THREE.Euler(
           Math.random() * 2 * Math.PI * Math.exp(- scale / 100),
           Math.random() * 2 * Math.PI * Math.exp(- scale / 100),
           Math.random() * 2 * Math.PI * Math.exp(- scale / 100),
           'XYZ' );
          var currentVector = new THREE.Vector3(planeDirection[0], planeDirection[1], planeDirection[2]);
          var currentPlaneDirectionVector = currentVector.applyEuler(rotation);
          var currentPlaneDirection = currentPlaneDirectionVector.toArray();
          let currentRes = valueFunc(pointsList, currentPlaneDirection, planePoint); //计算评价函数
          if(res == 0 || currentRes < res){ // 找到了更好的解
            res = currentRes; // 用新解替换旧的值
            planeDirection = currentPlaneDirection;
            console.log('scale', scale);
            console.log('res', res);
            noUpdateCount = 0;
          }else{
            noUpdateCount += 1;
          }
        }
        //保证法向量朝上
        if(planeDirection[2] < 0){
          planeDirection[0] = -planeDirection[0];
          planeDirection[1] = -planeDirection[1];
          planeDirection[2] = -planeDirection[2];
        }
      
        return planeDirection;
      }
      
      function calPointToPlaneDistance(planeDirection, planePoint, targetPoint){
        return Math.abs(planeDirection[0] * (targetPoint[0] - planePoint[0]) +
        planeDirection[1] * (targetPoint[1] - planePoint[1]) +
        planeDirection[2] * (targetPoint[2] - planePoint[2]));
      }
      
      function valueFunc(pointsList, planeDirection, planePoint){
        var res = [];
        for(let pointIndex in pointsList){
          res.push(calPointToPlaneDistance(planeDirection, planePoint, pointsList[pointIndex]));
        }
        return diffSq(res);
      }
      
      function diffSq(data){
        let total = 0;
        for(let dataIndex in data){
          total += data[dataIndex];
        }
        let avg = total / data.length;
        let res = 0;
        for(let dataIndex in data){
          res += (data[dataIndex] - avg) * (data[dataIndex] - avg);
        }
        return res;
      }
      
      function distance3d(pos1, pos2){
        return Math.sqrt((pos1[0] - pos2[0]) * (pos1[0] - pos2[0]) +
         (pos1[1] - pos2[1]) * (pos1[1] - pos2[1]) +
         (pos1[2] - pos2[2]) * (pos1[2] - pos2[2]));
      }
      

      运行效率也是不错的
      0_1467442869285_Screenshot from 2016-07-02 15:01:00.png

      最后法向量的值也是比较准的。看来摄像头基本没有倾斜,摄像头坐标系基本和小车本体坐标系是重合的。

      posted in 技术交流
      weijiz
      weijiz
    • 公司的大logo
                                                                                 ./oh/
      ////////////:`                                                          `+mMMM+ 
      MMMMMMMMMMMMMNy.                                                      .yNMMMMy  
      MMMMMMMMMMMMMMMN/                                                   .sMMMMMMd`  
      MMMMMMMMMMMMMMMMM/                                                `oNMMMMMMN-   
      MMMMMMMMMMMMMMMMMm`                                              /NMMMMMMMMd    
      MMMMMMMMMMMMMMMMMM:                                            .yMMMMMMMMMM+    
      MMMMMMMMMMMMMMMMMM+                                           /mMMMMMMMMMMM-    
      MMMMMMMMMMMMMMMMMMs                                         `sMMMMMMMMMMMMM:    
      MMMMMMMNoosNMMMMMMs                                        .dMMMMMMMMMMMMMMo    
      MMMMMMMm`  +MMMMMMs    ````````  ````````` `........      +NMMNmmmmmNMMMMMMm`   
      MMMMMMMm`  /MMMMMM/    /MMMMMMN- /MMMMMMMo -MMMMMMMy    `yMMy:`     yMMMMMMM:   
      MMMMMMMm`.+mMMMMMm`    /MMMMMMN- /MMMMMMMo -MMMMMMMy    oMN:        yMMMMMMM:   
      MMMMMMMm`sMMMMMMM+     /MMMMMMN- :MMMMMMMo -MMMMMMMy    sMs         yMMMMMMN.   
      MMMMMMMm`sMMMMMMo      /MMMMMMN- :MMMMMMMo -MMMMMMMy    sM-         hMMMMMMd`   
      MMMMMMMm`sMMMMMh.      :MMMMMMN- :MMMMMMMo -MMMMMMMy    sN`         hMMMMMM:    
      MMMMMMMm`sMMMMMMN+     :MMMMMMN- :MMMMMMMo -MMMMMMMy    yN`    /mmmdNMMMMN+     
      MMMMMMMm`sMMMMMMMM/    :MMMMMMN- :MMMMMMMo -MMMMMMMy    yN`    :dmmmMMMMN:      
      MMMMMMMm`sMMMMMMMMm.   :MMMMMMN- :MMMMMMM+ -MMMMMMMy    yN`        .NMMm.       
      MMMMMMMm`sMMMMMMMMMo   -NMMMMMN- :MMMMMMM+ -MMMMMMMy    yN`        .ddd/        
      MMMMMMMm`+yymMMMMMMd   -NMMMMMN- :MMMMMMM+ -MMMMMMMy    yN`        `NMMN/       
      MMMMMMMm`   .NMMMMMm   .NMMMMMN- -MMMMMMM+ -MMMMMMMy    yN`     `` `NMMMMo      
      MMMMMMMm`   `NMMMMMN`  .NMMMMMN- -MMMMMMM+ -MMMMMMMy    yN`    hNNNNMMMMMM/     
      MMMMMMMm`.:/hMMMMMMN   .NMMMMMN- -MMMMMMM+ -MMMMMMMy    yN`    +hhhdNMMMMMm`    
      MMMMMMMm`yMMMMMMMMMm   .NMMMMMMh+dMMMMMMMdohMMMMMMMh    hN`         hMMMMMM+    
      MMMMMMMm yMMMMMMMMMh   .NMMMMMMMMMMMMMMMMMMMMMMMMMMy    hM:         hMMMMMMd    
      MMMMMMMm yMMMMMMMMM:   `dMMMMMMMMMMMMMMMMMMMMMMMMMM:    /Mh         hMMMMMMd    
      MMMMMMMm yMMMMMMMMd`    /MMMMMMMMMMMMhmMMMMMMMMMMMs      sNo`       dMMMMMMy    
      MMMMMMMm yMMMMMMMN/      /NMMMMMMMMN/ -mMMMMMMMMNo       `sMmo-----:mMMMMMM:    
      MMMMMMMm yMMMMMMN/        .odNMMMmo`   `odMMMMmo.          sMMMMMMMMMMMMMMd     
      MMMMMMMm yMMMMdo`             `.`         `.`               oNMMMMMMMMMMMMs     
      :::::::- -:::.                                               :mMMMMMMMMMMMy     
                                                                    `yMMMMMMMMMMh     
                                                                      /mMMMMMMMMM.    
                                                                       `sNMMMMMMMs    
                                                                         -hMMMMMMN.   
                                                                           /mMMMMMd`  
                                                                            `+dNMMMh` 
                                                                               `/yNMy 
                                                                                  `:s+
      
      
      
      

      小一点的logo

       ......`                             .oh/
      MMMMMMMh`                         -hMMy 
      MMMMMMMMd                       `yMMMN` 
      MMMMMMMMM-                     /NMMMMy  
      MMMMhmMMM:                   `yMMMMMMh  
      MMMN -MMM- `sss:`sss+`ssso  -mh+++mMMN` 
      MMMN-mMMm  .MMMs.MMMh.MMMm  d+    mMMM. 
      MMMN/MMN.  .MMMs.MMMh.MMMm  d`    mMMd  
      MMMN/MMMm` .MMMs.MMMh.MMMm  d` .mmMMd.  
      MMMN/MMMMy .MMMs.MMMh.MMMm  d`   `mh`   
      MMMN./hMMN `MMMs.MMMh.MMMm  d`   `MM+   
      MMMN`.yMMN `MMMs.MMMh.MMMm  d` :mmMMM/  
      MMMN/MMMMm `NMMNdMMMNdMMMm  m`    mMMd  
      MMMN/MMMMo  dMMMMMNMMMMMMo  os    mMMm  
      MMMN/MMMd`  `yNMNy`:mMMm+    smyyyNMMo  
      yyys-ys/       `     `        +NMMMMM:  
                                     -dMMMM+  
                                       +NMMm` 
                                        `yNMy 
                                           :s+
      

      长logo

                                                                                  `/y:
                                                                               `:hMd. 
                                                                             `+mMMd`  
       +yyyyys+.                                                           `+mMMMN.   
       yMMMMMMMN/                                                         :dMMMMMy    
       yMMMMMMMMN                                            `          `yMMMMMMMy    
       yMMMdsNMMM .++- .::`.::` `:+++`/++.++/`++/.oo-.oo: .shdy- :oo-  -dhs+/yMMMN    
       yMMMs-dMMh /MM+ oMM:oMM: yMMMM.dMM:MMM.MMd/MMo/MMs`dMMMMN.sMM/ /Ny`  .oMMMN    
       yMMMsmMMN. /MM+ oMM:oMM:.MMMmm.dMM:MMM.MMd/MMo/MMs:MMmdMM/sMM/ hM+``:+yMMMo    
       yMMMsmMMMy`/MM+ oMM:oMM::MMN+/ dMM:MMM.MMd/MMdsMMs/MM++MMosMM/ hM+ -oydMNo`    
       yMMMsymMMM+/MM+ oMM:oMM::MMMMM dMM:MMM.MMd/MMMhMMs/MMhsMMosMM/ hMo `..sMd.     
       yMMMs./NMMh/MMo oMM:oMM::MMmss dMM:MMM-MMd/MMdsMMs/MMMhMMosMM/ hM/ -yymMMm-    
       yMMMshNMMMy:MMm/+MMhdMM-.MMNhy.dMMdMMMhMMd/MMo/MMs/MMdsMMo+MMm/yM/ -//yMMMd    
       yMMMsmMMMN-`mMMy.NMMMMm` dMMMM.oMMMMmMMMMo/MMo/MMs/MM++MMo.NMMo`hh.  .oMMMM    
       yNNNsdNmh-  .ydo :dmmy.  `+syy.`sdds.sdho`-ss:-ss/-ss:-ss: -yh/ `ods+oyMMMm    
       `...`..`      `   ```            ``   ``                          :mMMMMMMs    
                                                                          .sMMMMMh    
                                                                            -hMMMM-   
                                                                              :hMMm.  
                                                                                -sNm- 
                                                                                  `/s/
      
      
      ██████╗ ██╗     ██╗   ██╗███████╗██╗    ██╗██╗  ██╗ █████╗ ██╗     ███████╗
      ██╔══██╗██║     ██║   ██║██╔════╝██║    ██║██║  ██║██╔══██╗██║     ██╔════╝
      ██████╔╝██║     ██║   ██║█████╗  ██║ █╗ ██║███████║███████║██║     █████╗  
      ██╔══██╗██║     ██║   ██║██╔══╝  ██║███╗██║██╔══██║██╔══██║██║     ██╔══╝  
      ██████╔╝███████╗╚██████╔╝███████╗╚███╔███╔╝██║  ██║██║  ██║███████╗███████╗
      ╚═════╝ ╚══════╝ ╚═════╝ ╚══════╝ ╚══╝╚══╝ ╚═╝  ╚═╝╚═╝  ╚═╝╚══════╝╚══════╝
      
      posted in 杂谈
      weijiz
      weijiz
    • Linux下访问Windows文件的乱码问题

      乱码问题主要分为文件名的乱码问题和文件内容的乱码问题。文件内容的乱码问题比较容易解决。只要文件编辑器有选择编码的功能(比如atom),选择正确的文件编码就可以了。一般如果在linux下打开windows的文件出现乱码,把编码方式设置成GBK或者GB18030就可以了。如果在windows下打开Linux下创建的文件发现有乱码,把编码方式设置成UTF-8就可以了。下面重点说一下文件名乱码的问题。

      文件名为什么会出现乱码?

      这个要从文件系统说起。文件保存到硬盘上,不仅文件的内容要保存上去,文件的名字,创建时间等等元数据(meta data)也要保存到硬盘上。对于文件名这样的字符串数据,只要保存就会面临着编码的问题。Windows 默认的编码方式是GB18030(这个和GBK基本通用)。而Linux的默认编码方式是UTF-8。这就是乱码产生的罪魁祸首。所以其中一个系统创建的文件,在另一个系统看来就是乱码。

      如何解决乱码问题?

      解决方式很简单,只要把两个系统的编码方式改成一样的就行了。 Windows的编码方式是没办法进行修改的。但是Linux的编码方式是可以修改的。具体的设置文件就是/etc/fstab。这个文件设置硬盘分区的挂载方式,里面就包含了编码方式。对于不同格式的文件系统设置的方式也是不一样。

      对于Fat32格式的分区设置如下

      UUID=7905-27D8 /media/randoms/WORKS vfat iocharset=utf8,codepage=950,rw,user,exec,umask=000 0 0
      

      UUID后面跟的是硬盘分区的UUID,这个可以通过sudo blkid来获取。
      第二项是硬盘的挂载点。
      再后面的参数就是具体的文件系统格式
      然后就是具体的挂载选项,比如编码方式,读写方式等等。照着上面设置就没有问题了。codepage=950是GBK的编码方式,经过测试是没有问题的。

      对于nfs格式的分区就更容易了

      UUID=00045BDB000D47A2 /media/randoms/softwares ntfs rw,user,exec,umask=000 0 0
      

      格式完全是一样的,但是nfs不用设置编码方式,因为编码方式是写在硬盘上的。可以看出来nfs也更智能一些。

      解压文件还是有乱码

      如果在windows下压缩了一个zip文件,到Linux下解压会发现文件名全是乱码。这个问题产生的原因和之前的乱码问题基本一致。因为压缩的时候采用的是GB18030编码,解压的时候是UTF-8。解决起来也非常容易。

      unzip -O GBK filename
      

      加上-O参数指定文件名的编码类型就可以了。

      posted in 技术交流
      weijiz
      weijiz
    • 庄周梦蝶

      今天无聊时在网上看到一片文章,看完觉得很有意思。文章在此。

      文章很长,但是非常值得一看。剧透是不好的,但是不知道剧情的话,我也没办法继续说我下面的话了。所以你现在有两个选择,其一是看原文章,或者听我来剧透。

      故事发生在一个魔法的幻想世界里,故事的主人公一直在被一个噩梦困扰着。这个梦令他很难受,但是找不到原因。周围的人都非常关心他,像典型的轻小说男主一样。为了寻找噩梦的原因他到了大陆中心魔法师所在的山峰上。但是魔法师也无法解释他的噩梦的原因。魔法师告诉男主,他是来自另一个世界的人,但是他却失去了过去的记忆。同时魔法师还告诉他这个世界已经出现了危机,他的梦可能和这个危机相关。然后男主就踏上了冒险的旅程。到这里都是典型的后宫式轻小说剧情。然而画风一转,转至现实世界,医生正在治疗一个昏迷的病人。这个人的各种生命指标都显示他是很健康的,然而他就是没办法醒过来。即使采用电击等刺激手段令他醒过来,他还是会很快沉睡下去。原文的描述要比我好很多,我还是贴上最后结局的一段吧。

      “一个年轻人,不知道怎么了,服用了大量的安眠药物,估计是想自杀,但是没死成,在出租屋里好几天没出现,被邻居发现异常了。咱们派人撞开门送这里抢救了,进屋的时候,电脑还开着呢……”

      “小子命挺硬啊。”坐着的人掏出一根烟,结果看到了远处的禁烟标识只能又收了回去,“那么现在救过来了嘛?”

      “大夫什么都用了,电击苏醒,呼吸机什么都上了,但是那小子就是不醒……其实大夫说了,按照一般人早就没事了,但是这家伙很奇怪,他就是醒不过来。大夫正在考虑他是不是脑死亡了,但是检查似乎又没什么。”

      “这还的确很奇怪。”

      “不过现在看,这小子的各项状态不太好,似乎……要不行了……”

      “………………”

      “他干什么的?”

      “北漂族,应该是搞IT的吧,都说这一行猝死的多,没想到想不开的也多……看他屋里还有好几个屏幕,好几个电脑,想着估计也不算太拮据吧,不知道怎么就想不开了。”

      “…………”坐着的人陷入了沉思,“能联系到他家属吗?”

      “联系过了,正在赶来……”

      “好吧……”

      …………………………

      “我觉得,你似乎应该可以阻止这个世界的崩塌吧。”斯莱尔对着维森特的背影说,“我觉得你好像对这个世界有着不可思议的掌控力。”

      “你怎么知道的?“

      ”我是魔法师嘛,每天都在观察你。“斯莱尔淡淡的一笑。

      ”这个世界挺好的。“维森特半侧脸,斯莱尔可以看到他的眼角有着晶莹的闪光,”我有温柔的琳克斯,可爱的艾维斯,还有艾丝波依,还有亚当葵,还有皮特,还有……aki“

      ”就是因为我喜欢这里,所以我没有离开……“

      维森特意味深长的扭头看着斯莱尔。

      “我留在这里的理由,你应该也会明白吧……因为你……也是我。”

      斯莱尔笑了,维森特也笑了。

      远方的村落依然在崩塌,清晨的风吹落了树上的一片叶子,落到了墓园的一个无名碑上……

      原来幻想的魔法世界只是主人公的一个幻想,一个梦。本来看到前半部分的时候还想吐槽这设定全是套路。看到最后也就明白为什么前面会这么套路。不得不感叹作者的构思真是巧妙。

      然后我才意识到,整个故事的框架就是庄周梦蝶啊。你说到底这两个世界哪个才是梦呢?你可以说是这个世界的宅男做了一个梦,也可以说幻想世界的主人公被噩梦困扰。你能否定其中任何一个世界的真实性吗?
      从一般观点来看是现实世界这里的才是真实,而幻想世界是虚伪的。但是为什么呢?为什么这边的世界就是真实的呢?
      这不是很明显吗?如果我在这个世界把那人杀了,所谓的幻想世界就不存在了。这不是说明那个世界是依附于这个世界吗?
      但是存在的定义能由关系来给出吗?如果B依附于A那么A就是更根本的存在,B的存在性就会被抹杀吗?我们所观察到的世界由各种基本粒子构成。没有了基本粒子那么世界也就是不存在的。那么基本粒子层次的世界才是真实的,而我们观察到的都是虚伪的吗?比如你看到一个苹果,摸到一个苹果。但是这些实际上都不是真实的存在。这只是粒子间的相互作用让你产生此处有个苹果的感觉。那么到底什么才是存在呢?
      我是觉得如果认同我们这个世界是存在的话,那么就认同这样的结论。
      所有被感受到的,都是存在的。
      梦中的世界和现实的世界是同样等级的存在。只是只有在做梦的那一时刻,梦中的世界才能被你感受到。就好像连WIFI一样。如果没有手机,你就不知到这里有没有WIFI,有了手机就能看得到。但是WIFI到底有还是没有和你有没有手机完全没有关系。同样也可以说梦中的世界和你做不做梦也是没关系的。只是在你做梦时才能感受到而已。那么似乎又可以得到这样的结论,未被感受到的也是存在。额那到底什么才是存在呢?我这算是唯物呢还是唯心呢?

      posted in 杂谈
      weijiz
      weijiz
    • 小强的远程协助功能

      为了为您提供更好的服务,在2016年6月之后发售的小强都默认安装了远程协助软件。通过这个软件我们的技术人员能够直接连接到您的小强,为您解决技术问题。当然您也可以通过远程协助去更加方便的远程遥控自己的小强。下面介绍一下小强的远程协助功能的具体使用方法。

      确认远程协助是否已经启动

      在终端执行htop

      0_1467270333278_Screenshot from 2016-06-30 15:05:04.png

      如果能够看到叫做SharpLink的线程正在执行说明远程协助已经开始执行了。
      还可以通过更简单的方法确认程序的状态,执行一下指令

      sudo service sharplink status
      

      如果终端显示

      0_1537859628720_43283a44-17d6-42ca-874f-db1e421bc1e9-image.png

      则说明程序已经正常执行了。

      远程连接小强

      查看自己的ID

      每台小强都有一个自己的ID,请不要轻易的告诉别人。因为如果你没有更改默认密码,别人又知道你的ID的话,那么他就可以轻易的对你的小强进行操作。
      查看ID的方法也非常简单,在终端执行

      sudo service sharplink restart
      bwgetid
      

      此时终端的输出为

      D70101972AB9B7E674290A25485B3752EDA236F65EF2C4AC7E738390DA61903565E68B8C431B
      

      这个很长的字符串就是您的ID。
      如果您想要我们提供远程协助,只需要把这个ID告诉我们的工作人员就可以了。

      远程连接自己的小强
      在记住自己的ID之后,只要您的小强连接上互联网,您就可以在任意地方随时的控制它,不会受到路由器,局域网的限制。

      首先在您的电脑上安装SharpLink,这个软件是跨平台的,无论是Linux还是Windows都可以安装。安装的具体方法在项目的介绍里面。

      安装完成之后,在终端执行

      ./SharpLink.exe 9999 你的ID 127.0.0.1 22
      

      这个指令会把本地9999端口和小强的22端口映射起来。只要连接本地的9999端口你就可以和小强的22端口相连了。当然你也可以把9999换成自己喜欢的端口。

      此时在本地电脑上执行

      ssh -p 9990 xiaoqiang@127.0.0.1
      

      等待连接完成就可以控制小强了。

      打开和关闭远程协助

      如果你想要关闭远程协助也是非常简单的。在终端删除对应的服务文件就可以了。

      sudo systemctl disable sharplink
      

      如果你想重新打开远程协助服务

      sudo systemctl enable sharplink
      

      Enjoy it!

      posted in 产品服务
      weijiz
      weijiz
    • 小强ROS机器人教程(17)___利用ORB_SLAM2建立环境三维模型

      小强主页

      想要实现视觉导航,空间的三维模型是必须的。ORB_SLAM2就是一个非常有效的建立空间模型的算法。这个算法基于ORB特征点的识别,具有准确度高,运行效率高的特点。我们在原有算法的基础上进行了修改,增加了地图的保存和载入功能,使其更加适用于实际的应用场景。下面就介绍一下具体的使用方法。

      注意:以下方法目前程序版本已经不再支持,小强用户已经完全替换为Galileo导航系统。导航系统文档 https://doc.bwbot.org/en/books-online/galileo-servicebot-doc/

      注意:由于小强的ORB_SLAM2版本已经升级到伽利略版本,所以运行时会检测是否有有效的证书。默认发货时小强是配置好证书的。如果没有证书也可以联系客服免费获取。如果你不是小强用户,那么下面的教程无法执行。

      准备工作

      在启动ORB_SLAM2之前,请先确认小强的摄像头工作正常。
      ORB_SLAM2建图过程中需要移动小车,移动小车过程中ORB_SLAM2的运行状态不方便显示(ssh方式比较卡顿,也不可能拖着显示器),因此请先安装好小强图传遥控windows客户端。

      启动ORB_SLAM2

      ssh方式进入小强,执行以下指令

      roslaunch orb_slam2 map.launch
      

      0_1489829013495_5.png

      开始建立环境三维模型

      打开小强图传遥控windows客户端,点击“未连接“按钮连接小强。在图传窗口右键打开”原始图像“和”ORB_SLAM2的特征点图像“

      0_1489829229351_1.png
      0_1489829544143_0.png
      上图中,左侧图像是摄像头原始彩色图像,右侧是ORB_SLAM2处理后的黑白图像。当前ORB_SLAM2还没有初始化成功,所以黑白图像没有特征点。
      按住”w”键开始遥控小强往前缓慢移动,使ORB_SLAM2初始化成功,即黑白图像开始出现红绿色特征点
      0_1489829731061_2.png
      现在就可以开始遥控小强对周围环境建图,遥控过程中需要保证黑白图像一直存在红绿色特征点,不存在则说明视觉lost了,需要遥控小强退回到上次没lost的地方找回视觉位置。

      使用rviz查看建图效果

      在机器人上打开rviz

      rviz
      

      打开/home/xiaoqiang/Documents/ros/src/ORB_SLAM/Data/rivz.rviz配置文件

      有些系统可能无法如此打开,可以先把文件复制到本地,然后再在本地打开

      0_1489831624309_6.png
      如下图所示,红黑色点是建立的三维模型(稀疏特征点云),蓝色方框是keyframe可以表示小强轨迹
      0_1489831763020_7.png

      保存地图

      当地图建立的范围满足自己要求后,在虚拟机新开一个命令窗口,输入下列命令保存地图

      rosrun orb_slam2 save_map.py map1
      

      0_1489832316715_8.png

      地图文件会被保存进用户主目录的slamdb文件夹内。

      地图的载入

      保存地图之后可以再次载入ORB_SLAM2程序中。地图载入后程序可以非常迅速的定位出摄像头的具体位置。这样就可以开发出基于视觉定位的导航算法了。
      载入地图的方式也非常简单。

      运行

      rosparam set /galileo/galileo_map map1
      roslaunch orb_slam2 run.launch
      

      此处的run.launch实际上就是把LoadMap 设置成1.

      地图的后期处理

      在建立地图之后,想要使用这个地图进行导航,还需要对地图文件做进一步的操作。比如创建导航的线路,标记导航路径的行走方式等等。

      对于小强用户可以直接使用导航客户端进行地图处理。详情可参见 导航系统文档

      常见问题

      Q: 可不可以不使用IMU
      A: 小强的ORB_SLAM2是经过修改的版本,增加了IMU的融合。如果不想使用IMU可以在setting.yaml里面设置UseImu为0.

      Q: 小强的标定文件在什么地方
      A: 小强的标定文件,2018.6之后发货的,位于usb_cam包launch文件夹下ov2610.yaml。之前发货的位于ORB_SLAM2包内的setting.yaml,setting4.yaml 和setting5.yaml分别是map.launch和run.launch使用的。

      Q: 小强的摄像头很不清晰
      A: 可能是摄像头的镜头被碰到了导致没有聚焦。可以转动摄像头镜头,调整到图像清晰的位置。注意调整过之后摄像头参数需要重新标定。标定方法可以参见如何标定单目摄像头

      小强主页
      返回目录

      posted in 产品服务
      weijiz
      weijiz
    • 视觉导航路径编辑器使用教程

      利用小强可以建立出周围环境的三维地图,但是如何利用这个地图实现视觉循迹呢?视觉导航路径编辑器就是为了实现这个功能而编写的。通过这个软件你可以在三维空间中标记你想要小强运动的轨迹。然后将生成的轨迹文件导出给小强,小强就能按照你标定的轨迹进行运动了。下面就详细介绍一下软件的使用方法。

      安装

      软件提供了Ubuntu 的deb安装包
      64位deb包下载
      源代码在这里

      下载完成后执行一下指令安装
      注意这个路径编辑器已经和新版本的导航程序不兼容了,对于新版本的导航程序请使用Windows客户端(目前仅对巡检版开放)

      sudo dpkg -i path-drawer_1.0.0_amd64.deb
      

      等待安装完成即可

      采集空间数据

      路径编辑器需要载入小强采集的空间数据才能够进行操作,详细的操作可以参考这一篇。
      点击保存按钮之后,地图信息会被保存到 /home/xiaoqiang/slamdb文件夹内。

      启动软件

      安装完成后可以在Ubuntu的Dash菜单中找到名为Path Drawer的程序,点击启动即可。
      0_1467106335113_Screenshot from 2016-06-28 17:32:06.png

      导入数据

      启动后的软件界面如图所示
      0_1467106412161_Screenshot from 2016-06-28 17:33:21.png
      在左上角的菜单中选择文件->导入地图数据。在弹出的文件选择对话框中选择 /home/xiaoqiang/slamdb/mappoints.bson文件。
      成功导入后就能在软件中看到地图点的数据。这是从上向下的俯视图。
      0_1467106586049_Screenshot from 2016-06-28 17:36:19.png
      然后继续在左上角的菜单中选择文件->导入路径文件。在弹出的文件选择对话框中选择/home/xiaoqiang/slamdb/keyframes.bson文件。
      成功导入后能在软件中看到之前小车行走的路径。
      0_1467106782827_Screenshot from 2016-06-28 17:39:33.png

      绘制导航路线

      导航路线就是你想要小强行走的路径。当数据导出到小强后,小强就会按照你画的路径进行移动。下面介绍一下路径绘图工具的使用方法。

      1. 基本操作
        基本操作包括平移和缩放。如果鼠标左键拖动地图可以实现地图的平移。鼠标滚轴前后滚动可以实现地图的缩放,这在绘制路径的过程中非常的有用。对于对运动要求比较细致的地方可以放大后进行绘图。
      2. 直线工具
        点击左侧工具栏里的铅笔一样的图标。这就是直线工具。鼠标左键点击图上任意一点,然后移动鼠标就会出现一条红色直线。移动鼠标到想要的终止位置,再次点击鼠标左键,一条直线就绘制完成了。在点击一次左键之后,点击右键就可以取消此次绘图。
        0_1467107213569_Screenshot from 2016-06-28 17:46:45.png
      3. 橡皮擦工具
        点击左侧工具栏中的橡皮擦工具,然后按下鼠标左键进行拖动就可以擦除之前绘制的点。
        0_1467107312249_Screenshot from 2016-06-28 17:48:23.png
      4. 曲线工具
        点击左侧曲线工具,在曲线的起始点点击鼠标左键,然后在曲线的中间的再次点击一次鼠标左键,最后在曲线的结束点点击鼠标左键。这样一条曲线就绘制完成了。
        0_1467107546476_Screenshot from 2016-06-28 17:52:16.png
      5. 删除工具
        如果想要大范围的删除之前绘制的点,那么就可以利用这个删除工具。点击左侧的删除工具然后鼠标左键点击删除的起始点,可以看到在鼠标的移动过程中有一个矩形一直在跟随。再次点击鼠标左键就可以删除矩形选中的范围。右键可以取消选择。

      利用这几个工具就可以绘制出小强的导航路径了。注意要尽量沿着原有的轨迹进行来画线,这样可以保证在运动过程中路线是畅通的。从绿色的地图点可以大致看出地形,根据这些信息画出运动所范围允许的点。

      设置导航关键点

      对于比较复杂的图形可能运动的方式有很多种。比如一个8字形路径,小强可能先绕其中的一个圆运动,然后再绕另一个圆运动,也可以两个圆交叉的运动。所以很有必要指明小强运动的具体方式。
      下面以一个圆形轨迹为例。在圆形轨迹中,小强可以顺时针运动,同时也可能是逆时针运动。
      0_1467108435355_Screenshot from 2016-06-28 18:05:04.png

      点击左侧工具栏最下面的导航点设置按钮。然后开始标记关键点。随意点击导航路线上的一个点,可以看到,在这个点上出现了一个0. 这就表明0号点已经被添加到此处。
      0_1467108613778_Screenshot from 2016-06-28 18:08:53.png
      如果想要小强逆时针运动,就可以在右侧标记一个点。就这样依次把关键点加上
      0_1467108733738_Screenshot from 2016-06-28 18:12:07.png

      点击鼠标右键可以删除最近添加的一个导航点。同样也可以利用橡皮擦和删除工具来删除导航点。小强会按照关键点标记的顺序进行运动。

      导出数据

      1. 导出导航路径文件
        当导航路径绘制完成之后,在左上角的菜单中点击文件->导出导航路径文件,选择文件保存的位置即可。在导出文件后还可以从菜单中导入,进行二次编辑。
      2. 导出导航关键点文件
        当导航关键点绘制完成之后,在左上角的菜单中点击文件->保存导航点,选择文件的保存位置即可。 在导出文件后同样也可以再次从菜单中导入,进行二次编辑。注意:只能在导航路径文件导入成功之后才能导入导航关键点。

      导出的数据放入小强的对应文件夹内就可以开始视觉导航了。

      Enjoy it!

      posted in 产品服务
      weijiz
      weijiz
    • 用webgl来绘制二维点云吧

      做图形化程序web是非常方便的。最近做了一个项目就是用webgl来绘制二维点云,运行效果还是不错的。下面简单介绍一下webgl的使用方法和二维点云的绘制方法。

      首先什么是webgl?opengl大家一定都知道,就是 open graphic library. webgl 就相当于web中的opengl实现。利用webgl就可以充分利用显卡的性能绘制出很好的图形效果。

      webgl的基本概念
      三维图形在opengl中都是分割成三角形进行渲染的。比如一个正方形可以分成上下两个三角形。

      0_1467023401352_Screenshot from 2016-06-27 18:29:44.png

      这样一个正方形就有六个定点去确定下来。要利用webgl取画图就要指定两点就可以了,一个是所绘图形的所有定点,另一个是每个三角形的贴图。在webgl中这个是用 script type="x-shader/x-fragment"去指定的。
      下面就是画一个正方形的顶点计算script

      <script id="2d-vertex-shader" type="x-shader/x-vertex">
      attribute vec2 a_position;
      
      uniform vec2 u_resolution;
      uniform vec2 u_translation;
      uniform vec2 u_rotation;
      uniform vec2 u_scale;
      
      void main() {
        // Scale the positon
        vec2 scaledPosition = a_position * u_scale;
      
        // Rotate the position
        vec2 rotatedPosition = vec2(
           scaledPosition.x * u_rotation.y + scaledPosition.y * u_rotation.x,
           scaledPosition.y * u_rotation.y - scaledPosition.x * u_rotation.x);
      
        // Add in the translation.
        vec2 position = rotatedPosition + u_translation;
      
        // convert the position from pixels to 0.0 to 1.0
        vec2 zeroToOne = position / u_resolution;
      
        // convert from 0->1 to 0->2
        vec2 zeroToTwo = zeroToOne * 2.0;
      
        // convert from 0->2 to -1->+1 (clipspace)
        vec2 clipSpace = zeroToTwo - 1.0;
      
        gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
      }
      </script>
      

      首先定义了几个变量,然后根据转动,平移等坐标变换去计算应该绘制的坐标。gl_Position就是最后计算得出的坐标。整个计算过程由GPU去完成,因此运行效率也是很高的。

      定义贴图的script如下

      <script id="2d-fragment-shader" type="x-shader/x-fragment">
      precision mediump float;
      
      uniform vec4 u_color;
      
      void main() {
         gl_FragColor = u_color;
      }
      </script>
      

      这个脚本只是设置了填充的颜色,当然也可以填充图片之类的。

      变量的计算方式已经有了,怎么给这些变量设值呢?opengl提供了一些方法

        gl = canvas.getContext("webgl");
        if (!gl) {
          return;
        }
      
        // setup GLSL program
        var program = glUtils.createProgramFromScripts(gl, ["2d-vertex-shader", "2d-fragment-shader"]);
        gl.useProgram(program);
      
        // lookup uniforms
        resolutionLocation = gl.getUniformLocation(program, "u_resolution"); // 获取GPU中变量的地址
        colorLocation = gl.getUniformLocation(program, "u_color");
        translationLocation = gl.getUniformLocation(program, "u_translation");
        rotationLocation = gl.getUniformLocation(program, "u_rotation");
        scaleLocation = gl.getUniformLocation(program, "u_scale");
      
        var positionLocation = gl.getAttribLocation(program, "a_position");
        translation = [0, 0];
        rotation = [0, 1];
        scale = [1, 1];
        // set the resolution
        gl.uniform2f(resolutionLocation, canvas.width, canvas.height);  // 给GPU中的变量赋值
        // Set the translation.
        gl.uniform2fv(translationLocation, translation);
        // Set the rotation.
        gl.uniform2fv(rotationLocation, rotation);
        // Set the scale.
        gl.uniform2fv(scaleLocation, scale);
      

      执行 uniform2f 之后就会更改GPU内对应数据的值。

      通过

      gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
           x1, y1,
           x2, y1,
           x1, y2,
           x1, y2,
           x2, y1,
           x2, y2]), gl.STATIC_DRAW);
      

      去设置所绘图形的端点坐标,这里是绘制正方形,所以是六个顶点。

          gl.drawArrays(gl.TRIANGLES, 0, 6);
      

      执行上面这句就可以开始绘图了,这里的参数指明了采用三角形进行绘图,共绘制六个顶点。这样我们就能看到一个正方形了
      完整的代码执行例子, 不涉及到坐标的计算
      完整的有坐标计算和拉伸的例子
      推荐一个学习webgl的好网站

      对于我们所要绘制的点云,只有画正方形是不够的。还有图像的放大,缩小,平移等等。这些就可以通过改变计算顶点坐标时的参数来实现。具体算起来会比较复杂,但是原理上就是如此。每个点就是一个非常小的正方形。

      来一个软件的最终效果图吧。

      0_1467025052651_Screenshot from 2016-06-27 18:57:02.png

      放大后的点云图
      0_1467025084561_Screenshot from 2016-06-27 18:57:17.png

      posted in 技术交流
      weijiz
      weijiz
    • 杂谈1

      昨天和几个同学吃饭时谈到了宗教的问题。当时隐约感觉现在大众关于宗教的观点存在逻辑上的问题,但是一时没有想清楚。今早突然意识到问题在什么地方了。我觉的有必要说明一下。

      首先说一般大众是什么观点。那就是“你可以信教,但是你不能影响我的生活”。比如有的伊斯兰信徒不仅自己不吃猪肉,而且会强迫周围的人也不吃猪肉。乍一看这是很有道理,但是这里实际上存在一个逻辑上自相矛盾的地方。首先你的这个“只要不影响到别人就是可以接受的”的观点对方可能就是不接受的。当你想让对方接受这点的时候,实际上也是在强迫别人改变他们的生活方式。这就违背了不影响别人的基本原则。

      而且我们日常所做的事情又与他们有什么区别呢?在实际生活中强迫别人改变自己的生活方式也是很常见的。比如公交车上不让座就会被鄙视,但是不让座这件事对你的生活又没有直接影响。这不是违背此原则么?玉林的狗肉事件也是如此。爱狗人士的行为也是违背此原则的。当然现在似乎主流的思想认为是爱狗人士多管闲事。但是如果有一群人非常残忍的杀狗,并且还不是以食用为目的,只是因为他们觉得杀狗好玩。你还能忍吗?然而当你采取行动决定惩罚对方的时候,是不是又是在干涉别人的生活呢?

      所以人类社会就是人和人之间相互影响的一个系统。所谓“只要不影响到别人就是可以接受的”这个想法本身就是错误的,是没有人能做得到的。如果是这样的话。不让你吃猪肉这件事似乎又是可以理解的。也许在他们看来这个行为和残忍虐狗是一个级别的。

      我越来越觉得宗教问题不单纯是信不信神的问题。这是人对世界基本的认识问题,认识不一样就会导致对同样问题的理解完全不同。即使同是人类但思想的差别甚至比人和狗之间都要大。现在世界范围内宗教的冲突都在增加。想要和平共处下去,应该是双方多多交流。能够相互理解才是解决问题的关键,而不是我无法接收你的行为,那么我就不和你接触。孤立的行为只能双方思想上的差异越来越大,问题越来越严重。

      写到这里的时候,我发现似乎大众的观点并没有逻辑问题。“只要不影响到别人就是可以接受的”其逆否命题是“不可以接受的行为是影响到别人生活的行为”。因为你不让我吃猪肉影响到了我的生活,所以这种行为是不能接受的。但是你吃猪肉的行为同时也影响到了对方的生活(也许这个行为在他们看起来就像吃屎一样,让人觉得恶心),他们不让你吃也是合理的。我只能说这个问题太复杂,很难解决。

      posted in 杂谈
      weijiz
      weijiz
    • 使用 JavaScript、Node.js 和 Electron 创造专属于你的声效器

      作者:kmokidd
      链接:https://zhuanlan.zhihu.com/p/20225295
      来源:知乎
      著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

      使用 JavaScript、Node.js 和 Electron 创造专属于你的声效器

      JavaScript 桌面应用是什么

      即使在移动端和云端大行其道而,桌面端日渐落末的现在,我的心中仍然为桌面应用留有一个特殊的位置。和 Web 应用比起来桌面应用的优点还是很多的:只要把它们放在开始菜单栏或者 dock 上,你就能随时打开它们;还可以通过 alt-tab 或者 cmd-tab 切换应用;和操作系统之间的交互更良好(快捷键,通知栏等)。

      我将会在这篇文章中指导构建一个简单的桌面应用。当然,你也将了解到在使用 JavaScript 构建桌面应用的时候要哪些重要的概念。

      使用 JavaScript 开发桌面应用意味着在打包(package application)的时候你会需要根据操作系统的不同发出不同的命令。这一行为是将原生桌面应用兼容不同平台的概念抽象出来,方便维护应用。现在,我们可以借助 Electron 或者 NW.js 开发一个桌面应用。其实这两者提供的或多或少差不多的特性,但对于我来说,还是更偏向于 Electron。在做出选择之前,先详细了解它们并考虑各种情况,就不会选错的。

      基本假设

      开始教程之前,请允许我假设你已经有了一个常用的的编辑器(或者 IDE),系统中也安装了 Node.js 和 npm,并有基础的 HTML/CSS/JavaScript (对 Node.js 的 CommonJS 模块概念有所了解是最好,但不强求) 知识。如果以上知识你并不了解,为了防止这篇文章看到你头昏脑胀,推荐你先看看之前我写过的博文,补充一下基础知识。

      万事俱备,现在就把精力集中在学习 Electron 上,不要再担心界面的事情(将会构建的界面本质上就是普通的 Web 页面而已)。

      Electron 概览

      简而言之,Electron 提供了一个实时构建桌面应用的纯 JavaScript 环境。Electron 可以获取到你定义在 package.json 中 main 文件内容,然后执行它。通过这个文件(通常我们称之为 main.js),可以创建一个应用窗口,这个应用窗口包含一个渲染好的 web 界面,还可以和系统原生的 GUI 交互。

      具体来说,就是当你启动了一个 Electron 应用,就有一个主进程(main process )被创建了。这条进程将负责创建出应用的 GUI(也就是应用的窗口),并处理用户与这个 GUI 之间的交互。

      但直接启动 main.js 是无法显示应用窗口的,在 main.js 中通过调用BrowserWindow模块才能将使用应用窗口。然后每个浏览器窗口将执行它们各自的渲染器进程( renderer process )。渲染器进程将会处理一个真正的 web 页面(HTML + CSS + JavaScript),将页面渲染到窗口中。鉴于 Electron 使用的是基于 Chrominum 的浏览器内核,你就不太需要考虑兼容的问题。

      举个例子,如果你只想做一个计算器,那你的 main process 只会做一件事情:实例化一个窗口,并内置了一个计算器的界面(这个界面是你用 HTML、CSS 和 JavaScript 写的)。

      虽然理论上只有 main process 才能和原生 GUI 产生交互,但其实我们可以通过一些手段让 renderer process 与原生 GUI 交互(在后文中你将学习到如何实现)。

      main process 可以通过 Electron 中的一些模块直接和原生 GUI 交互。你的桌面应用可以使用任意的 Node 模块,比如用 node-notifier 显示系统通知,用 request 发出 HTTP 请求……

      Hello, world!

      做好前期准备,现在让我们从 Hello World 开始吧!

      使用的 repo

      这篇教程是基于一个声效器教程的 github 仓库,请使用下面的命令将它克隆到本地:

      git clone https://github.com/bojzi/sound-machine-electron-guide.git
      然后查看一下,你可以看看这个仓库中有哪些 tag:

      git checkout
      我们将跟随这些 tag 将声效器一步步构建出来:

      git checkout 00-blank-repository
      拉取(checkout)目标 tag 之后,执行:

      npm install
      这么做能保证项目所依赖的 Node 模块都会被拉取。

      如果你无法切换到某一个 tag,最简单的解决方式就是重置仓库,然后再 checkout:

      git add -A
      git reset --hard

      开工

      先把 tag 为 ‘00-blank-repository’ 拉取下拉:

      git checkout 00-blank-repository
      在项目文件夹中创建一个 package.json 文件,并在文件中加入以下内容:

      {
      “name”: “sound_machine”,
      “version”: “0.1.0”,
      “main”: “./main.js”,
      “scripts”: {
      “start”: “electron .”
      }
      }
      这个 package.json 的作用是:

      确定应用的名字和版本号,
      告诉 Electron main.js 是 main process 的入口,
      定义启动口令 - 在 CLI (终端或者命令行)中执行 npm start 即可完成依赖安装。
      现在快把 Electron 安装上吧。最简单的安装方式应该是通过 npm 安装预构建好的二进制文件,然后把它作为开发依赖(development dependency)写入 package.json 中(安装时带上 --save-dev 参数即可自动写入依赖)。在 CLI 中进入项目目录,执行下面的命令:

      npm install --save-dev electron-prebuilt
      预构建的二进制文件会根据操作系统不同而不同的,通过执行 npm start 安装。我们以开发依赖的方式使用它,是因为在项目构建中只有在开发阶段才会使用到 Electron。

      以上,就是本次 Electron 教程所需要的全部东西了。

      对世界说 Hi

      创建一个 app 文件夹,在文件夹中新建 index.html 文件,并写入以下内容:

      Hello, world!

      在项目的根目录创建 main.js 文件。Electron 主线程的入口是这个 JS 文件,然后 “Hello world!” 页面也通过它显示出来:

      ‘use strict’;

      var app = require(‘app’);
      var BrowserWindow = require(‘browser-window’);

      var mainWindow = null;

      app.on(‘ready’, function() {
      mainWindow = new BrowserWindow({
      height: 600,
      width: 800
      });

      mainWindow.loadUrl('file://' + __dirname + '/app/index.html');
      

      });
      看起来并不难吧?

      app 模块控制着应用的生命周期(比如,当应用进入准备状态(ready status)的时候要采取什么行动)。

      BrowserWindow 模块控制窗口的创建。

      mainWindow 对象就是你的应用窗口的主界面,当 JavaScript 垃圾回收机制被触发时窗口就会被关闭,此时该对象的值是null。

      当 app 获取到 ready 事件后,我们通过 BrowserWindow 创建一个 800x600 窗口。

      这个 window 的渲染器线程将会渲染 index.html 文件。

      执行下面这行代码,看看我们的应用是什么样的:

      npm start
      现在沐浴在这个 app 的圣光中吧。

      开发一个真正的应用

      华丽丽的声效器

      开始之前,我要问个问题:什么是声效器?

      声效器是一个小设备,当你按下不同按键的时候,它会发出不同声音,比如卡通音或者效果音。在办公室里听到这样有趣的声音,好像整个人都明亮起来了呢。用这个例子作为探索如何使用 Electron 是个很棒的主意。

      具体来说,我们将会实现以下功能,并涉及到以下知识:

      声效器的基础(实例化浏览器窗口),
      关闭声效器(主进程和渲染器进程之间的通信),
      随时播放声音(全局快捷键),
      创建一个快捷修饰键(修饰键,modifier keys, 指的是 Shift、Ctrl 和 Alt 键)设置页面(并将用户设置保存在主目录下),
      添加一个托盘图标(创建原生 GUI 元素、了解菜单和托盘图标的使用),
      将应用打包到 Mac、Windows 和 Linux 平台。
      实现声效器的基本功能

      开始构建以及应用的结构

      在开发过 “Hello World” 应用之后,现在可以着手制做我们的声效器了。

      一个典型的声效器会有很多的按钮,你需要按下那些按钮才能让机器发声,通常会是拟声词(比如笑声、掌声、打碎玻璃的声音等等)。

      响应点击 – 这是我们要做的第一件事情。

      我们的应用结构非常简单直白。

      在应用的根目录中,要有一个 package.json、main.js 和其他全局所需的应用文件。

      app/ 目录中要包含 HTML 文件、CSS 目录、JS 目录、wav 目录还有图片目录。

      出于简化这个教程的目的,所有和网页设计相关的文件都已经在一开始就放在仓库中了。请在命令行中输入git checkout 01-start-project 获取。现在,请你可以输入以下命令,重置你的仓库并拉取新的 tag:

      If you followed along with the “Hello, world!” example:
      git add -A
      git reset --hard
      Follow along with the tag 01-start-project:
      git checkout 01-start-project
      在本教程中,我们只使用两种声效,后面再找一些别的音效和图标,修改* index.js *就将它们扩展成有16种音效的声效器。

      main process 的其他内容

      首先找到 main.js 中定义声效器外形的部分,用下面这段替换掉:

      ‘use strict’;

      var app = require(‘app’);
      var BrowserWindow = require(‘browser-window’);

      var mainWindow = null;

      app.on(‘ready’, function() {
      mainWindow = new BrowserWindow({
      frame: false,
      height: 700,
      resizable: false,
      width: 368
      });

      mainWindow.loadUrl('file://' + __dirname + '/app/index.html');
      

      });
      当窗口被定义了大小,我们也就是在自定义这个窗口,使得它不可拉伸没有框架,让它看起来就像一个真正的声效器浮在桌面上。

      现在问题来了 – 要如何移动或者关闭一个没有标题栏的窗口。

      很快我就会说到自定义窗口(和应用)的关闭动作,还会谈到如何在主进程和渲染器进程中通信。不过现在让我们先把目光聚焦到“拖拽效果”上。你可以在 app/css 目录下找到 index.css 文件:

      html,
      body {
      …
      -webkit-app-region: drag;
      …
      }
      -webkit-app-region: drag;把整个 html 都变成了一个可拖拽的对象。现在问题来了,在可拖拽的对象上你怎么点击啊?!好的,可能你会想到把 html 中某个部分的这个属性值设置为no-drag;,那就允许该元素不可拖拽(但可以点击了)。让我们想想下面这段 index.css 片段:

      .button-sound {
      …
      -webkit-app-region: no-drag;
      }

      展示声效器

      现在通过 main.js 文件可以创建一个新窗口,并在窗口中显示出声效器的界面。如果通过npm start启动应用,你将会看到一个有动态效果的声效器。因为我们就是从一个静态页面开始,所以现在你看到的也是不会动的页面:

      将下面这段代码保存到 index.js 文件中(位置在 app/js 目录下),运行后应用后,你会发现可以与声效器交互了:

      ‘use strict’;

      var soundButtons = document.querySelectorAll(‘.button-sound’);

      for (var i = 0; i < soundButtons.length; i++) {
      var soundButton = soundButtons[i];
      var soundName = soundButton.attributes[‘data-sound’].value;

      prepareButton(soundButton, soundName);
      

      }

      function prepareButton(buttonEl, soundName) {
      buttonEl.querySelector(‘span’).style.backgroundImage = ‘url("img/icons/’ + soundName + ‘.png")’;

      var audio = new Audio(__dirname + '/wav/' + soundName + '.wav');
      buttonEl.addEventListener('click', function () {
          audio.currentTime = 0;
          audio.play();
      });
      

      }
      通过上面这段代码,我们:

      获取声音按钮,
      迭代访问按钮的data-sound属性值,
      给每个按钮加上背景图,
      通过 HTMLAudioElement 接口给每个按钮都添加一个点击事件,使之可以播放音频,
      通过下面这行命令运行你的应用吧:

      npm start

      通过远程事件从浏览窗口中关闭应用

      接着拉取02-basic-sound-machine的内容:

      git checkout 02-basic-sound-machine
      简单来说 - 应用窗口(渲染器进程)不应该和 GUI 发生交互(也就是不应该和“关闭窗口”有关联),Electron 的官方教程上说了:

      考虑到在网页中直接调用原生的 GUI 容易造成资源溢出,这很危险,开发者不能这么使用。如果开发者想要在网页上执行 GUI 操作,必须要通过渲染器进程和主进程的通信实现。

      Electron 为主进程和渲染器进程提供了 ipc (跨进程通信)模块,ipc 模块允许接收和发送通信频道的信息。频道由字符串表示(比如“channel-1”,“channel-2”这样),可以用于区分不同的信息接收者。传递的信息中也可以包含数据。根据接收到的信息,订阅者可以做出响应。信息传递的最大好处就是做到分离任务 – 主进程不需要知道是哪些渲染器进程发送了信息。

      这正是我们想要做的 – 将主进程(main.js)订阅到“关闭主窗口”频道中,当用户点击关闭按钮时,从渲染器进程(index.js)向该频道发送信息。

      Add the following to main.js to subscribe to a channel:
      将下面的代码实现了频道订阅,将它添加到 main.js 中:

      var ipc = require(‘ipc’);

      ipc.on(‘close-main-window’, function () {
      app.quit();
      });
      把 ipc 模块包含进来之后,从频道中订阅信息就非常简单了:过 on() 方法和频道名称,再加上一个回调函数就行了。

      要向该频道发送信息,就要把下面的代码加入 index.js 中:

      var ipc = require(‘ipc’);

      var closeEl = document.querySelector(‘.close’);
      closeEl.addEventListener(‘click’, function () {
      ipc.send(‘close-main-window’);
      });
      我们依然需要把 ipc 模块引入到文件中,给关闭按钮绑定点击事件。当点击了关闭按钮时,通过 send() 方法发送一条信息到“关闭主窗口”频道。

      不要忘记在在 index.css 中将关闭按钮设置为不可拖拽:

      .settings {
      …
      -webkit-app-region: no-drag;
      }
      就这样,我们的应用现在可以通过按钮关掉了。ipc 的通信可以通过事件和参数的传递变得很复杂,在后文中会有传递参数的例子。

      通过全局快捷键播放声音

      拉取03-closable-sound-machine:

      git checkout 03-closable-sound-machine
      声效器的地基已经打的不错。但是我们还面临着使用性的问题 – 这个应用要始终保持在桌面最前方,且可以被重复点击。

      这就是全局快捷键要介入的地方。Electron 提供了全局快捷模块(global shortcut module)允许开发者捕获组合键并做出相应的反应。在 Eelctron 中组合键被称为加速器,它以字符串的形式被记录下(比如 “Ctrl+Shift+1”)。

      因为我们想要捕获到原生的 GUI 事件(全局快捷键),并执行应用窗口事件(播放声音),我们将使用 ipc 模块从主进程发送信息到渲染器进程。

      在看代码之前,还有两件事情要我们考虑:

      全局快捷键会在 app 的 ready 事件被触发后注册(相关代码片段要被包含在 ‘ready’ 中)
      通过 ipc 模块从主进程向渲染器进程发送信息,你必须使用窗口对象的引用(类似于createdWindow.webContents.send(‘channel’))。
      记住上面的两点了吗?现在让我们来改写* main.js *吧:

      var globalShortcut = require(‘global-shortcut’);

      app.on(‘ready’, function() {
      … // 之前写过的代码

      globalShortcut.register('ctrl+shift+1', function () {
              mainWindow.webContents.send('global-shortcut', 0);
      });
      globalShortcut.register('ctrl+shift+2', function () {
          mainWindow.webContents.send('global-shortcut', 1);
      });
      

      });
      首先,要先引入 global-shortcut 模块,当应用进入ready状态之时,我们将会注册两个快捷键 – ‘Ctrl+Shift+1’ 和 ‘Ctrl+Shift+2’。这两个快捷键可以通过不同的参数向“全局快捷键”频道( “global-shortcut”channel)发送信息。通过参数匹配到到底要播放哪种声音,将下面的代码加入 index.js 中:

      ipc.on(‘global-shortcut’, function (arg) {
      var event = new MouseEvent(‘click’);
      soundButtons[arg].dispatchEvent(event);
      });
      为了保证整个架构足够简单,我们将会用 soundButtons 选择器模拟按钮的点击播放声音。当发送的信息是“1”时,我们将会获取 soundButtons[1] 元素,触发鼠标点击事件(注意:在生产环境中的应用,你需要封装好播放声音的代码,然后执行它)。

      在新窗口中通过用户设置配置 modifier keys

      下面请拉取04-global-shortcuts-bound:

      git checkout 04-global-shortcuts-bound
      通常我们会同时运行好多个应用,声效器中设置的快捷键很可能已经被占用了。所以现在要引入一个设置界面,允许用户更改修饰键(modifier keys)的原因(Ctrl、Alt 和 Shift)。

      要完成这一个功能,我们需要做下面这些事情:

      在主界面上添加设置按钮,
      实现一个设置窗口(设置页面上有对应的HTML、CSS 和 JS),
      开启和关闭设置窗口,以及更新全局快捷键的 ipc 信息,
      从用户的系统中读写存储设置信息的 JSON 文件。
      piu~ 以上就是我们要做的。

      设置按钮和设置窗口

      和关闭主窗口类似,我们将会把事件绑定到设置按钮上,(settings button),在* index.js *中加入发送给频道的信息:

      var settingsEl = document.querySelector(‘.settings’);
      settingsEl.addEventListener(‘click’, function () {
      ipc.send(‘open-settings-window’);
      });
      当点击了设置按钮,将会有一条信息向“打开设置窗口”这个频道发送。* main.js 可以响应这个事件,并打开一个新窗口,将以下代码加入 main.js *中:

      var settingsWindow = null;

      ipc.on(‘open-settings-window’, function () {
      if (settingsWindow) {
      return;
      }

      settingsWindow = new BrowserWindow({
          frame: false,
          height: 200,
          resizable: false,
          width: 200
      });
      
      settingsWindow.loadUrl('file://' + __dirname + '/app/settings.html');
      
      settingsWindow.on('closed', function () {
          settingsWindow = null;
      });
      

      });
      这一步和之前的类似,我们将会打开一个新的窗口。唯一的不同点就是,为了防止实例化两个一样的对象,我们将会检查设置窗口是否已经被打开了。

      当上述代码成功执行之后,我们需要再添加一个关闭设置窗口的动作。类似的,我们需要向频道中发送一条信息,但这次是从* settings.js 中发送(关闭按钮的事件是在 settings.js 中)。新建 settings.js *文件,并添加以下代码(如果已经有该文件,就直接在原文件中添加):

      ‘use strict’;

      var ipc = require(‘ipc’);

      var closeEl = document.querySelector(‘.close’);
      closeEl.addEventListener(‘click’, function (e) {
      ipc.send(‘close-settings-window’);
      });
      在 main.js 中监听该频道:

      ipc.on(‘close-settings-window’, function () {
      if (settingsWindow) {
      settingsWindow.close();
      }
      });
      现在,设置窗口已经可以实现我们的逻辑了。

      用户设置的读写

      执行05-settings-window-working:

      git checkout 05-settings-window-working
      设置窗口的交互过程是,存储设置信息以及刷新应用:

      创建一个 JSON 文件用于读写用户设置,
      用这个设置初始化设置窗口,
      通过用户的操作更新这个设置文档,
      通知主进程要更新设置页面。
      我们可以把实现读写设置的部分直接写进 main.js 中,但是如果把这部分独立成模块,可以随处引用这样不是更好吗?

      使用 JSON 做配置文件
      现在我们要创建一个 configuration.js 文件,再将这个文件引入到项目中。Node.js 使用了 CommonJS 作为编写模块的规范,也就是说你需要将你的 API 和这个 API 中可用的函数都要暴露出来。

      为了更简单地读写文件,我们将会使用 nconf 模块,这个模块封装了 JSON 文件的读写。但首先,我们需要将这个模块包含到项目中来:

      npm install --save nconf
      这行命令意味着 nconf 模块将会作为应用依赖被安装到项目中,当我们要发布应用的时候,这个模块会被一起打包给用户(save-dev 参数会使安装的模块只出现在开发阶段,发布应用的时候不会被包含进去)。

      在根目录下创建 configuration.js 文件,它的内容非常简单:

      ‘use strict’;

      var nconf = require(‘nconf’).file({file: getUserHome() + ‘/sound-machine-config.json’});

      function saveSettings(settingKey, settingValue) {
      nconf.set(settingKey, settingValue);
      nconf.save();
      }

      function readSettings(settingKey) {
      nconf.load();
      return nconf.get(settingKey);
      }

      function getUserHome() {
      return process.env[(process.platform == ‘win32’) ? ‘USERPROFILE’ : ‘HOME’];
      }

      module.exports = {
      saveSettings: saveSettings,
      readSettings: readSettings
      };
      我们要把文件位置和文件名传 nconf 模块(用 Node.js 的 process.env 获取到文件的位置),具体路径会根据平台而异。

      通过 nconf 模块的 set() 和 get() 方法结合文件操作的 save() 和 load(),我们可以实现设置文件的读写操作,然后通过 module.exports 将接口暴露到外部。

      初始化默认的快捷键设置
      在讲设置交互之前,为了避免用户是第一次打开这个应用,要先初始化一个设置文件。我们将会以数组的形式储存热键,对应的键是 “shortcutKeys”,储存在 main.js 中,我们需要把 configuration 模块包含到项目中:

      ‘use strict’;

      var configuration = require(‘./configuration’);

      app.on(‘ready’, function () {
      if (!configuration.readSettings(‘shortcutKeys’)) {
      configuration.saveSettings(‘shortcutKeys’, [‘ctrl’, ‘shift’]);
      }
      …
      }
      我们需要先检测键 ‘shortcutKeys’ 是否已经有对应的值了,如果没有我们需要初始化一个值。

      在 main.js 中,我们将重写全局快捷键的注册方法,在之后我们更新设置的时候,会直接调用这个方法。将原来的注册代码改成以下内容:

      app.on(‘ready’, function () {
      …
      setGlobalShortcuts();
      }

      function setGlobalShortcuts() {
      globalShortcut.unregisterAll();

      var shortcutKeysSetting = configuration.readSettings('shortcutKeys');
      var shortcutPrefix = shortcutKeysSetting.length === 0 ? '' : shortcutKeysSetting.join('+') + '+';
      
      globalShortcut.register(shortcutPrefix + '1', function () {
          mainWindow.webContents.send('global-shortcut', 0);
      });
      globalShortcut.register(shortcutPrefix + '2', function () {
          mainWindow.webContents.send('global-shortcut', 1);
      });
      

      }
      上述方法重置了全局快捷键的值,从设置中读取热键的数组,将它传入加速器兼容字符串(Accelerator-compatible)并注册新键。

      设置窗口的交互
      回到 settings.js 文件,我们需要绑定点击事件来改变我们的全局快捷键。首先,我们将会遍历复选框,记录下被勾选的选项(从 configuration 模块中读值):

      var configuration = require(‘…/configuration.js’);

      var modifierCheckboxes = document.querySelectorAll(‘.global-shortcut’);

      for (var i = 0; i < modifierCheckboxes.length; i++) {
      var shortcutKeys = configuration.readSettings(‘shortcutKeys’);
      var modifierKey = modifierCheckboxes[i].attributes[‘data-modifier-key’].value;
      modifierCheckboxes[i].checked = shortcutKeys.indexOf(modifierKey) !== -1;

      ... // Binding of clicks comes here
      

      }
      现在我们需要绑定复选框的行为。考虑到设置窗口(和它的渲染器进程)是不允许改变 GUI 绑定的。这说明我们需要从 setting.js 中发送信息(之后会处理这个信息的):

      for (var i = 0; i < modifierCheckboxes.length; i++) {
      …

      modifierCheckboxes[i].addEventListener('click', function (e) {
          bindModifierCheckboxes(e);
      });
      

      }

      function bindModifierCheckboxes(e) {
      var shortcutKeys = configuration.readSettings(‘shortcutKeys’);
      var modifierKey = e.target.attributes[‘data-modifier-key’].value;

      if (shortcutKeys.indexOf(modifierKey) !== -1) {
          var shortcutKeyIndex = shortcutKeys.indexOf(modifierKey);
          shortcutKeys.splice(shortcutKeyIndex, 1);
      }
      else {
          shortcutKeys.push(modifierKey);
      }
      
      configuration.saveSettings('shortcutKeys', shortcutKeys);
      ipc.send('set-global-shortcuts');
      

      }
      这段代码看起来比较长,但事实上它很简单。我们将会遍历所有的复选框,并绑定点击事件,在每次点击的时候检查设置数组中是否包含有热键。根据检查结果,更改数组,将结果保存到设置中,并向主进程发送信息,更新我们的全局快捷键。

      现在的工作就是在 main.js 中将 ipc 信息订阅到“设置全局快捷键”频道,并更新我们的全局快捷键:

      ipc.on(‘set-global-shortcuts’, function () {
      setGlobalShortcuts();
      });
      就这么简单,我们的全局快捷键已经可配置了!

      菜单中要放什么?

      接下来拉取 06-shortcuts-configurable:

      git checkout 06-shortcuts-configurable
      另一个在桌面应用中的重要概念就是“菜单”,比如右键菜单(点击右键出现的菜单)、托盘菜单(通常会有一个托盘 icon)和应用菜单(在 OS X 中)等等。

      在这一节中,我们将会添加一个托盘菜单。我们也将会借此机会尝试在 remote 模块中使用别的进程间的通信方式。

      remote 模块从渲染器进程到主进程完成 RPC 类型的调用。将模块引入的时候,这个模块是在主进程中被实例化的,所以它们的方法也会在主进程中被执行。实际开发中,这个行为是在远程地请求 index.js 中的原生 GUI 模块,然后又在 main.js 中调用 GUI 的方法。这么做的话,你需要在 index.js 中将 BrowserWindow 模块引入,然后实例化一个新的浏览器窗口。其实在主进程中有一个同步的调用,实际上是这个调用创建了新的浏览器窗口。

      现在让我们来看看要怎么样创建一个菜单,并在渲染器进程中将它绑定到一个托盘图标上。将下面这段代码加入 index.js 中:

      var remote = require(‘remote’);
      var Tray = remote.require(‘tray’);
      var Menu = remote.require(‘menu’);
      var path = require(‘path’);

      var trayIcon = null;

      if (process.platform === ‘darwin’) {
      trayIcon = new Tray(path.join(__dirname, ‘img/tray-iconTemplate.png’));
      }
      else {
      trayIcon = new Tray(path.join(__dirname, ‘img/tray-icon-alt.png’));
      }

      var trayMenuTemplate = [
      {
      label: ‘Sound machine’,
      enabled: false
      },
      {
      label: ‘Settings’,
      click: function () {
      ipc.send(‘open-settings-window’);
      }
      },
      {
      label: ‘Quit’,
      click: function () {
      ipc.send(‘close-main-window’);
      }
      }
      ];
      var trayMenu = Menu.buildFromTemplate(trayMenuTemplate);
      trayIcon.setContextMenu(trayMenu);
      原生的 GUI 模块(菜单和托盘)通过remote模块包含进来比较安全。

      OS X 支持图片模板(将图片文件名以 ‘Template’ 结尾,就会被定义成为图片模板),托盘图标可以通过模板来定义,这样我们的图标就会有“暗黑”和“光明”两个主题了。其他的操作系统用正常的图标就行。

      在 Electron 中有很多绑定菜单的方法。这里介绍的方法只是创建了一个菜单模板(将菜单项用数组的方式存储),然后通过这个模板创建菜单,托盘 icon 再绑定上这个菜单,就实现了我们的菜单功能。

      应用打包

      接下来拉取 07-ready-for-packaging:

      git checkout 07-ready-for-packaging
      如果你做了一个应用结果人们连下载都下载不了,怎么会有人用呢?

      通过 electron-packager 你可以将应用打包到全平台。这一步骤在 shell 中就可以完成,将应用打包好以后就能发布了。

      它可以作为一个命令行应用或者作为开发应用过程中的一步,构建一个更复杂的开发场景不是这篇文章要谈的内容,不过我们将通过 npm 脚本让应用打包更简单一点。用 electron-packager 打包的命令是这样的:

      electron-packager
      以上命令:

      将目录切换到项目所在路径,
      参数 ‘name of project’ 是你的项目名,参数 ‘plateform’ 确定了你要构建哪个平台的应用(Windows、Mac 还是 Linux),
      参数 ‘architecture’ 决定了使用 x86 还是 x64 还是两个架构都用,
      决定了使用的 Electron 版本。
      第一次打包应用需要比较久的时间,因为所有平台的二进制文件都需要下载,之后打包应用会比较快了。
      在 Mac 上我是这么做的:

      electron-packager ~/Projects/sound-machine SoundMachine --all --version=0.30.2 --out=~/Desktop --overwrite --icon=~/Projects/sound-machine/app/img/app-icon.icns
      首先你要将图标的格式转换成 .icns(在 Mac 上)或者 .ico(在 Windows 上),网络上有工具可以把 PNG 做这样的转换(确保下载的图片的扩展名是 .icns 而不是 .hqx)。如果从非 Windows 的系统上打包了 Windows 的应用,你应该需要处理一下路径(Mac 用户可以用 brew,Linux 用户可以用 apt-get)。

      每次都要执行这么长的一句命令一点都不合理。所以你可以在 package.json 中添加另一个脚本。首先,将electron-packager 作为 development dependency 安装:

      npm install --save-dev electron-packager
      然后在 package.json 中添加以下内容:

      “scripts”: {
      “start”: “electron .”,
      “package”: “electron-packager ./ SoundMachine --all --out ~/Desktop/SoundMachine --version 0.30.2 --overwrite --icon=./app/img/app-icon.icns”
      }
      接着执行:

      npm run-script package
      打包命令启动了 electron-packager,在当前目录中查看项目,在 Desktop 目录中构建。如果你使用的是 Windows,脚本内容需要一些细微的更新。

      声效器目前是 100MB 大小,不要担心,当你压缩它之后,所占空间会减半。

      如果你对此还有更大的计划,可以看看 electron-builder,它是根据 electron-packager 构建出的应用打包再做自动安装的处理。

      添加其他的特性

      现在你可以尝试开发别的功能了。

      这里有一些方案,可以启发你的灵感:

      应用的使用手册,说明了有那些快捷键和应用作者,
      在应用中给使用手册添加一个图标和菜单入口,
      构建一个打包脚本,用于快速构建和分发,
      使用* node-notifier 添加一个提示系统,告诉用户正在播放哪一个声音,
      使用
      lodash 让你的代码更加干净、具有更好的扩展性,
      在打包之前不要忘了压缩你的 CSS 和 JavaScript,
      结合上文提到的
      node-notifier *和一个服务器端的调用,通知用户是否需要更新版本……
      还有一个值得一试的东西 – 将代码中关于浏览器窗口的逻辑抽离出来,通过类似 browserify 的工具创建一个和声效器一样的网页。一份代码,两个产品(桌面端和 Web 引用)。酷毙了!

      更深入研究 Electron

      我们只是尝试了 Electron 的冰山一角,想要知道监控主机的电源情况、获取当前窗口的信息(比如光标的位置)等,Eletron 都能帮你做到。

      对于所有的内置工具(通常在开发 Electron 应用时使用),查看 Electron API 文档。

      这些文档在 Electron 的 github 仓库中都能找到。

      Sindre Sorhus 正在维护一份 Electron 资源清单,在那个上面你可以看到很多非常酷的项目,还能了解到一些系统架构做的很好的 Electron 应用,这些都能给你的开发带来灵感。

      Electron 是基于 io.js 的,大部分 Node.js 模块都可以兼容,可以使用它们扩展你的应用。去 npmjs.com 上看看有没有合适的。

      这样就够了吗?

      当然不。

      现在,可以来构建一个更大型的应用了。在这篇文章中,我几乎没有说到如何使用外部的库或者构建工具来构建一个应用,不过用 ES6 和 Typescript 的语法结合 Angular 和 React 来构建 Electron 应用也很简单,还可以用 gulp 或 grunt 构建流程。

      干嘛不用你最喜欢的语言,框架和工具,来试试构建一个 Filckr 同步工具(借助 Filckr API 和 node-filckrapi)或者一个 Gmail 客户端(使用 Google 的官方 Node.JS 客户端库?)

      选一个自己感兴趣的项目,开工吧!

      原文链接:Building a desktop application with Electron

      posted in 技术交流
      weijiz
      weijiz
    • 如何正确的处理TCP连接

      TCP是一种非常常用的socket,在做各种网络通信的时候,它也是必不可少的。但是很多情况下我们并没有正确的处理TCP连接。最终可能导致大量的僵尸连接耗尽了服务器的资源。这里就稍微详细的说明正确的TCP连接创建和关闭的方式,以及僵尸连接出现的原因。因为作为开发软件的人更关心的是如何正确的使用API,所以底层的和如何使用关系不大的东西,比如TCP三次握手,在这篇文章会尽量避免。

      以下以C#作为示例

      建立连接

      服务器开始监听

      IPAddress ip = IPAddress.Parse ("0.0.0.0");
      var serverSocket = new Socket (AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
      serverSocket.Bind (new IPEndPoint (ip, Convert.ToInt32 (9999)));
      serverSocket.Listen (1000);
      Socket clientSocket = serverSocket.Accept ();
      

      一般设置好服务器监听地址和端口,服务器就可以开始监听了。这里采用的是阻塞式的socket。程序最后会阻塞在最后一句直到有客户端连接到服务器。

      客户端建立连接

      IPAddress targetIp = IPAddress.Parse (ipstr);
      Socket mClientSocket = new Socket (AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
      mClientSocket.Connect (new IPEndPoint (targetIp, Convert.ToInt32 (9999)));
      

      客户端程序也是比较简单的,设置好服务器的地址和端口就可以连接了。这里socket的读取和写入都是阻塞的。所以一般都是要单独给每个socket连接开一个线程。

      创建连接的过程是很直观和简单的,一般也很少会犯错。但是socket的断开过程就是一个比较大的坑。

      不正确的断开连接

      如果服务器端或者客户端想要关闭连接,直接调用了Close方法。那么就会出问题。如服务器端程序执行

      mClientSocket.Close ();
      

      此时如果客户端正在调用 Receive 函数,客户端就会报出Socket被异常关闭的错误。但是客户端的这个socket实际上处在close_wait状态并没有关闭,socket也并不会被释放。如果程序这样一直执行下去最终就会耗尽系统的资源

      下面以实际的一个例子程序来演示一下

      服务器程序

      using System;
      using System.Net;
      using System.Net.Sockets;
      
      namespace testSocket
      {
      	class MainClass
      	{
      		public static void Main (string[] args)
      		{
      			
      			IPAddress ip = IPAddress.Parse ("0.0.0.0");
      			var serverSocket = new Socket (AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
      			serverSocket.Bind (new IPEndPoint (ip, Convert.ToInt32 (9999)));
      			serverSocket.Listen (1000);
      			Socket clientSocket = serverSocket.Accept ();
      			clientSocket.Close ();
      			Console.WriteLine ("Socket connected");
      			Console.ReadKey ();
      		}
      	}
      }
      
      

      客户端程序

      using System;
      using System.Net;
      using System.Net.Sockets;
      
      namespace socketClient
      {
      	class MainClass
      	{
      		public static void Main (string[] args)
      		{
      			IPAddress targetIp = IPAddress.Parse ("127.0.0.1");
      			Socket mClientSocket = new Socket (AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
      			mClientSocket.Connect (new IPEndPoint (targetIp, Convert.ToInt32 (9999)));
      			Console.WriteLine ("Socket connected");
      			Console.WriteLine ("Local Port: " + mClientSocket.LocalEndPoint);
      			Console.ReadKey ();
      		}
      	}
      }
      
      

      服务器程序在连接建立之后直接调用了Close函数。
      这时通过netstat可以看到两个连接的状态。
      在命令行执行

      netstat -nap |grep 46755
      

      其中46755是客户端显示的端口号
      显示如下图
      0_1466499938307_Screenshot from 2016-06-21 17:05:11.png
      可以看到服务器也有一个socket处在FIN_WAIT2的状态。但是这个并没有什么影响,过一段时间后系统会自动释放这个socket。
      0_1466500313131_Screenshot from 2016-06-21 17:11:38.png
      同样如果客户端直接关闭连接就会导致服务器的socket处在close_wait的状态。

      正确的断开连接

      关闭之前一定要调用Shutdown
      关闭之前一定要调用Shutdown
      关闭之前一定要调用Shutdown
      重要的事情说三遍。Shutdown的作用就是告诉socket不要在继续读写了,socket要准备关闭了。Shutdown的方式有几种,可以只关闭本地的读写,也可以只关闭远方的读写,也可以同时关闭。

      正确的服务器关闭例子

      using System;
      using System.Net;
      using System.Net.Sockets;
      
      namespace testSocket
      {
      	class MainClass
      	{
      		public static void Main (string[] args)
      		{
      			bool closeFlag = false;
      			IPAddress ip = IPAddress.Parse ("0.0.0.0");
      			var serverSocket = new Socket (AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
      			serverSocket.Bind (new IPEndPoint (ip, Convert.ToInt32 (9999)));
      			serverSocket.Listen (1000);
                              byte[] buf = new byte[1024 * 512];
      			Socket clientSocket = serverSocket.Accept ();
                              try{
                                  int size = clientSocket.Receive(buf);
                                  if(size == 0 && !closeFlag){
                                      closeFlag = true;
                                      clientSocket.Shutdown();
                                      clientSocket.Close();
                                  }
                              }catch(SocketException e){
                                  closeFlag = true;
                                  clientSocket.Shutdown();
                                  clientSocket.Close();
                              }
                              
                              if(!closeFlag){
                                  clientSocket.Shutdown (SocketShutdown.Both);
          			    clientSocket.Close ();
                              }
      			Console.WriteLine ("Socket connected");
      			Console.ReadKey ();
      		}
      	}
      }
      

      不仅在本地主动关闭的时候要调用Shutdown,对方不正常关闭的时候也要调用Shutdown。如果对方不正常的关闭连接那么就会本地的socket就会抛出异常。如果对方是正常的关闭socket那么Receive的返回值就是0,然后根据这个去关闭本地的socket。

      比较完整的C#socket客户端和服务器的例子程序可以看这里。
      服务器程序
      客户端程序

      posted in 技术交流
      weijiz
      weijiz
    • Linux 下的 C# 开发

      Linux 下的C#开发

      在2014年微软开源了C#, C#也被官方移植到了Linux平台。Microsoft 开始真正的 Love Linux 了。但是实际上早在官方移植之前,就已经有人实现了在Linux下的C#开发。而且由于已经有了很长世间的积淀,稳定性和软件库的完备性方面也要比官方的要好。这个项目就是Mono。这篇文章就是介绍Mono在Ubuntu下的具体安装和使用。

      Mono 的安装

      以下的例子都是以Ubuntu 14.04 为例, 你可能需要根据自己的系统进行调整。
      进入 Mono 官方的安装介绍页面
      按照网页中的指示,在终端中输入以下内容

      sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF
      echo "deb http://download.mono-project.com/repo/debian wheezy main" | sudo tee /etc/apt/sources.list.d/mono-xamarin.list
      sudo apt-get update
      

      等待更新完成后输入

      sudo apt-get install mono-complete
      

      这样Mono环境就安装完成了, 如果你只需要一个C#的编译和运行环境,那么到这里就足够了。
      安装IDE
      mono提供了一个非常好的IDE开发环境。

      sudo apt-get install monodevelop
      

      IDE的使用

      当IDE安装完成之后再Ubuntu的Dash里面就可以打开IDE了,如下图所示。
      从Dash打开MonoDevelop

      打开IDE后界面如下,使用方法基本和Visual Studio 是一样的。
      alt text
      需要注意的是Windows下的dll和Linux下的dll是不通用的,如果直接在Linux下打开Windows的项目需要重新添加dll的依赖。甚至有时还要重新创建项目才能使用。不过用Mono Develop创建的项目Visual Studio 是可以正常打开的。

      下面以一个HelloWorld例子来展示基本的使用方法, 在File-> New Solution 里面创建新的Console Application。如下图所示

      0_1466409873661_Screenshot from 2016-06-20 15%3A57%3A17.png

      如果想要添加dll依赖可以右键点击Refererces, 然后点击edit
      0_1466410032847_Screenshot from 2016-06-20 15%3A58%3A34.png

      在弹出的对话框里面选择对应的dll

      如果想要添加NuGet软件包可以右键点击Packages, 然后选择Add

      0_1466410114167_Screenshot from 2016-06-20 15%3A58%3A22.png

      在弹出的对话框中选择你需要的软件包。

      项目编辑完成之后按F5就可以直接执行了
      0_1466410209534_Screenshot from 2016-06-20 15%3A59%3A01.png

      项目的编译

      在安装了Mono Develop的情况下,可以直接利用IDE进行编译。但是如果没有装IDE那么就需要以下的指令进行编译。

      首先需要用NuGet自动安装项目的依赖软件包。在项目的根目录下执行以下指令。在执行前确认你已经下载了NuGet.exe文件。如果没有下载可以在这里下载。如果执行过程中出现证书错误等等,这是由于mono的版本比较低的原因。按照以上方法安装的话应该是没有这个问题的。

      mono Nuget.exe restore
      

      然后执行以下指令进行编译

      xbuild /p:Configuration=“Debug POSIX”

      其中Debug POSIX是 编译的设置。如果想要编译Release版本的就可以写

      xbuild /p:Configuration="Release"
      

      运行效率

      我没有做过具体的Benchmark,但就自己的使用而言我觉得Linux下的C#并没有明显的效率下降,CPU和内存的使用率也并不高(相同程序的比较)。个人也非常推荐用C#进行跨平台开发。

      posted in 技术交流
      weijiz
      weijiz
    • RE: 这个论坛是谁搭建的啊?

      @choury 截个图看看

      posted in 讨论区
      weijiz
      weijiz
    • RE: 公司代码托管系统使用简介

      @choury 因为github的私有源是收费的.我们也有github账户,开源的代码都放在上面 https://github.com/BlueWhaleRobot

      posted in 技术交流
      weijiz
      weijiz
    • API的基本开发原则

      在开发软件的过程中经常要自己去开发API。一般来说只要写出的程序能够满足需求就行了。但是随着开发项目的增加也逐渐发现,API必须要遵守某些特定的原则才行。否则就会产生问题隐患。

      1. 原子操作
        这个是最基本的原则,然而我最近才意识到。所有API完成的操作必须是原子操作。所谓原子操作就是整个操作不能被分成更小的操作。操作要么成功要么失败。不会出现在一半的状态。如果操作失败那么被操作对象就应该保持和操作前的状态一样。为什么要如此呢?这个原因是显而易见的。比如我们要通过一个API把A盒子放到B盒子里面,再把B盒子放到C盒子里面。如果在把A盒子放入B盒子成功之后,去放C盒子的时候发现C盒子已经满了。不太好的API的做法就是直接返回操作失败的状态,不进行还原操作。这时候就会出现A盒子在B盒子之中,但是又没放在C盒子里的状态。这种状态是程序设计之外的。假如再进行其他操作,会对整个系统产生什么影响都是无法预测的。所以原子操作是一个基本原则。
      2. RESTFUL
        这是WEB API的一种设计风格。感觉按照这种风格设计的API结构要更清晰。具体的内容可以参照网上的很多文章。
      posted in 技术交流
      weijiz
      weijiz
    • 在Github上发现了一个5神教

      今天在逛Github 的时候发现了一个5神教。看了之后笑死我了。这是一个专门给5写的一个库。里面包含了各种写给5用的函数库。

      // 加
      five() + five(); // 10
      // 减
      five() - five(); // 0
      // 乘
      five() * five();
      // 除
      five() / five();
      

      还有一些格式转化的函数

      five.upHigh() // ⁵
      five.downLow() // ₅
      five.tooSlow() // 5, with a ~500 millisecond delay
      five.roman() // V
      five.morseCode() // .....
      five.negative() // -5
      five.loud() // FIVE
      five.loud('piglatin') // IVEFAY
      five.smooth() // S
      five.mdFive() // 30056e1cab7a61d256fc8edd970d14f5
      

      看到这个就忍不住想问,这个库到底是干什么的?
      这个库就是写着玩的,实际上什么都干不了。

      看到这个项目的issue的时候,更是好笑了。

      比如说这个

      petty commented on Jul 18, 2014
      On the one hand … this is great, quite literally.

      But after searching Github, I’ve been unable to find the rest of the set. For example, what about three()? and what about constants, like pi()?

      I understand that making all numbers overly complicated could take time, so what’s the ETA?

      Joezo commented on Jul 18, 2014
      The beauty of five is that you can make any number you want.

      Want three()? Well that’s just
      (five() * five() - five() - five()) / five() or (five() + five() + five())/five()

      Want six()? five() + (five()/five())

      Want zero()? five() - five()
      🎉 1

      dufferzafar commented on Jul 18, 2014
      @Joezo You left pi 😜

      teuneboon commented on Jul 18, 2014
      @dufferzafar we can estimate pi: ( (five() * five() ) - ( ( five() + five() + five() ) / five() ) ) / ( five() + ( ( five() + five() ) / five() ) )

      shackpank commented on Jul 18, 2014
      @dufferzafar easiest way to get pi is through builtin Math object like this

      var five = require('five');
      Math[Buffer(((five() * five()) - (five() / five()) + ((five() * ((five() / five()) + (five() / five()))) * five() * five() * five()) * (five() - (five() / five())) + (five() * five())).toString(), 'hex').toString('utf8')]
      

      dufferzafar commented on Jul 18, 2014
      If this package useful? #82

      rex commented on Jun 27, 2015
      @shackpank I have tears in my eyes and my belly hurts from laughing. Far and away the best response ever made to any question ever asked in the history of GitHub.

      May five() live forever

      现在所有的数字他们都想用5来替换。他们已经找到了用5来替换pi的方式。五号神教已经正式成立了。

      *May five() live forever

      但是这个还有更好玩的玩法。现在的加减乘除运算还都是用的原来的符号。如果把整个系统改成Lamda演算的形式,这些符号都是能够用5来表示的。这样就变成一个彻底的五号神教了。

      posted in 杂谈
      weijiz
      weijiz
    • mongodb c++ 驱动的使用

      Mongodb也算是一个比较成熟的数据库了,以前用python和js很多时候都是用它作为数据库的。因为不用写schema,用起来很方便。但是没想到在c++里使用mongodb 却是一个大坑。主要是由于现在mongodb的c++驱动刚刚用C11的标准全部重写了一遍。不仅官方文档少得可怜,代码的稳定性也有问题。可能之后会好很多吧。

      首先是安装
      安装方面我就不多说了,官方的文档写得很详细。

      接着就是使用了
      数据库的基本操作,增删查改。但是在开始增删查改之前先要了解一下bson。这个是mongodb的数据表现形式。在存入数据库之前,都要先把数据变成这种格式。实际上和json很类似。
      创建一个bson对象

      auto doc = builder::basic::document{};
      

      之后就可以添加数据了

      doc.append(kvp(“name”, “John”));
      doc.append(kvp(“age”, “12”));
      

      kvp是key value pair的缩写。对于string,bool这样的类型可以直接写进去。但是有些变量必须指定数据类型才能添加, 比如

      doc.append(kvp(“size”, types::b_double{3.134}));
      

      具体有哪些可以使用的类型,可以直接看源代码。注意上面b_double{}这里是大括号。这是初始化一个结构体。并不是调用函数。所有的types里面的类型都是结构体。
      bson也可以添加比较复杂的数据类型。比如这样的一个json

      {
         “name”: “John”,
         “age”: 12,
         “friends”: [
             {
               “name”: “Jim”,
               “age”: 12,
             },
             {
               “name”: “Bob”,
               “age”: 12,
             }
          ]
      }
      

      这里面有嵌套的字典和列表。这时候就要用到sub_document 和sub_array了

      auto doc = builder::basic::document{};
      doc.append(kvp(“name”, “John”));
      doc.append(kvp(“age”, types::b_int32{12}));
      doc.append(kvp(“friends”, [&](sub_array array){
         array.append([&](sub_document sub_doc){
             sub_doc.append(kvp(“name”, “Jim”));
             sub_doc.append(kvp(“age”, types::b_int32{12}));
         }); 
         Array.append([&](sub_document sub_doc){
             sub_doc.append(kvp(“name”, “Bob”));
             sub_doc.append(kvp(“age”, types::b_int32{12}))
         });
      }));
      

      sub_document 和sub_array 是bson的内部数据类型。也就是不能从外面自己实例化一个然后去用。只能通过lamda函数去接收它传来的数据,然后更改其内容。
      数据的创建大致上就是如此。然后就是数据的访问了。给你一个bson对象如何访问各个属性呢?
      以上面创建的doc为例。

      std::string name =  stdx::string_view(doc.view()[“name”].get_utf8()).to_string();
      

      不知道官方是怎么想的,读取一个值要这么麻烦。而且还没文档。我是看源代码,一点一点找出到底怎么读出来的。
      如果访问不存在的属性会返回NULL

      auto data = doc["phone"];
      if(data){
         // 数据存在时执行此处
      }{
         // 数据不存在时执行此处
      }
      

      这个地方官方的例子写错了。正确方式如上。同样访问不存在的数据变量时也是这样。获取数据的时候要根据自己存入时的数据类型调对应的的get_<type>取出。具体的可以看源代码
      数据OK,下一步就可以进行具体的数据库操作了。这个起来还是比较容易的。
      插入

      mongocxx::instance inst{};
      mongocxx::client conn{mongocxx::uri{}};
      auto collection = conn["test"]["test"];
      collection.insert_one(doc.view());
      

      其余的操作也差不多。但是值得注意的是如果连续快速插入数据就程序就会报出插入失败的错误。
      比如

      While(true){
         collection.insert_one(doc.view());
      }
      

      经过测试两次插入之间必须要延时3ms左右才能保证稳定执行。即使这时采用bulk write的方式写入还是会有错误。可能是由于新版驱动还不稳定所以才引入的bug。

      程序写完之后,编译又是一个问题。官方的文档只字未提cmake应该怎么引入mongodb的包。导致我刚开始还以为要自己去写findmongodb.cmake文件。实际上mongodb是自带cmake config文件的。如果采用默认安装的话,文件的位置应该和下图一样。

      alt text

      在cmake里面要引入libbsoncxx和libmongocxx两个库。下面是一个具体的例子

      find_package(libmongocxx REQUIRED)
      find_package(libbsoncxx REQUIRED)
      
      include_directories(
      ${PROJECT_SOURCE_DIR}
      ${PROJECT_SOURCE_DIR}/include
      ${LIBMONGOCXX_INCLUDE_DIRS}
      ${LIBBSONCXX_INCLUDE_DIRS}
      )
      
      target_link_libraries(${PROJECT_NAME}
      ${LIBMONGOCXX_LIBRARY_DIRS}/libmongocxx.so
      ${LIBBSONCXX_LIBRARY_DIRS}/libbsoncxx.so
      )
      

      这样就可以编译了。

      posted in 技术交流
      weijiz
      weijiz
    • 1
    • 2
    • 32
    • 33
    • 34
    • 35
    • 36
    • 35 / 36