First experience with Raspberry Pi or microservices for home

This article will be useful to those who have never experimented with Raspberry before, but believe that this is the right time.





Hello, Habr! The tendency to ascribe the epithet “smart” to any technical device seems to have reached its climax (in terms of the amount of use, of course). In addition, most of my acquaintances not from the IT sphere still naively believe that every self-respecting programmer lives in the "smartest" house in the entire block, which has giant-sized server stands instead of walls, and in his spare time the same human programmer walks the smart dog from Boston Dynamics. In order to keep up with these modern standards, my friend and I decided to personally create something "smart", but simple, since at school circuitry and design of robots bypassed us.





, , 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 — . , , , . , , :





It's difficult to shoot this beautifully and against a white background.
.

. . 





, ( ), . , 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