Commit acda1e7f authored by Artem Sidyakin's avatar Artem Sidyakin

Camera control

- backend class for camera control (via Python script, no time for proper I2C)
- checking/creating the folder for shots
- properties to control some general settings
- proper handling upside down camera
- tabs code moved to dedicated files
- message box for errors
- deployment actions
parent 8acf85ca
import QtQuick 2.11
import QtQuick.Window 2.11
import QtQuick.Controls 2.4
import QtQuick.Layouts 1.11
Window {
id: dialog
// both title and message text properties are available to be set from "outside"
property string title
property string textMain
title: dialog.title
modality: Qt.WindowModal
width: 400
minimumWidth: width
maximumWidth: width
height: 200
minimumHeight: height
maximumHeight: height
Rectangle {
anchors.fill: parent
//color: Styles.regionBackground
//border.color: Styles.mainBackground
//border.width: 3
ColumnLayout {
anchors.fill: parent
spacing: 0
ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
// Layout.topMargin: 10
// Layout.leftMargin: 15
// Layout.rightMargin: 15
TextArea {
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: dialog.textMain
font.family: "Courier New"
font.pixelSize: 16
wrapMode: Text.WordWrap
}
}
Button {
id: btn
// Layout.leftMargin: 3
// Layout.rightMargin: 3
Layout.fillWidth: true
text: "Close"
onClicked: { dialog.close(); }
}
}
}
}
import QtQuick 2.11
import QtQuick.Window 2.11
import QtQuick.Layouts 1.11
import QtQuick.Controls 2.4
import QtMultimedia 5.11
Item {
GridLayout {
anchors.fill: parent
rows: 2
columns: 2
rowSpacing: 0
columnSpacing: 0
Rectangle {
Layout.row: 0
Layout.column: 0
Layout.fillWidth: true
Layout.fillHeight: true
Text {
id: cameraStatus
anchors.centerIn: parent
width: parent.width
text: qsTr("loading camera...")
font.pixelSize: 40
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
}
// Rectangle {
// anchors.fill: parent
// border.width: 5
// border.color: "gray"
VideoOutput {
anchors.fill: parent
orientation: cameraUpsideDown ? 180 : 0
fillMode: VideoOutput.PreserveAspectCrop
source: camera
Camera {
id: camera
//cameraState: tabCamera.checked ? Camera.ActiveState : Camera.LoadedState
viewfinder.resolution: Qt.size(848, 480) // picture quality
metaData.orientation: cameraUpsideDown ? 180 : 0
imageCapture {
onCaptureFailed: {
dialogError.textMain = "Some error taking a picture\n" + message;
dialogError.show();
}
onImageCaptured: {
//console.log("photo has been captured")
}
onImageSaved: {
//console.log("photo has been saved:", camera.imageCapture.capturedImagePath)
}
}
onError: {
cameraStatus.text = qsTr("Error: ") + errorString;
console.log(errorCode, errorString);
}
Component.onCompleted: {
//console.log(qsTr("camera orientation:"), camera.orientation);
//console.log(qsTr("camera state:"), camera.cameraState);
//console.log(qsTr("camera status:"), camera.cameraStatus);
//console.log(qsTr("camera supported resolutions:"), imageCapture.supportedResolutions);
// var supRezes = camera.supportedViewfinderResolutions();
// for (var rez in supRezes)
// {
// console.log(supRezes[rez].width, "x", supRezes[rez].height);
// }
}
}
}
// }
}
Rectangle {
Layout.row: 1
Layout.column: 0
Layout.fillWidth: true
Layout.fillHeight: true
Layout.maximumHeight: root.maxSize
color: root.sliderColor
Slider {
id: sliderPan
anchors.fill: parent
orientation: Qt.Horizontal
from: root.maxValue
value: 0
stepSize: 1
to: -root.maxValue
onPressedChanged: {
if (!pressed)
{
//console.log("new pan value:", sliderPan.value)
backend.movePanTilt(basePath, sliderPan.value, sliderTilt.value)
}
}
// onValueChanged: {
// console.log("new pan value:", sliderPan.value)
// backend.movePanTilt(basePath, sliderPan.value, sliderTilt.value)
// }
}
}
Rectangle {
Layout.row: 0
Layout.column: 1
Layout.fillWidth: true
Layout.fillHeight: true
Layout.maximumWidth: root.maxSize
color: root.sliderColor
Slider {
id: sliderTilt
anchors.fill: parent
orientation: Qt.Vertical
from: root.maxValue
value: 0
stepSize: 1
to: -root.maxValue
onPressedChanged: {
if (!pressed)
{
//console.log("new tilt value:", sliderTilt.value)
backend.movePanTilt(basePath, sliderPan.value, sliderTilt.value)
}
}
// onValueChanged: {
// console.log("new tilt value:", sliderTilt.value)
// backend.movePanTilt(basePath, sliderPan.value, sliderTilt.value)
// }
}
}
Button {
Layout.row: 1
Layout.column: 1
Layout.fillWidth: true
Layout.fillHeight: true
Layout.maximumWidth: root.maxSize
Layout.maximumHeight: root.maxSize
scale: hovered ? (pressed ? 0.9 : 1.1) : 1
background: Rectangle {
color: "transparent"
}
Image {
id: cameraIcon
anchors.fill: parent
source: "/img/camera.png"
}
onClicked: {
camera.imageCapture.captureToLocation(basePath + "shots/" + getCurrentDateTime() + ".jpg");
}
}
}
}
import QtQuick 2.11
import QtQuick.Window 2.11
import QtQuick.Layouts 1.11
import QtQuick.Controls 2.4
import Qt.labs.folderlistmodel 2.11
Item {
RowLayout {
anchors.fill: parent
//anchors.topMargin: 15
spacing: 0
Rectangle {
Layout.preferredWidth: parent.width * 0.3
Layout.fillHeight: true
ListView {
id: files
anchors.fill: parent
FolderListModel {
id: folderModel
folder: "file:" + basePath + "shots/"
nameFilters: ["*.jpg"]
}
model: folderModel
delegate: ItemDelegate {
width: parent.width
text: model.fileName
font.pixelSize: 15
contentItem: Text {
text: parent.text
font: parent.font
elide: Text.ElideRight
horizontalAlignment: Text.AlignRight
verticalAlignment: Text.AlignVCenter
wrapMode: Text.Wrap
}
highlighted: ListView.isCurrentItem
onClicked: { files.currentIndex = model.index; }
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: "lightgray"
Image {
id: currentPhoto
anchors.fill: parent
anchors.margins: 20
source: folderModel.get(files.currentIndex, "fileURL");
fillMode: Image.PreserveAspectFit
autoTransform: true
}
}
}
}
#include "backend.h"
Backend::Backend(QObject *parent) : QObject(parent)
{
process.setProgram("python");
}
bool Backend::movePanTilt(QString basePath, int pan, int tilt)
{
//qDebug() << "pan:" << pan << " | tilt:" << tilt;
QString errorMsg = QString();
if (process.state() != 0)
{
errorMsg = "waiting till previously started process ends...";
qDebug() << errorMsg;
emit someError(errorMsg);
return false;
}
process.setArguments(QStringList()
<< QString("%1/scripts/move.py").arg(basePath)
<< QString::number(pan)
<< QString::number(tilt)
);
process.start();
process.waitForFinished(500);
if (process.exitCode() != 0)
{
errorMsg = process.readAllStandardError();
qDebug() << "error:" << errorMsg;
emit someError(errorMsg);
return false;
}
else
{
//qDebug() << "output:" << process.readAllStandardOutput();
return true;
}
}
QString Backend::getBasePath(QString applicationDirPath)
{
QString baseFolder = QString();
QString pathLastMile = baseFolder.prepend("/");
#if defined(Q_OS_MAC)
pathLastMile = baseFolder.prepend("/../../..");
#elif defined(Q_OS_WIN)
pathLastMile = baseFolder.prepend("/..");
#endif
return QString("%1%2").arg(applicationDirPath).arg(pathLastMile);
}
#ifndef BACKEND_H
#define BACKEND_H
#include <QObject>
#include <QProcess>
#include <QStringList>
#include <QDebug>
class Backend : public QObject
{
Q_OBJECT
public:
explicit Backend(QObject *parent = nullptr);
signals:
void someError(QString errorMsg);
public slots:
bool movePanTilt(QString basePath, int pan, int tilt);
static QString getBasePath(QString applicationDirPath);
private:
QProcess process;
};
#endif // BACKEND_H
......@@ -3,6 +3,8 @@
#include <QQmlContext>
#include <QtDebug>
#include <QCameraInfo>
#include <QDir>
#include "backend.h"
int main(int argc, char *argv[])
{
......@@ -14,18 +16,22 @@ int main(int argc, char *argv[])
QQmlApplicationEngine engine;
QString shotsFolderName = "shots/";
QString shotsPathLastMile = shotsFolderName.prepend("/");
#if defined(Q_OS_MAC)
shotsPathLastMile = shotsFolderName.prepend("/../../..");
#elif defined(Q_OS_WIN)
webPagesPathLastMile = shotsFolderName.prepend("/..");
#endif
QString shotsPath = QString("%1%2")
.arg(app.applicationDirPath())
.arg(shotsPathLastMile);
qDebug() << shotsPath;
engine.rootContext()->setContextProperty("shotsPath", shotsPath);
QString basePath = Backend::getBasePath(app.applicationDirPath());
//qDebug() << basePath;
engine.rootContext()->setContextProperty("basePath", basePath);
// create the folder for shots, if it doesn't exist
QString shotsPath = QDir(basePath).filePath("shots");
if (!QDir(shotsPath).exists())
{
if (!QDir().mkdir(shotsPath))
{
qCritical("Error: couldn't create the folder for shots");
return -1;
}
}
qmlRegisterType<Backend>("io.qt.Backend", 1, 0, "Backend");
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
if (engine.rootObjects().isEmpty()) { return -1; }
......
import QtQuick 2.11
import QtQuick.Window 2.11
import QtMultimedia 5.11
import QtQuick.Layouts 1.11
import QtQuick.Controls 2.4
import Qt.labs.folderlistmodel 2.11
import io.qt.Backend 1.0
ApplicationWindow {
id: root
......@@ -13,11 +12,14 @@ ApplicationWindow {
minimumWidth: 700
height: 500
minimumHeight: 400
title: qsTr("WebGL demo")
title: qsTr("WebGL release demo")
// for sliders and button
property int maxSize: 80
property string sliderColor: "transparent"
property int maxValue: 70 // degrees for servos (with 20 safety gap)
property int maxSize: 60 // pixels for sliders items and button
property string sliderColor: "transparent" // perhaps some styling will take place
property bool cameraUpsideDown: true // if you need to rotate viewfinder to 180
Backend { id: backend }
header: TabBar {
id: tabBar
......@@ -33,189 +35,21 @@ ApplicationWindow {
anchors.fill: parent
currentIndex: tabBar.currentIndex
Item {
GridLayout {
anchors.fill: parent
rows: 2
columns: 2
rowSpacing: 0
columnSpacing: 0
Rectangle {
Layout.row: 0
Layout.column: 0
Layout.fillWidth: true
Layout.fillHeight: true
Text {
id: cameraStatus
anchors.centerIn: parent
width: parent.width
text: qsTr("loading camera...")
font.pixelSize: 40
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
}
// Rectangle {
// anchors.fill: parent
// border.width: 5
// border.color: "gray"
VideoOutput {
anchors.fill: parent
orientation: 180
fillMode: VideoOutput.PreserveAspectCrop
source: camera
Camera {
id: camera
cameraState: tabCamera.checked ? Camera.ActiveState : Camera.LoadedState
imageCapture {
onImageCaptured: {
console.log("photo has been captured")
}
onImageSaved: {
console.log("photo has been saved:", camera.imageCapture.capturedImagePath)
}
}
onError: {
cameraStatus.text = qsTr("Error: ") + errorString;
console.log(errorCode, errorString);
}
Component.onCompleted: {
// console.log(qsTr("camera orientation:"), camera.orientation);
// console.log(qsTr("camera state:"), camera.cameraState);
// console.log(qsTr("camera status:"), camera.cameraStatus);
// console.log(qsTr("camera supported resolutions:"), imageCapture.supportedResolutions);
}
}
}
// }
}
Rectangle {
Layout.row: 1
Layout.column: 0
Layout.fillWidth: true
Layout.fillHeight: true
Layout.maximumHeight: root.maxSize
color: root.sliderColor
Slider {
anchors.fill: parent
from: -90
value: 0
to: 90
}
}
Rectangle {
Layout.row: 0
Layout.column: 1
Layout.fillWidth: true
Layout.fillHeight: true
Layout.maximumWidth: root.maxSize
color: root.sliderColor
Slider {
anchors.fill: parent
orientation: Qt.Vertical
from: -90
value: 0
to: 90
}
}
Button {
Layout.row: 1
Layout.column: 1
Layout.fillWidth: true
Layout.fillHeight: true
Layout.maximumWidth: root.maxSize
Layout.maximumHeight: root.maxSize
scale: hovered ? (pressed ? 0.9 : 1.1) : 1
background: Rectangle {
color: "transparent"
}
Image {
id: cameraIcon
anchors.fill: parent
source: "/img/camera.png"
}
onClicked: {
camera.imageCapture.captureToLocation(shotsPath + getCurrentDateTime() + ".jpg");
}
}
}
}
Item {
RowLayout {
anchors.fill: parent
//anchors.topMargin: 15
spacing: 0
Rectangle {
Layout.preferredWidth: parent.width * 0.3
Layout.fillHeight: true
ListView {
id: files
anchors.fill: parent
FolderListModel {
id: folderModel
folder: "file:" + shotsPath
nameFilters: ["*.jpg"]
}
model: folderModel
delegate: ItemDelegate {
width: parent.width
text: model.fileName
font.pixelSize: 15
contentItem: Text {
text: parent.text
font: parent.font
elide: Text.ElideRight
horizontalAlignment: Text.AlignRight
verticalAlignment: Text.AlignVCenter
wrapMode: Text.Wrap
}
highlighted: ListView.isCurrentItem
onClicked: { files.currentIndex = model.index; }
}
}
// Component.onCompleted: {
// currentPhoto.source = "file:/" + folderModel.get(files.currentIndex, "filePath");
// }
}
TabCamera {}
TabPhotos {}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: "lightgray"
MessageBox {
id: dialogError
title: "Some error"
textMain: "Some error"
}
Image {
id: currentPhoto
anchors.fill: parent
anchors.margins: 20
source: folderModel.get(files.currentIndex, "fileURL");
fillMode: Image.PreserveAspectFit
}
}
}
Connections {
target: backend
onSomeError: {
dialogError.textMain = "Some error moving the camera\n" + errorMsg;
dialogError.show();
}
}
......
......@@ -2,5 +2,8 @@
<qresource prefix="/">
<file>main.qml</file>
<file>img/camera.png</file>
<file>MessageBox.qml</file>
<file>TabPhotos.qml</file>
<file>TabCamera.qml</file>
</qresource>
</RCC>
import sys
import time
import pantilthat
if len(sys.argv) < 3:
sys.stderr.write("You need to provide pan and tilt values")
sys.exit(1)
if len(sys.argv) > 3:
sys.stderr.write("You need to provide pan and tilt values only")
sys.exit(1)
tiltValue = sys.argv[1]
try: tiltValue = int(tiltValue)
except:
sys.stderr.write("Tilt value should be int")
sys.exit(1)
#print(isinstance(tiltValue, int))
if (tiltValue < -90 or tiltValue > 90):
sys.stderr.write("Tilt value should be within (-90;90)")
sys.exit(1)
panValue = sys.argv[2]
try: panValue = int(panValue)
except:
sys.stderr.write("Pan value should be int")
sys.exit(1)
#print(isinstance(panValue, int))
if (panValue < -90 or panValue > 90):
sys.stderr.write("Pan value should be within (-90;90)")
sys.exit(1)
pantilthat.tilt(tiltValue)
pantilthat.pan(panValue)
time.sleep(0.25)
sys.exit(0)
......@@ -14,7 +14,11 @@ DEFINES += QT_DEPRECATED_WARNINGS
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += \
main.cpp
main.cpp \
backend.cpp
HEADERS += \
backend.h
RESOURCES += qml.qrc
......@@ -28,3 +32,24 @@ QML_DESIGNER_IMPORT_PATH =
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
OTHER_FILES += \
$$PWD/scripts
# copies the given files to the destination directory
defineTest(copyToDestDir) {
files = $$1
dir = $$2
win32:dir ~= s,/,\\,g
for(file, files) {
# replace slashes in paths with backslashes for Windows
win32:file ~= s,/,\\,g
QMAKE_POST_LINK += $$QMAKE_COPY_DIR $$shell_quote($$file) $$shell_quote($$dir) $$escape_expand(\\n\\t)