December 5, 2020
By: Kevin

raspberry(linux), clojure (websockt), emacs (cider), (pythoy ble, serial, opencv), etc.

  1. 系统架构
  2. 树莓派
    1. 开机自启动
    2. 自动更新
    3. 远端修改配置&执行脚本
  3. emacs的trampmode
  4. websocket
  5. 摄像头拍照
  6. 串口通讯
  7. clojure调用脚本
  8. static scope 和 dynamic scope
  9. 线程的pause和resume

系统架构

  1. 树莓派端: 作为设备管理器, 开机时候通过websocket和服务器建立连接, 汇报状态
  2. 服务器端: 一方面为client提供rest接口, 另一方面通过websocket和树莓派通讯, 监控状态, 下发命令, 调用硬件
                                +--------------------+
                                |       client       |
                                +---------+----------+
                                          |
                                          |
                                          |restful
                                          |
                                          |
                               +----------+----------+
                               |                     |
                               | web/socket-server   |
                               |                     |
                               +----------+----------+
                                          |               websocket
       +----------------+-----------------+---------------+-------------------+
       |                |                 |               |                   |
       |                |                 |               |                   |
  +----+-----+     +----+----+        +---+----+     +----+-----+       +-----+----+
  |   pi     |     |   pi    |        |    pi  |     |    pi    |       |   ....   |
  +----------+     +---------+        +----+---+     +----------+       +----------+
                                          | ble
                               usb        |      com
                      +-----------------+ | +----------------+
                      |  usb cameras    +-+-+  height detect |
                      +-----------------+ | +----------------+
                                          |
                                    +-----+------------+
                                    |    mi   scale    |
                                    +------------------+

树莓派

硬件:

  1. 开发用的 是raspberry 400(4G内存, 就是那个键盘)
  2. 部署是用的rasbberry 4b(2G内存, 那块绿色的小板子)

400

/sshx:pi-work:/home/pi $ lsb_release -a
No LSB modules are available.
Distributor ID:	Raspbian
Description:	Raspbian GNU/Linux 10 (buster)
Release:	10

系统开发依赖的软件和库

| package | version        |
|---------|----------------|
| openjdk | openjdk 11.0.9 |
| clojure | clojure1.10    |
| python  | Python 3.7.3   |
|         |                |

开机自启动

树莓派的应用使用clojure写的,我们要把启动命令写成一个脚本

#!/bin/sh
cd /home/pi/sandbox/rc/new-device/app
lein run

这个脚本放到开机的入口

#!/bin/sh -e
#
# rc.local
#
# This script is executed at the end of each multiuser runlevel.
# Make sure that the script will "exit 0" on success or any other
# value on error.
#
# In order to enable or disable this script just change the execution
# bits.
#
# By default this script does nothing.

# Print the IP address
_IP=$(hostname -I) || true
if [ "$_IP" ]; then
  printf "My IP address is %s\n" "$_IP"
fi

su pi -c "exec /home/pi/run-client.sh"  # <--- this line make the magic happen.

exit 0;

自动更新

设置系统的cron定时任务, 最小的定时粒度是分钟, 星号是通配符

+-------------------------------------------------------------+
|                                                             |
| *     *     *   *    *        command to be executed        |
| -     -     -   -    -                                      |
| |     |     |   |    |                                      |
| |     |     |   |    +----- day of week (0-6) (Sunday=0)    |
| |     |     |   +------- month (1-12)                       |
| |     |     +--------- day of month (1-31)                  |
| |     +----------- hour (0-23)                              |
| +------------- minute (0-59)                                |
|                                                             |
|                                                             |
+-------------------------------------------------------------+

通过自动拉取git代码, 自动更新.

每分钟git pull一次, 设置好要忽略的文件,保证pull可以成功, 结合重启后的自动运行, 完成了系统升级

远端修改配置&执行脚本

通过特定接口触发配置文件修改

emacs的trampmode

极大方便了开发和调试, 使用emacs tramp功能同时远端调试server和设备

  remote server                                        remote raspberry

 +--------------+              websocket              +-------------+
 |    service   +-------------------------------------+socket client|
 +--------------+                                     +-------------+
 |    nrepl     |                                     |    nrepl    |
 +--------------+                                     +-------------+
 |    sshd      |                                     |     sshd    |
 +------+-------+                                     +------+------+
        |                                                    |
        |ssh                local dev emacs                  | ssh
 +------+----------------------------------------------------+------+
 |                            ssh                                   |
 +------------------------------------------------------------------+
 |                           emacs tramp                            |
 +------------------------------------------------------------------+
 |                  cider projectile etc.                           |
 +------------------------------------------------------------------+

websocket

双向连接始终是一个复杂的话题, socket是一个比较容易接受的方案, 但是socket的使用可以非常复杂.

  1. socket怎么建立
  2. 消息队列缓冲
  3. socket保活
  4. 断连后恢复

sente 帮我们解决了大部分的socket繁琐底层事务.

摄像头拍照

使用opencv写的python脚本, 抓特定摄像头的输入为图片

import cv2

cam = cv2.VideoCapture(0)
ret, frame = cam.read()
if not ret:
    print("failed to grab frame")
else:
    img_name = "opencv_frame.png"
    cv2.imwrite(img_name, frame)
    print("{} written!".format(img_name))
cam.release()

串口通讯

我们系统中使用超声传感器做为身高测量. 这个传感器使用UART串口, 我们使用USB转连

   +-----------------+                       +-----------------+
   |                 +----------+------------+                 |
   |  raspberry pi   |  usb port|  uart port |     sensor      |
   |                 +----------+------------+                 |
   +-----------------+                       +-----------------+

