Primera experiencia con Raspberry Pi o microservicios para el hogar

Este artículo será útil para aquellos que nunca antes han experimentado con Raspberry, pero creen que este es el momento adecuado.





¡Hola, Habr! La tendencia a atribuir el epíteto “inteligente” a cualquier dispositivo técnico parece haber llegado a su punto culminante (en cuanto al número de usos, por supuesto). Además, la mayoría de mis conocidos que no pertenecen a la esfera de TI todavía creen ingenuamente que todo programador que se precie vive en la casa "más inteligente" de todo el bloque, que tiene soportes de servidor de tamaño gigante en lugar de paredes, y en su tiempo libre mismo programador humano pasea al perro inteligente de Boston Dynamics. Para mantenernos al día con estos estándares modernos, mi amigo y yo decidimos crear personalmente algo "inteligente", pero simple, ya que en la escuela los circuitos y el diseño de robots nos pasaban por alto.





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





Es difícil fotografiar así de hermoso y sobre un fondo blanco.
.

. . 





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