深度优先

这个家伙好懒,除了文章什么都没留下

0%

http://www.hardyun.com/355.html

视频链接

Youtube: https://www.youtube.com/watch?v=1qs97MuROMs
B站: https://www.bilibili.com/video/BV1ZE411F7pB

相关配置和脚本

1. crontab 定时任务列表

1
2
3
4
5
6
7
pi@raspberrypi:~ $ crontab -l
*/2 * * * * /usr/local/bin /camera.sh
00 04 * * * /usr/local/bin/deleteold.sh
00 03 * * * /usr/local/bin/sun.sh
05 06 * * * /usr/local/bin/daynight.sh day
19 18 * * * /usr/local/bin/daynight.sh night
@reboot /usr/local/bin/initgpio.sh

2. 拍照脚本 camera.sh

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
pi@raspberrypi:~ $ cat /usr/local/bin/camera.sh
#!/bin/bash

photo_root_dir="/home/pi/Pictures"
ircutnode="/sys/class/gpio/gpio17/value"
suffix=""
filename=""

cmdname="veye_raspistill"

date_day=$(date +%Y-%m-%d)
date_time=$(date  +%Y-%m-%d-%H-%M )
#echo $date_time

errorlog="/home/pi/error.txt"
ret=$(ps -A -ww | grep $cmdname | awk '{print $4}')
if [ "$ret" = "$cmdname" ];then
  echo "$date_time: Photo command process $ret blocked, reboot!" >> $errorlog
  /usr/bin/sudo /sbin/shutdown -r now
  exit 1
fi

if [ -f $ircutnode ];then
  ircutvalue=$( cat $ircutnode)
  if [ "$ircutvalue" = "0" ];then
    suffix="day"
  else
    suffix="night"
  fi
fi
#echo $suffix

filename=${date_time}_${suffix}

current_dir=$photo_root_dir/$date_day
#echo $current_dir

if [ ! -d $current_dir ]; then
    mkdir -p $current_dir
fi

/usr/local/bin/$cmdname -o $current_dir/ $filename.jpg

temperature_18b20=$(cat /sys/bus/w1/devices/ 28-00000b48b969/w1_slave | grep 't=' | sed 's/^.*t=//g' | awk '{print $1/1000}')
#echo $temperature_18b20

temperature_cpu=$(cat /sys/class/thermal/thermal_zone0 /temp | awk '{print $1/1000}')
#echo $temperature_cpu

#echo $date_time $temperature_18b20 $temperature_cpu > $current_dir/$filename.txt
printf "%s %-7s %-6sn" $date_time $temperature_18b20 $temperature_cpu > $current_dir/$filename.txt

/bin/sync

**3. 删除前第6天照片的脚本 deleteold.sh**

pi@raspberrypi:~ $ cat /usr/local/bin/deleteold.sh
#!/bin/bash

photo_root_dir="/home/pi/Pictures"

#date_day=$(date +%Y-%m-%d)
date_day=$(date -I -d "$(date +%Y-%m-%d) -6 days")

to_clear_dir=$photo_root_dir/$date_day

if [ -d $to_clear_dir ]; then
  rm -rf "$to_clear_dir"
  echo "$to_clear_dir has been deleted!"
  /bin/ sync
fi

4. 绐定经纬度和日期,批量生成每天日出和日落时间的脚本 create-date-list.sh
派上需要安装的软件

1
sudo apt install lynx jq
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
pi@raspberrypi:~ $ cat create-date-list.sh
#!/bin/bash

lat=42.33
lng=118.88

date_start="2020-02-17"
days=30

for i in $(eval echo "{0..${days}"}); do
  dateid=$(date -I -d "$date_start + $i days")
  #echo ===$dateid
  sunrise_utc=$(lynx --dump   "https://api.sunrise-sunset.org/json?lat=$lat&lng=$lng&date=$dateid" | jq '.results.sunrise' )
  sunrise_utc="${sunrise_utc//"}"
  sunrise_utc=$(date -d "$dateid $sunrise_utc UTC" "+%Y-%m-%d %H:%M")
  #echo $sunrise_utc

  #date_day=$(echo $sunrise_utc | awk '{print $1}')
  date_day=$(date -I -d "$date_start +$(expr $i + 1) days")
  date_sunrise=$(echo $sunrise_utc | awk '{print $2}')

  sleep 1

  #echo ===$date_day
  #echo $date_sunrise
  sunset_utc=$(lynx --dump  "https://api.sunrise-sunset.org/json?lat=$lat&lng=$lng&date=$date_day" | jq '.results.sunset' )
  sunset_utc="${sunset_utc//"}"
  sunset_utc=$(date -d "$date_day $sunset_utc UTC" "+%Y-%m-%d %H:%M")
  #echo $sunset_utc

  date_sunset=$(echo $sunset_utc | awk '{print $2}' )

  echo $date_day $date_sunrise $date_sunset

  sleep 1
done

将生成的日出日落时间数据保存到sun.txt文件中

1
./create-date-list.sh 2>&1 | tee sun.txt

5. 生成每天日出日落时切换摄像头日夜模式的脚本 sun.sh

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
#!/bin/bash

sunfile="/home/pi/sun.txt"
suncron="/home/pi/crontabfile"

date_day=$(date +%Y-%m-%d)

sun=$(cat $sunfile | grep $date_day)
sunrise=$(echo $sun | awk '{print $2}')
sunset=$(echo $sun | awk '{print $3}')

#echo $sunrise $sunset

sunrise_h=$(echo $sunrise | awk -F ':' '{print $1}')
sunrise_m=$(echo $sunrise | awk -F ':' '{print $2}')

if [ $(($sunrise_m % 2)) -eq 0 ];then
  sunrise_m=$(printf "%02d" $((sunrise_m+1)))
fi

sunset_h=$(echo $sunset | awk -F ':' '{print $1}')
sunset_m=$(echo $sunset | awk -F ':' '{print $2}')

if [ $(($sunset_m % 2)) -eq 0 ];then
  sunset_m=$(printf "%02d" $((sunset_m+1)))
fi

echo "*/2 * * * * /usr/local/bin/camera.sh" > $suncron
echo "00 04 * * * /usr/local/bin/deleteold.sh" >> $suncron
echo "00 03 * * * /usr/local/bin/sun.sh" >> $suncron

echo $sunrise_m $sunrise_h "* * * /usr/local/bin/daynight.sh day"   >> $suncron
echo $sunset_m  $sunset_h  "* * * /usr/local/bin/daynight.sh night" >> $suncron

echo "@reboot /usr/local/bin/initgpio.sh" >> $suncron

/bin/sync
/usr/bin/crontab $suncron

6. 通过GPIO切换摄像头日夜模式的脚本 daynight.sh

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
#!/bin/bash

gpiodir="/sys/class/gpio"
gpio_irled=5    # 0 - on,  1 - off
gpio_led=6  # 0 - on,  1 - off
gpio_mode=17    # 0 - day, 1 - night

gpio_init ()
{
  echo $1 $2
  echo $1 > $gpiodir /export
  sleep 1
  echo out > $gpiodir/gpio $1/direction
  echo $2 > $gpiodir/gpio$1 /value
}

gpio_ctrl ()
{
  echo gpio$1 $2
  if [ ! -f $gpiodir/gpio$1/value ];then
    gpio_init $1 $2
  else
    echo $2 > $gpiodir/gpio $1/value
  fi
}

#init_gpio 17 0

ir_control ()
{
  if [ "$1" = "night" ]; then
    echo "=== night mode ==="
    gpio_ctrl $gpio_irled 0
    gpio_ctrl $gpio_mode 1
  else
    echo "=== day mode ==="
    gpio_ctrl $gpio_irled 1
    gpio_ctrl $gpio_mode 0
  fi
}

# turn off led light
gpio_ctrl $gpio_led 1

ir_control $1

7. 派重启后恢复摄像头日夜模式的脚本 initgpio.sh

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
#!/bin/bash

sunfile="/home/pi/sun.txt"

date_day=$(date +%Y-%m-%d)

sun=$(cat $sunfile | grep $date_day)
sunrise=$(echo $sun | awk '{print $2}')
sunset=$(echo $sun | awk '{print $3}')

#echo $sunrise $sunset

sunrise_h=$(echo $sunrise | awk -F ':' '{print $1}')
sunrise_m=$(echo $sunrise | awk -F ':' '{print $2}')

if [ $(($sunrise_m % 2)) -eq 0 ];then
  sunrise_m=$(printf "%02d" $((sunrise_m+1)))
fi

sunset_h=$(echo $sunset | awk -F ':' '{print $1}')
sunset_m=$(echo $sunset | awk -F ':' '{print $2}')

if [ $(($sunset_m % 2)) -eq 0 ];then
  sunset_m=$(printf "%02d" $((sunset_m+1)))
fi

sunrise="$sunrise_h:$sunrise_m"
sunset="$sunset_h:$sunset_m"

currenttime=$(date +%H:%M)

if [[ "$currenttime" > "$sunrise" ]] && [[ "$currenttime" < "$sunset" ]];then
  /usr/local/bin /daynight.sh day