usb port和uart port之间通过一个 USB to TTL的设备做转接, 其中的连线方式如下, 注意tx-rx, rx-tx, 即接受端和发送端连接, 发送端和接受端连接.

        usb to TTL                        height detec
        +-----------+                    +-----------+
        |           |                    |           |
        |           |                    |           |
        |       +5V +--------------------+ VCC       |
        |           |                    |           |
        |        RX +--------------------+ TX        |
        |           |                    |           |
        |        TX +--------------------+ RX        |
        |           |                    |           |
        |       GND +--------------------+ GND       |                          .
        |           |                    |           |
        |       3V3 +---              ---+ OUT       |
        |           |                    |           |
        |           |                    |           |
        |           |                    |           |
        +-----------+                    +-----------+

使用python的串口读取库来做串口的读写通讯

下发指令:0x55 0xaa 0x01 0x01 0x01 checksum 读取数据:0x55 0xAA 0x01 0x01 0x02 0x33 checksum

第5,6位是读到的数据

查看系统现有的串口 python -m serial.tools.list_ports

packet = bytearray()
packet.append(0x55)
packet.append(0xaa)
packet.append(0x01)
packet.append(0x01)
packet.append(0x01)

srial.Serial('/tty/USB0', 9600, timeout=1)
ser.write(packet)
ser_bytes = ser.readall()
distance = struct.unpack('>H', ser_bytes[4:6])

clojure调用脚本

clojure需要调用python脚本实现串口读取, usb摄像头拍照都需要调用shell脚本, sh函数返回被调用脚本执行结果的一个map, 返回状态码, 标准输出stdout, stderr的字符串.

(clojure.java.shell/sh "pwd")
=> {:exit 0,
    :out "/Users/kevin/sandbox/rc/new-device/back-end\n",
    :err ""}

static scope 和 dynamic scope

静态作用域, 又称为词法作用域 (lexical scope), 这种作用域是我们接触的绝大多数语言的作用域 (Java, C/C++, Python甚至JavaScript).

每个block都有自己的作用域, 子blog的作用域中的变量可以override父级作用域的同名变量, 作用域的规则在编译期间完全决定.

(let [x 1]
  (let [x 2]
    (prn "inside scope:" x))
  (prn "outside scope:" x))

动态作用域是clojure的一个特性, 是一个动态特性 (执行期间)

要理解symbol, var, value

                    +--------------+------------+------------+
                    |              |            |            |
 (def a 1) -------> |  symbol: a   | -----------|  value: 1  |
                    |              |     var    |            |
                    +--------------+------------+------------+

                           #'a  is the same as (var a)

实现方式:

  1. 在声明一个var (def/defn)的时候, 声明为dynamic (defn ^:dynamic *a* 1).
  2. 线程在执行某段代码时候, 可以使用 (binding [*a* 2] ...) 在thread-local中存储变量.
  3. 退出这个block的时候恢复原来的绑定值.

应用场景:

动态控制程序的执行行为, 比如dbeug, 单元测试, 打log等, 比如下面的单元测试固定了执行的硬件版本.

(t/deftest test-vs-cmd
  (t/testing "vs-cmd-890"
    (binding [state/*hardware-version* (fn [] "890")]
      (t/is (=
             "01458100000000000000"
             (sut/vs->cmd [:v1 :v3 :v7 :v9 :v16] :open))))))

线程的pause和resume

在java中, 实现线程的暂停和继续需要用到wait和notify. Clojure的实现则更简单

(ns httpkit-back-end.util
  (:require [clojure.core.async :refer [>!! chan timeout alts!!]]))

(def *msg-record (atom {}))

(defn msg-send [{:keys [device-id msg-type msg-id] :as msg} send-fn]
  (let [c (chan)]
    (swap! *msg-record assoc-in [device-id msg-type msg-id ]  c)
    (send-fn)
    (let [[msg-body _] (alts!! [(timeout 5000) c])]
      (swap! *msg-record  dissoc device-id)
      msg-body)))

(defn msg-recv [{:keys [device-id msg-type msg-id msg-body]}]
  (let [c (get-in @*msg-record [device-id msg-type msg-id])]
    (when c
      (>!! c msg-body ))))

下面是我们的三个单元测试场景, 分别模拟了正常和超时的情况, 注意到msg-reve都是在一个单独的线程完成的.

(ns httpkit-back-end.util-test
  (:require [httpkit-back-end.util :as sut]
            [clojure.core.async :refer [thread]]
            [clojure.test :as t]))

(t/deftest 收发消息
  (t/testing "收发完整流程"
    (t/is
     (= "100"
        (do
          (thread
            (Thread/sleep 10)
            (sut/msg-recv {:device-id 1 :msg-type :a/b :msg-id 123 :msg-body "100"}))
          (sut/msg-send {:device-id 1 :msg-type :a/b :msg-id 123} prn)))))
  (t/testing "超时时间之内 < 5s"
    (t/is
     (= "100"
        (do
          (thread
            (Thread/sleep 1000)
            (sut/msg-recv {:device-id "1" :msg-type :a/b :msg-id 124 :msg-body "100"}))
          (sut/msg-send {:device-id "1" :msg-type :a/b :msg-id 124} prn)))))
  (t/testing "超时 > 5s"
    (t/is
     (nil?
      (do
        (thread
          (Thread/sleep 6000)
          (sut/msg-recv {:device-id "1" :msg-type :a/b :msg-id 125 :msg-body "100"}))
        (sut/msg-send {:device-id "1" :msg-type :a/b :msg-id 125} prn))))))
Tags: clojure raspberry linux