EdmondFrank's 时光足迹

この先は暗い夜道だけかもしれない それでも信じて進むんだ。星がその道を少しでも照らしてくれるのを。
或许前路永夜,即便如此我也要前进,因为星光即使微弱也会我为照亮前途。
——《四月は君の嘘》

使用树莓派实现24小时不间断直播



使用树莓派进行24小时不间断直播

开始

多余的话就不多说了,今天本文为大家介绍两种使用树莓派来做直播服务器的方法。

方案一 ffmpeg + ffserver搭建流媒体服务器

首先
我们用到的工具有:

硬件方面:

  • 树莓派主板一块
  • 兼容树莓派的USB摄像头一个

软件方面:

  • ffmpeg,负责媒体文件的转码工作,把你服务器上的源媒体文件转成要发出去的流媒体文件。
  • ffserver,负责响应客户端的流媒体请求,把流媒体数据发送给客户端,相当与一个小型的服务端。

具体的工作方式就如下图所示:

多个输入源被“喂”到广播服务器,这些多媒体内容就会分发到多个客户端。上图的目的是显示地表明你的流系统能够被分成多个块部署到网络上,允许你广播不同的在线内容,而不需要改变流媒体系统的结构。

配置

无论是树莓派官方摄像头模块还是其他兼容的USB摄像头,连接好摄像头之后,运行命令去启用摄像头:

sudo raspi-config

ffserver.conf,ffserver启动时的配置文件,在这个文件中主要是对网络协议,缓存文件feed1.ffm(见下述)和要发送的流媒体文件的格式参数做具体的设定。

feed1.ffm,可以看成是一个流媒体数据的缓存文件,ffmpeg把转码好的数据发送给ffserver,如果没有客户端连接请求,ffserver把数据缓存到该文件中。

下面就是一个ffserver.conf的一个例子

Port 8090                      # Port to bind the server to
BindAddress 0.0.0.0
MaxHTTPConnections 2000
MaxClients 1000
MaxBandwidth 10000             # Maximum bandwidth per client
                               # set this high enough to exceed stream bitrate
CustomLog -
NoDaemon                       # Remove this if you want FFserver to daemonize after start

<Feed feed1.ffm>               # This is the input feed where FFmpeg will send
   File ./feed1.ffm            # video stream.
   FileMaxSize 64M              # Maximum file size for buffering video
   ACL allow 127.0.0.1         # Allowed IPs
</Feed>

<Stream test.webm>              # Output stream URL definition
   Feed feed1.ffm              # Feed from which to receive video
   Format webm

   # Audio settings
   AudioCodec vorbis
   AudioBitRate 64             # Audio bitrate

   # Video settings
   VideoCodec libvpx
   VideoSize 720x576           # Video resolution
   VideoFrameRate 25           # Video FPS
   AVOptionVideo flags +global_header  # Parameters passed to encoder
                                       # (same as ffmpeg command-line parameters)
   AVOptionVideo cpu-used 0
   AVOptionVideo qmin 10
   AVOptionVideo qmax 42
   AVOptionVideo quality good
   AVOptionAudio flags +global_header
   PreRoll 15
   StartSendOnKey
   VideoBitRate 400            # Video bitrate
</Stream>

<Stream status.html>            # Server status URL
   Format status
   # Only allow local people to get the status
   ACL allow localhost
   ACL allow 192.168.0.0 192.168.255.255
</Stream>

<Redirect index.html>    # Just an URL redirect for index
   # Redirect index.html to the appropriate site
   URL http://www.ffmpeg.org/
</Redirect>

ffserver启动时默认查看 /etc/ffserver.conf 配置文件,你可以通过-f选项控制查阅的配置文件。

ffserver -f ffserver.conf

运行结果如下所示的话,那么ffserver就算是启动成功了。

打开http://localhost:8090/status.html可以看到当前server中各个流的状态。

接入视频流

ffserver启动之后,就可以向
http://localhost:8090/feed1.ffm接入视频流。

注意,这里不需要指定编码格式,FFserver会重新编码。

视频流的来源可以是文件、摄像头或者录制屏幕。

接入视频文件

ffmpeg -i testvideo.mp4 http://localhost:8090/feed1.ffm

