RaspberryPiまたは家庭用マイクロサービスの最初の経験

この記事は、これまでラズベリーを試したことがないが、今が適切な時期であると信じている人に役立ちます。





こんにちは、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- :





  1. .





  2. . : Wi-Fi, MinIO, , .





.





Stay tuned! 








All Articles