Artikel ini akan bermanfaat bagi mereka yang belum pernah bereksperimen dengan Raspberry sebelumnya, tetapi percayalah bahwa ini adalah waktu yang tepat.
Halo, Habr! Kecenderungan untuk mengaitkan julukan "pintar" dengan perangkat teknis apa pun tampaknya telah mencapai klimaksnya (dalam hal jumlah penggunaan, tentu saja). Selain itu, sebagian besar kenalan saya yang bukan dari bidang TI masih secara naif percaya bahwa setiap programmer yang menghargai diri sendiri tinggal di rumah "paling cerdas" di seluruh blok, yang memiliki penyangga server berukuran raksasa, bukan dinding, dan di waktu luangnya, programmer manusia yang sama menuntun anjing pintar dari Boston Dynamics. Untuk mengikuti standar modern ini, teman saya dan saya memutuskan untuk secara pribadi membuat sesuatu yang "pintar", tetapi sederhana, karena sirkuit sekolah dan desain robot melewati kami.
, , 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!