接入录制屏幕

ffmpeg -f x11grab -r 25 -s 640x512 -i :0.0 -f alsa -i pulse http://localhost:8090/feed1.ffm

这里有两个-f,第一个指的是视频流,第二个指的是音频流。视频流是抓取屏幕形成视频,-r设置帧率为25帧/s,-s设置抓取图像大小为640x512,-i设置录制视频的初始坐标。音频流设置为alsa(Advanced Linux Sound Architecture),从Linux系统中获取音频。这其中这样ffmpeg可以录制屏幕feed到feed1.ffm中。

接入摄像头直播

ffmpeg -f video4linux2 -s 640x480 -r 25 -i /dev/video0 -f alsa -i pulse http://localhost:8090/feed1.ffm

方案二 avconv 和 GStreamer 用于采集摄像头捕获的视频流并推送到 RTMP 服务

首先
我们用到的工具有:

硬件方面:

  • 树莓派主板一块
  • 兼容树莓派的USB摄像头一个

软件方面:

  • avconv 和 GStreamer 用于采集摄像头捕获的视频流并推送到 RTMP 服务
  • NGINX 和 RTMP 模块,用于接收视频流,同时提供视频发布功能

安装&配置

因为这里我们要用到nginx的rtmp模块作为服务端,而系统自带的apt安装的nginx是没有这个模块的,所以我们需要先安装后移除nginx然后再手动编译(安装是为了下载好相关依赖)。

sudo apt-get update
#安装 nginx
sudo apt-get -y install nginx
#移除 nginx
sudo apt-get -y remove nginx
sudo apt-get clean
#清空 nginx 的配置文件
sudo rm -rf /etc/nginx/*
#安装编译用的模块
sudo apt-get install -y curl build-essential libpcre3 libpcre3-dev libpcre++-dev zlib1g-dev libcurl4-openssl-dev libssl-dev
#创建存放网页的目录给 nginx 使用
sudo mkdir -p /var/www
#创建编译用的目录
mkdir -p ~/nginx_src
cd ~/nginx_src
#下载 nginx 源码包
wget http://nginx.org/download/nginx-1.11.8.tar.gz
#下载 nginx-rtmp-module 源码包
wget https://github.com/arut/nginx-rtmp-module/archive/master.zip
tar -zxvf nginx-1.11.8.tar.gz
unzip master.zip
cd nginx-1.11.8
#设定编译参数
./configure --prefix=/var/www --sbin-path=/usr/sbin/nginx --conf-path=/etc/nginx/nginx.conf --pid-path=/var/run/nginx.pid --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --with-http_ssl_module --without-http_proxy_module --add-module=/home/pi/nginx_src/nginx-rtmp-module-master
#开始编译安装
make
sudo make install

配置 nginx

sudo gedit /etc/nginx/nginx.conf

在末尾添加

rtmp {
    server {
        listen 1935;
        chunk_size 4096;
        application live {
            live on;
            record off;
        }
    }
}

重启 nginx 服务。

sudo service nginx start

安装 avconv 和 GStreamer

sudo apt-get update
sudo apt-get install libav-tools
#安装 GStreamer
sudo apt-get install gstreamer1.0-tools
#安装 GStreamer 扩展组件
sudo apt-get  install libgstreamer1.0-0 libgstreamer1.0-0-dbg libgstreamer1.0-dev liborc-0.4-0 liborc-0.4-0-dbg liborc-0.4-dev liborc-0.4-doc gir1.2-gst-plugins-base-1.0 gir1.2-gstreamer-1.0 gstreamer1.0-alsa gstreamer1.0-doc gstreamer1.0-omx gstreamer1.0-plugins-bad gstreamer1.0-plugins-bad-dbg gstreamer1.0-plugins-bad-doc gstreamer1.0-plugins-base gstreamer1.0-plugins-base-apps gstreamer1.0-plugins-base-dbg gstreamer1.0-plugins-base-doc gstreamer1.0-plugins-good gstreamer1.0-plugins-good-dbg gstreamer1.0-plugins-good-doc gstreamer1.0-plugins-ugly gstreamer1.0-plugins-ugly-dbg gstreamer1.0-plugins-ugly-doc gstreamer1.0-pulseaudio gstreamer1.0-tools gstreamer1.0-x libgstreamer-plugins-bad1.0-0 libgstreamer-plugins-bad1.0-dev libgstreamer-plugins-base1.0-0 libgstreamer-plugins-base1.0-dev

采集与呈现视频流

gst-launch-1.0 -v v4l2src device=/dev/video0 ! 'video/x-raw, width=1024, height=768, framerate=30/1' ! queue ! videoconvert ! omxh264enc ! h264parse ! flvmux ! rtmpsink location='rtmp://树莓派的IP地址/live live=1' &

采用以上命令就可以在后台采集USB摄像头拍摄的直播内容并推送到rtmp服务端上了。

呈现直播视频画面

1、使用 RTMP 播放器播放视频流
例如 VLC 等播放器(桌面版和手机版均有)支持 RTMP 视频流播放,填入 rtmp://树莓派的IP地址/live 即可播放。不过这个软件有数十秒的缓冲延迟,需要设定缓冲时间来缩短延迟。

2、推送至斗鱼直播平台观看
你可能注意到了 GStreamer 这个命令中有 location 这个参数。这个参数是设定采集到的视频流推向哪里,通过设定这个参数可以将视频流推向任何支持 RTMP 协议的服务器。

斗鱼平台同样采用了 RTMP 协议传输直播视频,首先获取斗鱼的 RTMP 推流地址。开启了直播室之后可以获得推流码。注意,斗鱼的推流码是有时限的,取到推流码需要尽快使用以免过期。

树莓派和L298N电机驱动模块实现智能小车控制



树莓派与L298N驱动模块实现智能小车控制

准备

首先在讲整体实现之前,笔者先附上自己的开发环境以及使用到的工具、硬件等。

用到的工具有:

  • 树莓派3代(自带wifi模块)
  • L298N电机驱动板
  • USB移动电源一个(为树莓派供电)
  • 电池组一组(为驱动模块、智能小车的电机供电)
  • 智能小车底盘
  • 杜邦线若干
  • 电脑一台(我的系统:Ubuntu 16.04 LTS Python3.5)

树莓派的GPIO引脚定义

树莓派的GPIO引脚共分为两种类型,一种是PHYSICAL NUMBERING

单纯地用从上至下,从左至右的顺序来定义引脚。

另外一种引脚定义方式是GPIO NUMBERING

采用特殊(未知)的方式来标记GPIO接口

这里,我采用的是第一种使用的引脚定义的方式。

L298N电机驱动模块

L298N 是一种双H桥电机驱动芯片,其中每个H桥可以提供2A的电流,功率部分的供电电压范围是2.5-48v,逻辑部分5v供电,接受5vTTL电平。一般情况下,功率部分的电压应大于6V否则芯片可能不能正常工作。

实物图如下

使用说明如下

我的接线图


代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
import argparse
import tornado.ioloop
import tornado.web
from datetime import datetime
import os
from operator import itemgetter
import RPi.GPIO as GPIO
import requests
from time import sleep

class PostHandler(tornado.web.RequestHandler):

    # I don't understand decorators, but this fixed my "can't set attribute" error
    @property
    def settings(self):
        return self._settings

    @settings.setter
    def settings(self,settings):
        self._settings = settings

    def initialize(self, settings):
        self.settings = settings

    def post(self):
        timestamp = datetime.now()
        data_json = tornado.escape.json_decode(self.request.body)
        allowed_commands = set(['37','38','39','40'])
        command = data_json['command']
        command = list(command.keys())
        command = set(command)
        command = allowed_commands & command
        file_path = str(os.path.dirname(os.path.realpath(__file__)))+"/session.txt"
        log_entry = str(command)+" "+str(timestamp)
        log_entries.append((command,timestamp))
        with open(file_path,"a") as writer:
            writer.write(log_entry+"\n")
        print(log_entry)
        speed = self.settings['speed']
        if '37' in command:
            motor.forward_left(speed)
        elif '38' in command:
            print("forward")
            motor.forward(100)
        elif '39' in command:
            motor.forward_right(speed)
        elif '40' in command:
            print("backward")
            motor.backward(100)
        else:
            motor.stop()

# This only works on data from the same live python process. It doesn't 
# read from the session.txt file. It only sorts data from the active
# python process. This is required because it reads from a list instead
# of a file  on data from the same live python process. It doesn't 
# read from the session.txt file. It only sorts data from the active
# log_entries python list
class StoreLogEntriesHandler(tornado.web.RequestHandler):
    def get(self):
        file_path = str(os.path.dirname(os.path.realpath(__file__)))+"/clean_session.txt"
        sorted_log_entries = sorted(log_entries,key=itemgetter(1))
        prev_command = set()
        allowed_commands = set(['38','37','39','40'])
        for log_entry in sorted_log_entries:
            command = log_entry[0]
            timestamp = log_entry[1]
            if len(command ^ prev_command) > 0:
                prev_command = command
                with open(file_path,"a") as writer:
                    readable_command = []
                    for element in list(command):
                        if element == '37':
                            readable_command.append("left")
                        if element == '38':
                            readable_command.append("up")
                        if element == '39':
                            readable_command.append("right")
                        if element == '40':
                            readable_command.append("down")
                    log_entry = str(list(readable_command))+" "+str(timestamp)
                    writer.write(log_entry+"\n")
                print(log_entry)
        self.write("Finished")

class MultipleKeysHandler(tornado.web.RequestHandler):

    def get(self):
        print("HelloWorld")
        self.write('''
                <!DOCTYPE html>
                <html>
                    <head>
                        <script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
                        <script>
                            var keys = {};

                            $(document).keydown(function (e) {
                                keys[e.which] = true;
                                
                                var json_upload = JSON.stringify({command:keys});
                                var xmlhttp = new XMLHttpRequest(); 
                                xmlhttp.open("POST", "/post");
                                xmlhttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
                                xmlhttp.send(json_upload);

                                printKeys();
                            });

                            $(document).keyup(function (e) {
                                delete keys[e.which];
                                
                                var json_upload = JSON.stringify({command:keys});
                                var xmlhttp = new XMLHttpRequest(); 
                                xmlhttp.open("POST", "/post");
                                xmlhttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
                                xmlhttp.send(json_upload);

                                printKeys();
                            });

                            function printKeys() {
                                var hcommandtml = '';
                                for (var i in keys) {
                                    if (!keys.hasOwnProperty(i)) continue;
                                    html += '<p>' + i + '</p>';
                                }
                                $('#out').html(html);
                            }

                        </script>
                    </head>
                    <body>
                        Click in thiscommand frame, then try holding down some keys
                        <div id="out"></div>
                    </body>
                </html>
            ''')


# class Motor:
#     def __init__(self, pinForward, pinBackward, pinControlStraight,
#     pinLeft, pinRight, pinControlSteering):
#         self.pinForward = pinForward
#         self.pinBackward = pinBackward
#         self.pinControlStraight = pinControlStraight
#         self.pinLeft = pinLeft
#         self.pinRight = pinRight
#         self.pinControlSteering = pinControlSteering
#         GPIO.setup(self.pinForward, GPIO.OUT)
#         GPIO.setup(self.pinBackward, GPIO.OUT)
#         GPIO.setup(self.pinControlStraight, GPIO.OUT)

#         GPIO.setup(self.pinLeft, GPIO.OUT)
#         GPIO.setup(self.pinRight, GPIO.OUT)
#         GPIO.setup(self.pinControlSteering, GPIO.OUT)

#         self.pwm_forward = GPIO.PWM(self.pinForward, 100)
#         self.pwm_backward = GPIO.PWM(self.pinBackward, 100)
#         self.pwm_forward.start(0)
#         self.pwm_backward.start(0)

#         self.pwm_left = GPIO.PWM(self.pinLeft, 100)
#         self.pwm_right = GPIO.PWM(self.pinRight, 100)
#         self.pwm_left.start(0)
#         self.pwm_right.start(0)

#         GPIO.output(self.pinControlStraight,GPIO.HIGH) 
#         GPIO.output(self.pinControlSteering,GPIO.HIGH) 

#     def forward(self, speed):
#         """ pinForward is the forward Pin, so we change its duty
#              cycle according to speed. """
#         self.pwm_backward.ChangeDutyCycle(0)
#         self.pwm_forward.ChangeDutyCycle(speed)    

#     def forward_left(self, speed):
#         """ pinForward is the forward Pin, so we change its duty
#              cycle according to speed. """
#         self.pwm_backward.ChangeDutyCycle(0)
#         self.pwm_forward.ChangeDutyCycle(speed)  
#         self.pwm_right.ChangeDutyCycle(0)
#         self.pwm_left.ChangeDutyCycle(100)   

#     def forward_right(self, speed):
#         """ pinForward is the forward Pin, so we change its duty
#              cycle according to speed. """
#         self.pwm_backward.ChangeDutyCycle(0)
#         self.pwm_forward.ChangeDutyCycle(speed)
#         self.pwm_left.ChangeDutyCycle(0)
#         self.pwm_right.ChangeDutyCycle(100)

#     def backward(self, speed):
#         """ pinBackward is the forward Pin, so we change its duty
#              cycle according to speed. """

#         self.pwm_forward.ChangeDutyCycle(0)
#         self.pwm_backward.ChangeDutyCycle(speed)

#     def left(self, speed):
#         """ pinForward is the forward Pin, so we change its duty
#              cycle according to speed. """
#         self.pwm_right.ChangeDutyCycle(0)
#         self.pwm_left.ChangeDutyCycle(speed)  

#     def right(self, speed):
#         """ pinForward is the forward Pin, so we change its duty
#              cycle according to speed. """
#         self.pwm_left.ChangeDutyCycle(0)
#         self.pwm_right.ChangeDutyCycle(speed)   

#     def stop(self):
#         """ Set the duty cycle of both control pins to zero to stop the motor. """

#         self.pwm_forward.ChangeDutyCycle(0)
#         self.pwm_backward.ChangeDutyCycle(0)
#         self.pwm_left.ChangeDutyCycle(0)
#         self.pwm_right.ChangeDutyCycle(0)

class Motor:
    def __init__(self, pinForward, pinBackward, pinForward2,
     pinBackward2,pinLeft, pinRight):
        """ Initialize the motor with its control pins and start pulse-width
             modulation """

        self.pinForward = pinForward
        self.pinBackward = pinBackward
        self.pinForward2 = pinForward2
        self.pinLeft = pinLeft
        self.pinRight = pinRight
        self.pinBackward2 = pinBackward2


        GPIO.setup(self.pinLeft, GPIO.OUT)
        GPIO.setup(self.pinRight, GPIO.OUT)

        GPIO.setup(self.pinForward, GPIO.OUT)
        GPIO.setup(self.pinBackward, GPIO.OUT)
        GPIO.setup(self.pinForward2, GPIO.OUT)
        GPIO.setup(self.pinBackward2, GPIO.OUT)

        self.pwm_left = GPIO.PWM(self.pinLeft, 100)
        self.pwm_right = GPIO.PWM(self.pinRight, 100)
        self.pwm_left.start(0)
        self.pwm_right.start(0)
    def forward(self, speed):
        self.pwm_right.ChangeDutyCycle(speed)
        self.pwm_left.ChangeDutyCycle(speed)
        GPIO.output(self.pinForward, True)
        GPIO.output(self.pinBackward, False)
        GPIO.output(self.pinForward2, True)
        GPIO.output(self.pinBackward2, False)
    def backward(self, speed):
        self.pwm_right.ChangeDutyCycle(speed)
        self.pwm_left.ChangeDutyCycle(speed)
        GPIO.output(self.pinForward, False)
        GPIO.output(self.pinBackward,  True)
        GPIO.output(self.pinForward2, False)
        GPIO.output(self.pinBackward2,  True)

    def forward_right(self, speed):
        self.pwm_right.ChangeDutyCycle(speed)
        self.pwm_left.ChangeDutyCycle(100)
        # GPIO.output(self.pinForward, True)
        # GPIO.output(self.pinBackward, False)
        # GPIO.output(self.pinForward2, True)
        # GPIO.output(self.pinBackward2, False)
    def forward_left(self, speed):
        self.pwm_right.ChangeDutyCycle(100)
        self.pwm_left.ChangeDutyCycle(speed)
        # GPIO.output(self.pinForward, True)
        # GPIO.output(self.pinBackward, False)
        # GPIO.output(self.pinForward2, True)
        # GPIO.output(self.pinBackward2, False)

    def stop(self):
        """ Set the duty cycle of both control pins to zero 
            to stop the motor. """
        self.pwm_left.ChangeDutyCycle(0)
        self.pwm_right.ChangeDutyCycle(0)
        GPIO.output(self.pinForward,0)
        GPIO.output(self.pinBackward,0)
        GPIO.output(self.pinForward2,0)
        GPIO.output(self.pinBackward2,0)
def make_app(settings):
    return tornado.web.Application([
        (r"/drive",MultipleKeysHandler),(r"/post", PostHandler, {'settings':settings}),
        (r"/StoreLogEntries",StoreLogEntriesHandler)
    ])

if __name__ == "__main__":

    # Parse CLI args
    ap = argparse.ArgumentParser()
    ap.add_argument("-s", "--speed_percent", required=True, help="Between 0 and 100")
    args = vars(ap.parse_args())
    #GPIO.cleanup(0)
    GPIO.setmode(GPIO.BOARD)
    motor = Motor(18, 19, 21, 22, 23, 24)
    log_entries = []
    settings = {'speed':float(args['speed_percent'])}
    app = make_app(settings)
    app.listen(81)
    tornado.ioloop.IOLoop.current().start()

使用说明

首先,按照依赖配置好树莓派中的python环境,建议使用python3以上版本。

然后在树莓派接入路由后,采用远程终端的方式,运行以上的python脚本。

sudo python3 drive_api.py --speed_percent 5

注:非root用户一定要加上sudo,否则无法读写树莓派的GPIO口。

最后,通过电脑在同一个内网内使用浏览器打开地址192.168.1.208:81/drive(这个地址根据你的树莓派接入路由的实际地址而定,以上为笔者实验使用地址,仅供格式参考)。

在打开的网页内,通过电脑的方向键就可以对树莓派进行“驾驶”了。

重读人月神话



重读《人月神话》

何为《人月神话》?

今天,偶然地重读了一遍《人月神话》。在IT领域中,即使这本书出版距今已经超过十年,但其中的道理依旧盛行。

《人月神话》虽然是布鲁克斯博士在IBM公司研发并管理System/360计算机家族和OS/360软件支持包期间的项目管理经验,但是其经典程度堪称软件开发项目管理的典范。

什么成就了它的经典

翻开《人月神话》这本书的第一感受,这边书不像以往文绉绉的项目管理或软件工程手册。作者用他切身的经验,结合自己精彩的文笔,写出了一本有温度的指导。

书中的很多问题和案例都直击了一个软件开发流程当中出现的情景。作者以一些生动的比喻更为形象的让读者身感同受。

书中的精炼

前车之覆,后车之鉴。

在执行项目或任务过程中,一味地添加人员并不能加快项目的进度。
因为软件开发本质上是一项系统工作——错综复杂的关系下实践、沟通、交流的工作量非常之大,它很快就消耗了任务分解节省下来的个人时间。从而,添加更多的人手,实际上是延长了而不是缩短了时间进度。

研究表明,效率高和效率低的实施者之间个体差异非常大,经常能够达到数量级水平。

系统设计之中,概念的完整性应该是最重要的考虑因素,为了反映一系列连贯的设计思路,宁可省略一些不规则的特性和改进。

简洁和直白都来自概念的完整性。在语法上,每个部分应使用相同的技巧;在语义上,应具有同样的相似性。因此,易用性实际上需要设计的一致性和概念的完整性。

在等待时,实现人员应该做什么?
整个创造性活动包括三个独立的阶段:体系结构、设计实现、物理实现,实际情况中,他们往往可以同时开始和并发进行。

坚持至少拥有两个系统或版本以上的开发设想,避免在设计第二个系统的时候就出现过分设计。

文档化的规格,手册不仅要描述包括所有界面在内的用户可见的一切,还要避免描述用户看不见的事物。后者是编程实现人员的的工作范畴,其设计和创造是不应该被限制的。

贯彻执行,计划书写的再完善,没有贯彻执行也是一张白纸而已。

巴比伦塔的管理教训:大型编程项目中的交流和组织能力非常重要。

团队之间的交流沟通方案:
非正式途径:电话、短信、邮件、一切即时通讯手段。
项目会议:常规会议,进度会议。
工作手册及项目文档:准备好开发相关的手册和交互文档。

团队组织的目的是减少所需要的交流和合作的数量,其最好的方法是人力划分和职责限定。

实践是最好的老师,但智者还能从其他地方有所收获。

工作量 = 常数 x 指令数量1.5次方

使用适当的高级语言,编程的生产率可以提高5倍。

书面记录决策是必要的。只有记录下来,分歧才会明朗,矛盾才会突出。书写这项活动需要上百次的细小决定,正是由于它们的存在,人们才能从令人迷惑的现象中得到清晰,确定的策略。

普遍的做法是,选择一种方法,试试看;如果失败了,没关系,再试试别的方法。不管怎么样,重要的是先去尝试。

在项目开发中应该构建 “试验性工厂” 和 “产品” 这两个步骤,不要把产品原型发布给用户。对于大多数项目而言,第一个开发的系统并不合用,它可能太慢、太大或难以使用,这样要解决所有的问题除了重新开始以外,没有其他的办法。

系统软件开发是 “减熵” 的过程,所以它本身是处于亚稳态的。软件维护是 “增熵” 的过程,即使是最熟练的软件维护工作,也只是放缓了系统退化到非稳态的进程。

系统各个组成部分的开发者都会做出一些假设,而这些假设之间的不匹配是大多数致命和难以察觉的bug的主要来源。

模块分割、模块独立、结构化编程、构件单元测试是避免系统性bug的良好手段。

需要什么样的文档?
(1)使用程序:每个用户都需要一段对程序进行描述的文字。可是大多数文档只提供了很少总结性的内容,无法达到用户的要求,就是像描绘了树木,形容了树皮和树叶,但却没有一副森林的图案。
(2)目的:主要功能是什么?开发程序的目的是什么?
(3)环境:程序运行在什么样的机器、硬件配置和操作系统上?
(4)范围:输入的有效范围是什么?允许显示的合法输出范围是什么?
(5)实现功能和使用的算法:精确地阐述它做了什么?
(6)“输入——输出”格式:必须是确切的,完整的。
(7)操作指令:包括控制台及输出内容中正常和异常结束的行为。
(8)选项:用户的功能选项有哪些?如何在选项之间进行挑选?
(9)运行时间:在指定的配置下,解决特定规模问题所需要的时间。
(10)精度和校验:期望结果的精确程度?如何进行精度的检测?

团队在书中的倒影

我们团队一年来的开发弊端都有在书中的案例体现。

《人月神话》就像是一个个项目开发小组的倒影,项目交流成本、开发者效率的差异、开发人员各自独立的项目假设造成的隐藏bug、对项目进度的乐观预估,其中最为突出的莫过于是巴比伦塔的管理教训,沟通和有效组织的缺乏,直接拖缓了整个项目的进度。

我想,在经验中总结前进,最有效的莫过于《人月神话》开篇的第一章:前车之覆,后车之鉴。

                                            By 领沃EdmondFrank

数据恢复利器-Testdisk

数据恢复利器-Testdisk

今天我要给大家介绍的主角是Testdisk

首先,Testdisk是一个强大的免费的跨平台的数据恢复工具,根据它的官网上的简介,这款软件主要被设计用于恢复丢失的分区以及修复那些由于人为或者病毒等原因导致分区表错误而无法启动系统的问题。

除此之外,Testdisk更多的特性大家可以参考官方列出的功能列表:

TestDisk can Fix partition table, recover deleted partition Recover FAT32 boot sector from its backup Rebuild FAT12/FAT16/FAT32 boot sector Fix FAT tables Rebuild NTFS boot sector Recover NTFS boot sector from its backup Fix MFT using MFT mirror Locate ext2/ext3/ext4 Backup SuperBlock Undelete files from FAT, exFAT, NTFS and ext2 filesystem Copy files from deleted FAT, exFAT, NTFS and ext2/ext3/ext4 partitions.

对于Testdisk的强大之处,肯定是不容质疑的,对此笔者在之前自己的系统修复过程切身体会过Testdisk的实用与强大之处(在笔者的旧硬盘全盘分区表丢失的情况之下,使用Testdisk成功恢复了大部分的分区,并能成功启动系统。看到系统还能成功开机那一刻别提多激动了!)

跨平台

Testdisk不仅强大而且还能够跨平台,跨平台,跨平台使用(重要的事情说三遍)并且还支持多种文件系统。

简单的介绍就到这里了!!

下面我们就用一个实际的例子来演示一下 Testdisk的具体使用方法:

本例子摘取自:Testdisk 操作指南 PS:毕竟为了演示而认为制造一些错误也是有一定风险的,所以为了方便和安全起见笔者这里摘抄一份别人的例子啦,反正也是为了向大家安利下这个软件。

前提条件:

  • TestDisk 需要用管理员权限来运行。

使用 TestDisk 的重点:

  • 用 方向键 和 上一页/下一页 按键在TestDisk 中导航。
  • 然后, 按Enter 键确认你的选择.
  • 若要返回前一页或者退出TestDisk, 按 q (Quit) 键。
  • 若要在 TestDisk 下保存修改,按 y (Yes) 或者 Enter 键来确认
  • 如果确实要把分区信息写入主引导记录(MBR),应该选择 “Write” 选项并按 Enter 键。

运行Testdisk

如果 TestDisk 还没有被安装, 可以从这里下载 TestDisk Download。然后解压缩这个归档文件,包括子文件夹。

 一、新建日志

  • 选择 Create 来让 Testdisk 新建一个日志文件( log file) ,里边包含了一些技术信息和消息, 除非你要往一个日志里追加信息 log 或者 你从只读存储器里执行 TestDisk 而且必须在别的地方建立日志 log。
  • 选择 None 如果你不想让过程中的细节和消息记录到日志文件里 log file (比如当 Testdisk 是在只读位置执行的时候,这很有用).
  • 按 Enter 键以继续. enter image description here

 二、选择磁盘

所有的硬盘都应该能被TestDisk检测到并且辅以正确的大小列出来:

  • 用 上/下 键 来选择丢失分区的硬盘。
  • 按 Enter 键继续。 enter image description here

三、选择分区表类型

TestDisk 会显示分区表类型。

  • 选择正确的分区表类型 - 在 TestDisk 自动检测分区表类型之后,一般预设的那个值是正确的。
  • 按 Enter 键继续。 enter image description here

四、查看当前分区状态

TestDisk 显示这个菜单的时候 (参见 TestDisk Menu Items). + 用预设的“analyze”(分析)选项来检查当前的分区结构并搜索丢失的分区。 + 分析过程中按 Enter 键继续。 第一个分区显示了两次,它指向了一个毁坏的分区或一个无效的分区表入口。 + 无效的 NTFS boot 指向了一个错误的 NTFS boot 扇区, 所以这是一个损坏的文件系统。 在扩展分区中,只有一个逻辑分区(分区标签为2)可用。 有一个逻辑分区不见了。 + 选 Quick Search (快速搜索)来继续。 然后,当前的结构就会被列出来。 接下来就可以在当前的分区结构中检查丢失或错误的分区了。

enter image description here

五、快速搜索分区

在 Quick Search(快速搜索)的过程中, TestDisk 找到了两个分区,包括那个不见的逻辑分区(标签为 Partition 3 ) enter image description here + 高亮这个分区并按 p 来列出文件 (若要返回前一页,请按 q ). + 这里所有的目录和文件都正确列出来了。 + 按 Enter 键继续。 + enter image description here +

六、保存分区表

  • 当全部分区都可用的时候 并且数据已正确列出,应该选 Write 菜单项保存分区结构. 菜单项 Extd Part gives you the opportunity to decide if the extended partition will use all available disk space or only the required (minimal) space.

  • 当一个分区,第一个,仍然找不到, 高亮菜单项 深度搜索 (没有自动进行的时候) ,按 Enter 键继续.

enter image description here (经过笔者的几次实验和朋友的反馈,其实到了这一步已经能够解决80%以上的问题了!)

所以,有关Testdisk更加深入的功能和其他详细用法大家可以前往这个中文版的官方指南在这里笔者就不赘述了!