Commit 79bb5f6d authored by David's avatar David

Merge pull request #6 from dkowis/camera

Camera!
parents 26c0e882 22a24091
......@@ -35,6 +35,7 @@ repositories {
jcenter()
maven { url "https://dl.bintray.com/jerady/maven" }
maven { url "http://dl.bintray.com/jetbrains/spek" }
maven { url 'https://jitpack.io' }
maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
}
......@@ -73,6 +74,16 @@ dependencies {
"io.github.microutils:kotlin-logging:1.5.6",
'io.github.config4k:config4k:0.4.0'
//V4l4j for the raspberry pi, to get video directly from v4l
//Seems to be reasonably effective. OpenCV would be cool, but it's icky.
// The other option is fork out to rpi commands directly
//This requires the bcm2835_v4l2 module to be loaded
// Need to figure out how to get this to build, it's a huge PITA
//implementation 'com.github.sarxos:v4l4j:0.9.1-r507'
implementation 'com.github.Hopding:JRPiCam:1.1.1'
//Gonna use the DarkSky forecast info
implementation 'tk.plogitech:darksky-forecast-api-jackson:1.3.1',
'org.slf4j:jul-to-slf4j:1.7.25'
......
package `is`.kow.deskscreen
import `is`.kow.deskscreen.camera.CameraController
import `is`.kow.deskscreen.views.DefaultView
import javafx.scene.input.MouseEvent
import javafx.scene.input.TouchEvent
......@@ -11,6 +12,8 @@ class RootView : View() {
val displayController: DisplayController by inject()
val transitionsController: TransitionsController by inject()
val cameraController: CameraController by inject()
override val root = stackpane {
transitionsController.setStartingView(find(DefaultView::class))
......
package `is`.kow.deskscreen
import `is`.kow.deskscreen.views.DefaultView
import `is`.kow.deskscreen.camera.CameraView
import `is`.kow.deskscreen.system.SystemView
import `is`.kow.deskscreen.views.DefaultView
import `is`.kow.deskscreen.wx.LoadingView
import `is`.kow.deskscreen.wx.RadarView
import mu.KotlinLogging
......@@ -20,6 +21,7 @@ class TransitionsController : Controller() {
private val loadingView: LoadingView by inject()
private val radarView: RadarView by inject()
private val infoView: SystemView by inject()
private val cameraView: CameraView by inject()
private val currentView = AtomicReference<View>()
......@@ -46,6 +48,11 @@ class TransitionsController : Controller() {
setView(infoView)
}
fun showCameraView() {
logger.debug("Switching to camera view")
setView(cameraView)
}
private fun setView(thing: View) {
//TODO: there's something wrong here, I haven't done it right...
currentView.getAndSet(thing).replaceWith(thing)
......
package `is`.kow.deskscreen.camera
import `is`.kow.deskscreen.MainController
import `is`.kow.deskscreen.TransitionsController
import `is`.kow.deskscreen.utils.Util
import com.github.thomasnield.rxkotlinfx.doOnNextFx
import com.github.thomasnield.rxkotlinfx.observeOnFx
import com.hopding.jrpicam.RPiCamera
import io.reactivex.Observable
import io.reactivex.schedulers.Schedulers
import javafx.event.ActionEvent
import javafx.scene.image.Image
import mu.KotlinLogging
import tornadofx.*
import java.nio.file.Paths
import java.util.concurrent.TimeUnit
class CameraController : Controller() {
val logger = KotlinLogging.logger {}
private val camera: RPiCamera?
private val mainController: MainController by inject()
private val cameraStateModel: CameraStateModel by inject()
private val transitionsController: TransitionsController by inject()
init {
//Only care that the folders exist
//TODO: add a cleanup mechanism that only keeps some number of pictures, or some amount of space or something
//Or just always use toBufferedStill
Paths.get("/home/pi/Pictures").toFile().mkdirs()
camera = if (mainController.isPi) {
try {
RPiCamera("/home/pi/Pictures")
} catch (e: Exception) {
//Crapopolis
logger.error("Unable to initialize camera", e)
null
}
} else {
null
}
}
fun takePicture(actionEvents: Observable<ActionEvent>) {
actionEvents
.observeOn(Schedulers.io())
.doOnNextFx {
//UPdate state
cameraStateModel.state.set(PictureTakingState.TAKING)
}
.map {
val timeout = 5
//gimme a timer countdown
logger.debug("Right before starting range timeout")
Observable.intervalRange(0, timeout.toLong() + 1, 0, 1, TimeUnit.SECONDS)
.map {
timeout - it
}
.observeOnFx()
.map {
logger.debug("COUNTING: $it")
cameraStateModel.countdownPercentage.value = it / timeout.toDouble()
}
.subscribe()
//The camera blocks things. need to also start the countdown observable for the thing.
if (mainController.isPi) {
//Camera won't be null if it's pi!
val bf = camera
?.setRotation(270)
?.setDateTimeOn()
?.setTimeout(timeout * 1000) //This timeout is in milliseconds
?.setFullPreviewOn()
?.takeBufferedStill()!!
//lets just convert it directly
Util.toFXImage(bf, null)
} else {
//load the cat picture WHY WON'T YOU LOAD MY KITTY
val stream = this.javaClass.getResourceAsStream("/imagery/MaineCoon-cropped.jpeg")
val img = Image(stream)
Thread.sleep((timeout * 1000).toLong())
img
}
}
.doOnNextFx {
logger.debug("SETTING IMAGE IN THE MODEL AND CHANGING STATE")
cameraStateModel.image.set(it)
cameraStateModel.state.set(PictureTakingState.PREVIEWING)
}
.doOnError {
logger.error("Error during taking picture!", it)
}
.subscribe()
}
fun keepPicture(actionEvents: Observable<ActionEvent>) {
actionEvents
.doOnNext {
logger.error("SHARE PICTURE OR SOMETHING!")
}
.observeOnFx()
.map {
cameraStateModel.image.value = null
cameraStateModel.state.set(PictureTakingState.READY)
//TODO: would set state to sharing or something to display what to do with the picture
//Until then, just return them to the main frame
transitionsController.showDefaultView()
}
.subscribe()
}
fun rejectPicture(actionEvents: Observable<ActionEvent>) {
actionEvents
.observeOnFx()
.map {
cameraStateModel.image.value = null
cameraStateModel.state.set(PictureTakingState.READY)
}
.subscribe()
}
fun cancelPictureTaking(actionEvents: Observable<ActionEvent>) {
actionEvents
.observeOnFx()
.doOnNext {
cameraStateModel.image.value = null
cameraStateModel.state.set(PictureTakingState.READY)
transitionsController.showDefaultView()
}
.subscribe()
}
}
\ No newline at end of file
package `is`.kow.deskscreen.camera
import com.github.thomasnield.rxkotlinfx.actionEvents
import com.github.thomasnield.rxkotlinfx.toObservableChanges
import com.github.thomasnield.rxkotlinfx.toObservableChangesNonNull
import javafx.beans.binding.Bindings
import javafx.beans.binding.BooleanBinding
import javafx.beans.property.SimpleDoubleProperty
import javafx.beans.property.SimpleObjectProperty
import javafx.geometry.Pos
import javafx.scene.control.ProgressBar
import javafx.scene.image.Image
import javafx.scene.layout.Priority
import mu.KotlinLogging
import org.kordamp.ikonli.fontawesome5.FontAwesomeSolid
import org.kordamp.ikonli.javafx.FontIcon
import tornadofx.*
import java.util.concurrent.atomic.AtomicReference
enum class PictureTakingState {
READY, TAKING, PREVIEWING, GOOD
}
class CameraState {
val imageProperty = SimpleObjectProperty<Image>(this, "image", null)
var image by imageProperty
val stateProperty = SimpleObjectProperty<PictureTakingState>(this, "state", PictureTakingState.READY)
var state by stateProperty
val countdownPercentageProperty = SimpleDoubleProperty(this, "countdown", 5.0)
var countdownPercentage by countdownPercentageProperty
}
class CameraStateModel : ItemViewModel<CameraState>() {
private val logger = KotlinLogging.logger {}
val image: SimpleObjectProperty<Image> = bind(CameraState::imageProperty)
val state: SimpleObjectProperty<PictureTakingState> = bind(CameraState::stateProperty)
val countdownPercentage = bind(CameraState::countdownPercentageProperty)
val previewing: BooleanBinding = Bindings.equal(PictureTakingState.PREVIEWING, state)
val readyToTake: BooleanBinding = Bindings.equal(PictureTakingState.READY, state)
val taking: BooleanBinding = Bindings.equal(PictureTakingState.TAKING, state)
}
class CameraView : View() {
private val logger = KotlinLogging.logger {}
private val cameraLoadingView: CameraLoadingView by inject()
private val cameraImagePreviewView: CameraImagePreviewView by inject()
private val cameraStateModel: CameraStateModel by inject()
private val cameraController: CameraController by inject()
private val currentView = AtomicReference<View>()
init {
//set up the state, it's weird that I even have to do this, the initial value doesn't work
cameraStateModel.state.set(PictureTakingState.READY)
currentView.set(cameraLoadingView)
}
override fun onDock() {
//Hook up an observable to the state to have it swap views when ready
cameraStateModel.state.toObservableChangesNonNull()
.map { change ->
val to = change.newVal
to
}
.map {
when (it) {
PictureTakingState.PREVIEWING -> currentView.getAndSet(cameraImagePreviewView).replaceWith(cameraImagePreviewView)
else -> currentView.getAndSet(cameraLoadingView).replaceWith(cameraLoadingView)
}
}
.subscribe()
}
override val root = borderpane {
//Left and right can be 160px max
center = currentView.get().root
right = vbox {
prefWidth = 160.0
hbox {
alignment = Pos.CENTER
button("", FontIcon.of(FontAwesomeSolid.CHECK_SQUARE, 60)) {
style {
baseColor = c("black")
backgroundColor += c("green")
}
enableWhen {
cameraStateModel.previewing
}
cameraController.keepPicture(actionEvents())
//Picture is good, enabled only after a capture
//Make it green
}
}
//Kinda want a space in-between these two
hbox {
alignment = Pos.CENTER
style {
paddingTop = 10.0
}
button("Take Picture!") {
style {
textFill = c("black")
}
//Call the controller action that updates the model and starts the count down
enableWhen(cameraStateModel.readyToTake)
cameraController.takePicture(actionEvents())
}
}
label("Delay to take picture")
hbox {
hgrow = Priority.ALWAYS
progressbar(cameraStateModel.countdownPercentage) {
useMaxWidth = true
prefWidthProperty().bind(this@hbox.widthProperty())
}
}
}
left = borderpane {
prefWidth = 160.0
top = hbox {
alignment = Pos.CENTER
button("", FontIcon.of(FontAwesomeSolid.WINDOW_CLOSE, 60)) {
cameraController.rejectPicture(actionEvents())
style {
baseColor = c("black")
backgroundColor += c("red")
}
//Picture is no good, don't keep it
enableWhen {
cameraStateModel.previewing
}
}
}
bottom = hbox {
alignment = Pos.CENTER
button("", FontIcon.of(FontAwesomeSolid.EJECT, 60)) {
cameraController.cancelPictureTaking(actionEvents())
style {
baseColor = c("black")
}
disableWhen {
cameraStateModel.taking
}
}
}
}
}
}
class CameraLoadingView : View() {
override val root = hbox {
vgrow = Priority.ALWAYS
hgrow = Priority.ALWAYS
alignment = Pos.CENTER
progressindicator {
progress = ProgressBar.INDETERMINATE_PROGRESS
}
}
}
class CameraImagePreviewView : View() {
private val cameraStateModel: CameraStateModel by inject()
private val logger = KotlinLogging.logger {}
override val root = hbox {
//Put teh image here
imageview {
this.fitHeight = 480.0
this.fitWidth = 480.0
this.image = cameraStateModel.image.get()
//Why doesn't this get triggered, because first time through it's a race condition
cameraStateModel.image.toObservableChanges()
.map {
if (it.newVal != null) {
this.image = it.newVal
} else {
//Can this even work?
logger.debug("NO IMAGE")
this.image = null
}
}
.subscribe()
}
}
}
package `is`.kow.deskscreen.utils
import javafx.scene.image.Image
import javafx.scene.image.PixelFormat
import javafx.scene.image.WritableImage
import sun.awt.image.IntegerComponentRaster
import java.awt.image.BufferedImage
import java.io.InputStream
import java.nio.file.Files
import java.nio.file.Path
......@@ -31,4 +36,79 @@ object Util {
fun md5File(path: Path): String {
return md5Stuff(Files.newInputStream(path))
}
/**
* Stolen from the JDK, because it's apparently not in the javafx on the rpi. Maybe different versions?
* Snapshots the specified [BufferedImage] and stores a copy of
* its pixels into a JavaFX [Image] object, creating a new
* object if needed.
* The returned `Image` will be a static snapshot of the state
* of the pixels in the `BufferedImage` at the time the method
* completes. Further changes to the `BufferedImage` will not
* be reflected in the `Image`.
*
*
* The optional JavaFX [WritableImage] parameter may be reused
* to store the copy of the pixels.
* A new `Image` will be created if the supplied object is null,
* is too small or of a type which the image pixels cannot be easily
* converted into.
*
* @param bimg the `BufferedImage` object to be converted
* @param wimg an optional `WritableImage` object that can be
* used to store the returned pixel data
* @return an `Image` object representing a snapshot of the
* current pixels in the `BufferedImage`.
* @since JavaFX 2.2
*/
fun toFXImage(bimg: BufferedImage, wimg: WritableImage?): WritableImage {
var bimg = bimg
var wimg = wimg
val bw = bimg.width
val bh = bimg.height
when (bimg.type) {
BufferedImage.TYPE_INT_ARGB, BufferedImage.TYPE_INT_ARGB_PRE -> {
}
else -> {
val converted = BufferedImage(bw, bh, BufferedImage.TYPE_INT_ARGB_PRE)
val g2d = converted.createGraphics()
g2d.drawImage(bimg, 0, 0, null)
g2d.dispose()
bimg = converted
}
}
// assert(bimg.getType == TYPE_INT_ARGB[_PRE]);
if (wimg != null) {
val iw = wimg.width.toInt()
val ih = wimg.height.toInt()
if (iw < bw || ih < bh) {
wimg = null
} else if (bw < iw || bh < ih) {
val empty = IntArray(iw)
val pw = wimg.pixelWriter
val pf = PixelFormat.getIntArgbPreInstance()
if (bw < iw) {
pw.setPixels(bw, 0, iw - bw, bh, pf, empty, 0, 0)
}
if (bh < ih) {
pw.setPixels(0, bh, iw, ih - bh, pf, empty, 0, 0)
}
}
}
if (wimg == null) {
wimg = WritableImage(bw, bh)
}
val pw = wimg.pixelWriter
val icr = bimg.raster as IntegerComponentRaster
val data = icr.dataStorage
val offset = icr.getDataOffset(0)
val scan = icr.scanlineStride
val pf = if (bimg.isAlphaPremultiplied)
PixelFormat.getIntArgbPreInstance()
else
PixelFormat.getIntArgbInstance()
pw.setPixels(0, 0, bw, bh, pf, data, offset, scan)
return wimg
}
}
\ No newline at end of file
package `is`.kow.deskscreen.views
import `is`.kow.deskscreen.Styles
import `is`.kow.deskscreen.TransitionsController
import `is`.kow.deskscreen.temperature.TemperatureView
import javafx.geometry.Pos
import javafx.scene.paint.CycleMethod
import javafx.scene.paint.LinearGradient
import javafx.scene.paint.Stop
import mu.KotlinLogging
import org.kordamp.ikonli.fontawesome5.FontAwesomeSolid
import org.kordamp.ikonli.javafx.FontIcon
import tornadofx.*
//TODO: need to ensure a max width
class NamePlateView : View() {
val temperatureView: TemperatureView by inject()
private val logger = KotlinLogging.logger {}
private val temperatureView: TemperatureView by inject()
private val transitionsController: TransitionsController by inject()
override val root = borderpane {
style {
......@@ -48,7 +54,17 @@ class NamePlateView : View() {
}
}
right = temperatureView.root
right = borderpane {
left = button("", FontIcon.of(FontAwesomeSolid.CAMERA, 30)) {
style {
baseColor = c("black")
}
action {
transitionsController.showCameraView()
}
}
right = temperatureView.root
}
bottom = hbox {
style {
......
......@@ -151,7 +151,9 @@ class WeatherController : Controller() {
//TODO: emit to the ticker the error to display it on the screen
logger.error("Failed to acquire station identifier!", throwable)
}
.retry()
.retryWhen {
Observable.timer(1, TimeUnit.SECONDS)
}
.subscribe()
//Acquire the hourly forecast url too
......@@ -176,7 +178,9 @@ class WeatherController : Controller() {
//TODO: emit to the ticker the error to display it on the screen
logger.error("Failed to acquire hourly forecast url", throwable)
}
.retry() //TODO: should have some kind of backoff or whatever
.retryWhen {
Observable.timer(1, TimeUnit.SECONDS)
}
.subscribe()
//EMIT!
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment