この記事は、これまでラズベリーを試したことがないが、今が適切な時期であると信じている人に役立ちます。
こんにちは、Habr!形容詞「スマート」を技術的なデバイスに帰する傾向は、(もちろん、使用回数の点で)最高潮に達したようです。さらに、IT分野以外の私の知人のほとんどは、すべての自尊心のあるプログラマーが、壁の代わりに巨大なサイズのサーバースタンドを備えたブロック全体の「最もスマートな」家に住んでいるとまだ素朴に信じています。同じ人間のプログラマーがボストンダイナミクスの賢い犬を散歩させます。これらの現代的な基準に追いつくために、私の友人と私は、学校では回路とロボットの設計が私たちを迂回したので、「スマート」であるがシンプルなものを個人的に作成することにしました。
, , aka . , , , .
:
Raspberry Pi, , . MQTT Raspberry Data Analyzer. , - Object Storage. DB . REST API . , .
.
Raspberry Pi
, - , Raspberry Pi , , - — ( ). , - .
:
Raspberry Pi 4
SD- ( Raspberry). , SD- , / Raspberry ( ).
PIR- HC-SR501,
microHDMI HDMI
«-».
5- OV5647
– 5V/1A.
Raspberry . . Raspberry Pi OS Full – . , , IDE Python (Thonny Python IDE), Java (BlueJ). . Raspberry GPIO , . , (, ) .
«-» , . 5- (5V ) , ( GND ) , , , , , GPIO + - . , GPIO26.
python-, . Raspberry.
PIR-:
from gpiozero import MotionSensor
from datetime import timezone
pir = MotionSensor(26)
while True:
pir.wait_for_motion()
dt = datetime.datetime.utcnow()
st = dt.strftime('%d.%m.%Y %H:%M:%S')
print("Motion Detected at : " + st)
, Wi-Fi , false-positive — . , , , . , , :
. .
, ( ), . , UUID. , , device_uuid. — .
import uuid
def getDeviceId():
try:
deviceUUIDFile = open("device_uuid", "r")
deviceUUID = deviceUUIDFile.read()
print("Device UUID : " + deviceUUID)
return deviceUUID
except FileNotFoundError:
print("Configuring new UUID for this device...")
deviceUUIDFile = open("device_uuid", "w")
deviceUUID = str(uuid.uuid4())
print("Device UUID : " + deviceUUID)
deviceUUIDFile.write(deviceUUID)
return deviceUUID
MQTT :
import paho.mqtt.client as mqtt
mqttClient = mqtt.Client("P1")
mqttClient.loop_start() #
mqttClient.connect(BROKER_ADDRESS)
while-true json :
{
"device_id": "123e4567-e89b-12d3-a456-426614174000",
"id": "133d4167-18ds-11d1-b446-826314134110",
"place": "office_room",
"filename": "133d4167-18ds-11d1-b446-826314134110_alarm.mp4",
"type": "detected_motion",
"occurred_at": "01.01.2021 20:19:56»
}
MQTT :
MP4_VIDEO_EXT = '.mp4'
alarmUUID = str(uuid.uuid4())
filename = '{}_alarm'.format(alarmUUID)
message = json.dumps({
'device_id': deviceUUID,
'id': alarmUUID,
'place': 'office_room',
'filename': filename + MP4_VIDEO_EXT,
'type': 'detected_motion',
'occurred_at': st
}, sort_keys=True)
mqttClient.publish("raspberry/main", message)
. .
import picamera
VIDEO_TIME_SEC = 15
FILE_DIR = 'snapshots/'
MP4_VIDEO_EXT = '.mp4'
H264_VIDEO_EXT = '.h264'
camera = picamera.PiCamera()
camera.resolution = 640,480
def record(filename):
h264_file = filename + H264_VIDEO_EXT
print("Recording : " + h264_file)
camera.start_recording(h264_file)
camera.wait_recording(VIDEO_TIME_SEC)
camera.stop_recording()
print("Recorded")
# mp4
mp4_file = filename + MP4_VIDEO_EXT
command = "MP4Box -add " + h264_file + " " + mp4_file
print("Converting from .h264 to mp4")
call([command], shell=True)
print(«Converted")
, MinIO. MinIO, . MinIO .
from minio import Minio
from minio.error import S3Error
MINIO_HOST = «0.0.0.0:443»
BUCKET_NAME = ‘raspberrycamera’
client = Minio(
MINIO_HOST,
access_key="minio",
secret_key="minio123",
secure=False
)
found = client.bucket_exists(BUCKET_NAME)
if not found:
client.make_bucket(BUCKET_NAME)
else:
print("Bucket {} already exists».format(BUCKET_NAME)")
def sendToMinio(filename):
try:
print("Sending to minio")
client.fput_object(
BUCKET_NAME, filename, FILE_DIR + filename
)
print("Video has been sent")
except Exception as e:
print(e)
print("Couldn't send to Minio»)
– . Rasbperry . . Docker , docker-compose:
version: '3.1'
services:
app:
restart: on-failure
build:
context: .
dockerfile: Dockerfile
environment:
POSTGRES_URL: "jdbc:postgresql://database:5432/alarms"
POSTGRES_USER: "postgres"
POSTGRES_PASSWORD: "changeme"
MQTT_BROKER_HOST: "mosquitto"
MQTT_BROKER_PORT: "1883"
MQTT_BROKER_TOPICS: "raspberry/main"
MINIO_HOST: "https://minio"
MINIO_PORT: "443"
MINIO_ACCESS_KEY: "minio"
MINIO_SECRET_KEY: "minio123"
MINIO_BUCKET: "raspberrycamera"
ports:
- "8080:8080"
depends_on:
- database
links:
- database
database:
container_name: database
image: postgres
ports:
- "5432:5432"
environment:
- POSTGRES_PASSWORD=changeme
- POSTGRES_USER=postgres
- POSTGRES_DB=alarms
mosquitto:
image: eclipse-mosquitto
ports:
- 1883:1883
- 8883:8883
restart: unless-stopped
minio:
image: minio/minio
command: server --address ":443" /data
ports:
- "443:443"
environment:
MINIO_ACCESS_KEY: "minio"
MINIO_SECRET_KEY: "minio123"
volumes:
- /tmp/minio/data:/data
- /tmp/.minio:/root/.minio
MQTT-
.
MQTT-. MQTT — - TCP/IP, — MQTT- . MQTT . -, , , , , , – ( , Raspberry ). -, . , , – , , ( , ). MQTT- open-source Mosquitto.
MinIO
, - . , , . open-source MinIO. , , - .
bucket’ ( ):
, . Java Spring . MQTT- :
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-mqtt</artifactId>
<version>5.4.2</version>
</dependency>
:
@Configuration
public class MqttConfiguration {
@Value("${mqtt.broker.host}")
private String brokerHost;
@Value("${mqtt.broker.port}")
private String brokerPort;
@Value("${mqtt.broker.topics}")
private String topics;
@Bean
public MessageChannel mqttInputChannel() {
return new DirectChannel();
}
@Bean
public MessageProducer inbound() {
String[] parsedTopics = parseTopics();
MqttPahoMessageDrivenChannelAdapter adapter =
new MqttPahoMessageDrivenChannelAdapter(
"tcp://" + brokerHost + ":" + brokerPort,
UUID.randomUUID().toString(),
parsedTopics);
adapter.setCompletionTimeout(5000);
adapter.setConverter(new DefaultPahoMessageConverter());
adapter.setQos(1);
adapter.setOutputChannel(mqttInputChannel());
return adapter;
}
private String[] parseTopics() {
return topics.split(",");
}
@Bean
@ServiceActivator(inputChannel = "mqttInputChannel")
public MessageHandler handler() {
return new MqttMessageHandler();
}
}
MqttMessageHandler:
public class MqttMessageHandler implements MessageHandler {
@Autowired
private AlarmRepository alarmRepository;
@Autowired
private DeviceRepository deviceRepository;
private Gson gson = new GsonBuilder().create();
private DateFormat sdf = new SimpleDateFormat("dd.MM.yyyy H:m:s");
@Override
public void handleMessage(Message<?> message) throws MessagingException {
String payload = (String) message.getPayload();
Map<String, String> parsedMessage = (Map<String, String>) gson.fromJson(payload, Map.class);
long occurredAt = 0L;
try {
occurredAt = sdf.parse(parsedMessage.get("occurred_at")).getTime();
} catch (ParseException e) {
e.printStackTrace();
return;
}
UUID deviceID = UUID.fromString(parsedMessage.get("device_id"));
Device device = new Device(deviceID, "", new Date().getTime(), occurredAt);
deviceRepository.saveAndFlush(device);
Alarm alarm = new Alarm(
UUID.fromString(parsedMessage.get("id")),
parsedMessage.get("place"),
parsedMessage.get("filename"),
parsedMessage.get("type"),
device,
occurredAt,
false
);
alarmRepository.saveAndFlush(alarm);
}
}
:
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.0.3</version>
</dependency>
MinIO:
@Configuration
public class MinioConfiguration {
@Value("${minio.host}")
private String host;
@Value("${minio.port}")
private String port;
@Value("${minio.access.key}")
private String accessKey;
@Value("${minio.secret.key}")
private String secretKey;
@Value("${minio.bucket}")
private String bucket;
@Bean
public MinioClient getClient() {
return MinioClient.builder()
.endpoint(host, Integer.parseInt(port), false)
.credentials(accessKey, secretKey)
.build();
}
@Bean
public MinioFileManager getManager(MinioClient client) {
return new MinioFileManager(client);
}
}
, ?
MinioFileManager — , .
MinIO — - HTTP .
HTTP video streaming
-.
, , . Range. , : bytes=0-1000000. «» HTTP = 203 (Partial content). , , . , 200. :
Content-Type. . video/mp4
Accept-Ranges. , , , — : Accept-Ranges: bytes.
Content-Length. , -. , ( ).
Content-Range. , , : Content-Range: bytes 1000-15000/250000.
. readFile MinIO . Range slice , , .
public class MinioFileManager implements FileManager {
@Value("${minio.bucket}")
private String bucket;
private final MinioClient client;
public MinioFileManager(MinioClient mc) {
client = mc;
}
public Video getVideo(String filename, VideoRange range) throws Exception {
byte[] data = readFile(filename);
Video video = new Video(data);
return slice(video, range);
}
private Video slice(Video video, VideoRange range) {
if (range.wholeVideo()) {
return video;
}
int finalSize;
if (video.shorterThan(range.getEnd()) || range.withNoEnd()) {
finalSize = video.getSize() - (int) range.getStart();
} else {
finalSize = (int) range.difference();
}
byte[] result = new byte[finalSize];
System.arraycopy(video.asArray(), (int) range.getStart(), result, 0, result.length);
return new Video(result, false, video.getSize());
}
private byte[] readFile(String filename) throws Exception {
try (InputStream is = client.getObject(
GetObjectArgs.builder()
.bucket(bucket)
.object(filename)
.build())) {
ByteArrayOutputStream bufferedOutputStream = new ByteArrayOutputStream();
byte[] data = new byte[1024];
int nRead;
while ((nRead = is.read(data, 0, data.length)) != -1) {
bufferedOutputStream.write(data, 0, nRead);
}
int resultLength = bufferedOutputStream.size();
bufferedOutputStream.flush();
byte[] result = new byte[resultLength];
System.arraycopy(bufferedOutputStream.toByteArray(), (int) 0, result, 0, result.length);
return result;
}
}
public void removeFile(String filename) {
List<DeleteObject> objects = new LinkedList<>();
objects.add(new DeleteObject(filename));
Iterable<Result<DeleteError>> results =
client.removeObjects(
RemoveObjectsArgs.builder().bucket(bucket).objects(objects).build());
try {
for (Result<DeleteError> result : results) {
DeleteError error = result.get();
System.out.println(
"Error in deleting object " + error.objectName() + "; " + error.message());
}
} catch (Exception e) {
e.printStackTrace();
}
}
, . VideoResponseFactory, : -, .
public class VideoResponseFactory {
private final String contentType = "video/mp4";
private final String CONTENT_TYPE = "Content-Type";
private final String ACCEPT_RANGES = "Accept-Ranges";
private final String CONTENT_LENGTH = "Content-length";
private final String CONTENT_RANGE = "Content-Range";
private ResponseEntity<byte[]> toPartialResponse(Video video, String stringRanges) {
long[] ranges = parseRanges(stringRanges);
long start = ranges[0];
long end = ranges[1];
long rangeEnd = end;
if (end == -1) {
rangeEnd = video.originalSize() - 1;
}
return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT)
.header(CONTENT_TYPE, contentType)
.header(ACCEPT_RANGES, "bytes")
.header(CONTENT_LENGTH, String.valueOf(video.getSize()))
.header(CONTENT_RANGE, "bytes" + " " + start + "-" + rangeEnd + "/" + video.originalSize())
.body(video.asArray());
}
private long[] parseRanges(String stringRanges) {
String[] ranges = stringRanges.split("-");
long start = Long.parseLong(ranges[0].substring(6));
long end;
if (ranges.length > 1) {
end = Long.parseLong(ranges[1]);
} else {
end = -1;
}
return new long[] {start, end};
}
public ResponseEntity<byte[]> toResponse(Video video, String ranges) {
if (video.isFull()) {
return toFullResponse(video.asArray());
} else {
return toPartialResponse(video, ranges);
}
}
private ResponseEntity<byte[]> toFullResponse(byte[] video) {
return ResponseEntity.status(HttpStatus.OK)
.header(CONTENT_TYPE, contentType)
.header(CONTENT_LENGTH, String.valueOf(video.length))
.header(ACCEPT_RANGES, "bytes")
.body(video);
}
}
:
@RestController
@RequestMapping("/video")
public class VideoController {
private FileManager fm;
private AlarmRepository repository;
private VideoResponseFactory rf;
public VideoController(MinioFileManager manager, AlarmRepository repo, VideoResponseFactory rf) {
fm = manager;
repository = repo;
this.rf = rf;
}
@GetMapping("/stream/{filename}")
public Mono<ResponseEntity<byte[]>> streamVideo(@RequestHeader(value = "Range", required = false) String httpRangeList,
@PathVariable("filename") String filename) throws Exception {
Video video = fm.getVideo(filename, VideoRange.of(httpRangeList));
ResponseEntity<byte[]> response = rf.toResponse(video, httpRangeList);
Optional<Alarm> stored = repository.findAlarmByFilename(filename);
if (stored.isPresent()) {
Alarm alarm = stored.get();
alarm.seen();
repository.saveAndFlush(alarm);
}
return Mono.just(response);
}
}
IoT-, , . TODO- :
.
. : Wi-Fi, MinIO, , .
.
Stay tuned!