else
  /usr/local/bin/daynight.sh night
fi

在异地另一个树莓派上远程获取照片

1. 定时任务

1
2
pi@raspberrypi:~ $ crontab -l
0 1 * * * /usr/local/bin/getphoto.sh

2. 每天自动获取远程派摄像头内照片的脚本 getphoto.sh

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
#!/bin/bash

target_dir="/opt/picamera"
mailto="xxx@163.com"

date_day=$(date -I -d "$(date +%Y-%m-%d) -1 days")

if [ ! -d $target_dir ];then
  mkdir -p $target_dir
fi

cmd="rsync -av -e 'ssh -p[PORT]' pi@[IP]:/home/pi/Pictures/$date_day $target_dir"

n=0
timeout=50
until [ $n -ge $timeout ]
do
  eval $cmd && break
  n=$[$n+ 1]
  sleep 5
done

file_num=$(ls $target_dir/$date_day | wc -l)
#echo $file_num

if [ "$file_num" = "1440" ];then
  title= "Get photos successfully [$file_num]"
else
  title="Failed to get photos [$file_num]"
fi

photo_num=$(ls $target_dir/$date_day/*.jpg | wc -l)
txt_num=$(ls $target_dir/$date_day/*.txt | wc -l)

msg="photo:$photo_num, txt:$txt_num, retry:$n"

#echo $title : $msg
echo "$msg" | mutt -s "$title" -- $mailto

邮件功能需要安装

1
sudo apt install msmtp mutt

相关配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pi@raspberrypi:~ $ cat ~/.msmtprc
account default
host smtp.163.com
from xxx@163.com
auth login
user xxx@163.com
password [PASSWORD]
logfile ~/.msmtp.log

pi@raspberrypi:~ $ cat ~/.muttrc
set sendmail="/usr/bin/msmtp"
set use_from=yes
set realname="rpi-camera"
set from="xxx@163.com"
set envelope_from=yes
set editor="vim -nw"

3. 在树莓派上用ffmpeg将照片合成视频的命令

1
ffmpeg -framerate 25 -pattern_type glob -i '*.jpg' -c:v libx264 -profile:v high -crf 20 -pix_fmt yuv420p output.mp4

https://blog.csdn.net/A632189007/article/details/78779920
https://www.cnblogs.com/xiao987334176/p/12691686.html

Portainer介绍

PortainerDocker的图形化管理工具,提供状态显示面板、应用模板快速部署、容器镜像网络数据卷的基本操作(包括上传下载镜像,创建容器等操作)、事件日志显示、容器控制台操作、Swarm集群和服务等集中管理和操作、登录用户管理和控制等功能。功能十分全面,基本能满足中小型单位对容器管理的全部需求。

下载Portainer镜像

1
2
# 查询当前有哪些Portainer镜像
docker search portainer

上图就是查询出来的有下载量的portainer镜像,我们下载第一个镜像:docker.io/portainer/portainer

1
2
# 下载镜像
docker pull docker.io/portainer/portainer

单机版运行

如果仅有一个docker宿主机,则可使用单机版运行,Portainer单机版运行十分简单,只需要一条语句即可启动容器,来管理该机器上的docker镜像、容器等数据。

1
2
3
4
5
docker run -d -p 9000:9000 \
--restart=always \
-v /var/run/docker.sock:/var/run/docker.sock \
--name prtainer-test \
docker.io/portainer/portainer

该语句用宿主机9000端口关联容器中的9000端口,并给容器起名为portainer-test。执行完该命令之后,使用该机器IP:PORT即可访问Portainer

访问方式:http://IP:9000

首次登陆需要注册用户,给admin用户设置密码:

单机版这里选择local即可,选择完毕,点击Connect即可连接到本地docker

注意:该页面上有提示需要挂载本地 /var/run/docker.socker与容器内的/var/run/docker.socker连接。因此,在启动时必须指定该挂载文件。

首页:

容器列表:

点击容器列表中的容器名Name,即可查看容器详情:

并且在容器详情页可以使用该容器创建镜像:

镜像列表(在镜像列表可以直接pull一个镜像,可以从远程pull,也可以从私有库中pull。从私有库中pull,需要将私有库的地址提前进行配置,这个在后面会说):

点击镜像ID,即可查看镜像详情信息,在详情信息页面,除了镜像的一些信息外,还可以对该镜像进行打标签tag操作,然后将镜像push到远程仓库或者私有仓库中。

仓库管理页面(该界面可以查看配置的镜像仓库列表,同时可以添加仓库,添加成功之后,即可在image镜像页面进行pullpush操作。):

添加镜像仓库:

Portainer中还有一些别的操作,比如权限管理、网络管理等等,可以安装上进行了解学习。

集群运行

更多的情况下,我们会有一个docker集群,可能有几台机器,也可能有几十台机器,因此,进行集群管理就十分重要了,Portainer也支持集群管理,Portainer可以和Swarm一起来进行集群管理操作。这里我首先搭建了一个Swarm

Swarm集群的搭建方法可参考这篇文章:通过Swarm搭建Docker集群

portainer集群方式启动(这里我喜欢通过简单启动的方式,然后在界面上进行节点的添加):

1
docker run -d -p 9000:9000 --restart=always --name prtainer-test docker.io/portainer/portainer

启动Portainer之后,首页还是给admin用户设置密码(这里和单机启动一样)。接下来是设置节点了,如下图:

这里我们选择Remote这个模块,下面会要求添加一个名字以及节点URL,名字可以自取,只要能够理解即可,Endpoint URLSwarm集群中设置的节点URL,比如我机器IP是10.0.11.152,监听的端口是默认的2375,则这里的URL就写:10.0.11.152:2375

如果是集群方式启动,建议portainer安装启动在Swarm管理节点,并且首次设置Endpoint URL时设置管理节点的URL。

填写完毕点击Connect即可进入管理页面。在管理页面左上角会显示管理的集群节点列表:

想要查看那个节点的信息,则点击节点即可。镜像、容器操作与单机模式下基本一样。这里只需要说下节点添加。

点击导航栏Endpoints进入节点列表页面:

从上图中一目了然就应该知道如何添加节点了,需要填写一个名字NameEndpoint URL以及节点IP,就可以添加一个集群节点了,十分简单。

汉化

创建目录,并解压文件

1
2
3
4
5
6
mkdir -p /data/portainer/data /data/portainer/public
cd cd /data/portainer wget https://dl.quchao.net/Soft/Portainer-CN.zip
unzip Portainer-CN.zip -d public

# 复制到docker里
docker cp . 48bb9525bad3:/public

OK,Portainer的基本操作就这么多,具体的操作步骤还需要大家自己去学习理解。

https://www.cnblogs.com/linhuiy/p/12668535.html

前言

在后端Api的开发过程中,无法避免的会遇到接口迭代的过程,如何保证新老接口的共存和接口的向前的兼容呢,这时候就需要对Api进行版本的控制,那如何优雅的控制Api的版本呢?

开始

Microsoft.AspNetCore.Mvc.Versioning 是一个微软官方推出的一个用于管理Api版本的包,配置简单,功能强大。 github地址.

新建一个WebApi项目并通过命令引用包。

Install-Package Microsoft.AspNetCore.Mvc.Versioning

最新版本已经支持Core3.1

项目结构如下

StartupConfigureServices 中增加一下配置。

1
2
3
4
5
6
7
services.AddApiVersioning(options =>
{
options.ReportApiVersions = true;
options.AssumeDefaultVersionWhenUnspecified = true;
options.DefaultApiVersion = new ApiVersion(1, 0);
});

  • ReportApiVersions:是否在请求头中返回受支持的版本信息。
  • AssumeDefaultVersionWhenUnspecified:请求没有指明版本的情况下是否使用默认的版本。
  • DefaultApiVersion:默认的版本号。

通过QueryString进行版本控制

分别在两个不同的Controller中添加一个获取版本信息的接口

1
2
3
4
5
6
7
8
9
10
11
12
namespace version.Controllers.v1
{
[ApiVersion("1.0")]
[ApiController]
[Route("api/[controller]")]
public class ValuesController : Controller
{
[HttpGet("version")]
public string Version() => (HttpContext.GetRequestedApiVersion().ToString());
}
}

1
2
3
4
5
6
7
8
9
10
11
12
namespace version.Controllers.v2
{
[ApiVersion("2.0")]
[ApiController]
[Route("api/[controller]")]
public class ValuesController : Controller
{
[HttpGet("version")]
public string Version() => (HttpContext.GetRequestedApiVersion().ToString());
}
}

HttpContext.GetRequestedApiVersion().ToString() 是用于获取请求接口的版本信息。

我们通过postman来请求这两个接口当我们没有给到具体请求哪个版本的时候会根据在ConfigureServices中配置的默认版本去执行。

指定版本请求结果

在响应头中会显示当前支持的所有的Api版本

通过URL Path进行版本控制

一般在Api开发中不会去QueryString的方式去进行版本控制,而是使用URL路径段的方式来控制版本。

修改两个Controller中的代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
namespace version.Controllers.v1
{
[ApiVersion("1.0")]
[ApiController]
[Route("api/v{version:ApiVersion}/[controller]")]
public class ValuesController : Controller
{
[HttpGet("version")]
public string Version() => (HttpContext.GetRequestedApiVersion().ToString());
}
}

1
2
3
4
5
6
7
8
9
10
11
12
namespace version.Controllers.v2
{
[ApiVersion("2.0")]
[ApiController]
[Route("api/v{version:ApiVersion}/[controller]")]
public class ValuesController : Controller
{
[HttpGet("version")]
public string Version() => (HttpContext.GetRequestedApiVersion().ToString());
}
}

通过postman进行测试



可以看到当我们使用指定的版本是可以正常访问的时候,但是如果我们去掉了Api版本号就会抛出404,并不能像QueryString一样调用默认的Api版本,因为URL Path的方式不允许隐式匹配设置的默认Api版本。所以必须申明所有的Api版本。且在请求Api同时必须带上Api版本号。

通过Media Type进行版本控制

我们还可以使用content-type来实现版本的控制

修改ConfigureServices中的配置

1
2
3
4
5
6
7
8
services.AddApiVersioning(options =>
{
options.ApiVersionReader = new MediaTypeApiVersionReader();
options.AssumeDefaultVersionWhenUnspecified = true;
options.ApiVersionSelector = new CurrentImplementationApiVersionSelector(options);

});

CurrentImplementationApiVersionSelector 如果没有在content-type中传递Api版本好,将默认匹配最新的Api版本

分别修改两个Controller

1
2
3
4
5
6
7
8
9
10
11
12
namespace version.Controllers.v1
{
[ApiVersion("1.0")]
[ApiController]
[Route("api/[controller]")]
public class ValuesController : Controller
{
[HttpGet("version")]
public string Version() => (HttpContext.GetRequestedApiVersion().ToString());
}
}

1
2
3
4
5
6
7
8
9
10
11
12
namespace version.Controllers.v2
{
[ApiVersion("2.0")]
[ApiController]
[Route("api/[controller]")]
public class ValuesController : Controller
{
[HttpGet("version")]
public string Version() => (HttpContext.GetRequestedApiVersion().ToString());
}
}

使用Postman测试

通过自定义Headers进行版本控制

修改ConfigureServices中的配置

1
2
3
4
5
6
7
8
9
services.AddControllers();
services.AddApiVersioning(options =>
{
options.ReportApiVersions = true;
options.ApiVersionReader = new HeaderApiVersionReader("api_version");
options.AssumeDefaultVersionWhenUnspecified = true;
options.DefaultApiVersion = new ApiVersion(1, 0);
});

api_version 是你Headers中Key的名字。

使用Postman测试

特性

当哪个Api版本不在更新,就需要弃用掉这个版本。当Deprecated值为true时说明该Api版本已经已经弃用,但是弃用不代表不能请求。只是会在响应头中告知次版本已经已经弃用。

1
2
3
4
5
6
7
8
9
10
11
12
namespace version.Controllers.v1
{
[ApiVersion("1.0",Deprecated= true)]
[ApiController]
[Route("api/[controller]")]
public class ValuesController : Controller
{
[HttpGet("version")]
public string Version() => (HttpContext.GetRequestedApiVersion().ToString());
}
}

20200409164902

项目总有一些功能是不需要版本的控制,所以我们希望它不受版本控制。可以添加[ApiVersionNeutral]特性使Api支持版本控制。

1
2
3
4
5
6
7
8
9
10
11
12
namespace version.Controllers.v1
{
[ApiVersionNeutral]
[ApiController]
[Route("api/[controller]")]
public class ValuesController : Controller
{
[HttpGet("version")]
public string Version() => (HttpContext.GetRequestedApiVersion().ToString());
}
}

MapToApiVersion 可以将单个Api归类于任何版本。在一个Controller中可以存在多个版本的Api。我们可以配合Deprecated来灵活的控制我们的Api。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace version.Controllers.v1
{
[ApiVersion("3.0")]
[ApiVersion("1.0",Deprecated= true)]
[ApiController]
[Route("api/v{version:ApiVersion}/[controller]")]
public class ValuesController : Controller
{
[HttpGet("version"), MapToApiVersion("1.0")]
public string Version() => (HttpContext.GetRequestedApiVersion().ToString());

[HttpGet("version3"), MapToApiVersion("3.0")]
public string Version3() => (HttpContext.GetRequestedApiVersion().ToString());
}
}

通过postman测试一下。

总结

可以看到Microsoft.AspNetCore.Mvc.Versioning功能还能强大的,基本满足了大部分的需求,还有一些功能可能没有在本文中涉及到,可以去这里.翻阅。