Skip to content

Commit

Permalink
using timestamped subfolder of images for a session
Browse files Browse the repository at this point in the history
revert shell kill changes as they were not good
  • Loading branch information
szigyi committed Mar 19, 2021
1 parent 92d1a21 commit 4e45fd2
Show file tree
Hide file tree
Showing 5 changed files with 46 additions and 30 deletions.
3 changes: 1 addition & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ assemblyJarName in assembly := "expose-to-the-light_" + scalaMajorVersion + "-"
scalacOptions ++= Seq(
"-Xfatal-warnings",
"-deprecation",
"-unchecked",
"-language:implicitConversions"
"-unchecked"
)

libraryDependencies ++= Seq(
Expand Down
6 changes: 3 additions & 3 deletions src/main/scala/hu/szigyi/ettl/app/CliEttlApp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import scala.util.{Failure, Success, Try}
// 18: DummyCamera create fake images - numbered images - https://www.baeldung.com/java-add-text-to-image
// 19: do not rename image file names, leave as original
// 20: remove unnecessary info from logs
// TODO 21: store files of one session in a dedicated timestamped directory
// 21: store files of one session in a dedicated timestamped directory
// TODO 22: make script that installs the app and make it runnable from commandline to macOS and debian
// TODO 23: can provide camera's settings for every capture (evaluated at the time of capture if it is possible)
// TODO 24: emergency shutdown: cancel the execution (cancel fs2 stream?)
Expand Down Expand Up @@ -68,8 +68,8 @@ object CliEttlApp extends IOApp with StrictLogging {
val interval = Duration(intervalSeconds, TimeUnit.SECONDS)

val ettl =
if (dummyCamera) new EttlApp(appConfig, new DummyCamera, new SchedulerImpl(clock, schedulerAwakingFrequency))
else new EttlApp(appConfig, new GCameraImpl, new SchedulerImpl(clock, schedulerAwakingFrequency))
if (dummyCamera) new EttlApp(appConfig, new DummyCamera, new SchedulerImpl(clock, schedulerAwakingFrequency), clock.instant())
else new EttlApp(appConfig, new GCameraImpl, new SchedulerImpl(clock, schedulerAwakingFrequency), clock.instant())

logger.info(s" Clock: $clock")
logger.info(s" Dummy Camera: $dummyCamera")
Expand Down
5 changes: 2 additions & 3 deletions src/main/scala/hu/szigyi/ettl/service/CameraHandler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package hu.szigyi.ettl.service

import com.typesafe.scalalogging.StrictLogging
import hu.szigyi.ettl.hal.{GCamera, GConfiguration, GFile}
import hu.szigyi.ettl.util.ShellKill
import org.gphoto2.GPhotoException

import scala.util.{Failure, Try}
Expand All @@ -12,11 +11,11 @@ object CameraHandler extends StrictLogging {
def takePhoto(camera: GCamera): Try[GFile] =
camera.captureImage

def connectToCamera(camera: GCamera): Try[GConfiguration] =
def connectToCamera(camera: GCamera, shellKill: => Unit): Try[GConfiguration] =
initialiseCamera(camera).recoverWith {
case e: GPhotoException if e.result == -53 =>
logger.warn("Executing shell kill and retry connecting to the camera...")
ShellKill.killGPhoto2Processes()
shellKill
initialiseCamera(camera)
case unrecoverableException =>
Failure(unrecoverableException)
Expand Down
39 changes: 27 additions & 12 deletions src/main/scala/hu/szigyi/ettl/service/EttlApp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,31 @@ import hu.szigyi.ettl.app.CliEttlApp.AppConfiguration
import hu.szigyi.ettl.hal.{GCamera, GConfiguration, GFile}
import hu.szigyi.ettl.model.Model.SettingsCameraModel
import hu.szigyi.ettl.service.CameraHandler.{connectToCamera, takePhoto}
import hu.szigyi.ettl.util.ShellKill
import hu.szigyi.ettl.util.Timing.time

import java.nio.file.Path
import java.time.{Instant, ZoneId}
import java.time.format.DateTimeFormatter
import scala.concurrent.duration.Duration
import scala.util.Try
import scala.util.{Failure, Success, Try}

class EttlApp(appConfig: AppConfiguration, camera: GCamera, scheduler: Scheduler) extends StrictLogging {
class EttlApp(appConfig: AppConfiguration, camera: GCamera, scheduler: Scheduler, now: Instant) extends StrictLogging {

def execute(setting: Option[SettingsCameraModel], numberOfCaptures: Int, interval: Duration): Try[Seq[Path]] =
for {
config <- connectToCamera(camera)
imagePaths <- scheduler.schedule(numberOfCaptures, interval, capture(camera, config, numberOfCaptures, setting))
_ <- config.close
sessionFolderName <- Try(nowToSessionId(now))
_ <- createSessionFolder(sessionFolderName)
config <- connectToCamera(camera, ShellKill.killGPhoto2Processes())
imagePaths <- scheduler.schedule(numberOfCaptures, interval, capture(camera, config, numberOfCaptures, setting, sessionFolderName))
_ <- config.close
} yield imagePaths

private def capture(camera: GCamera, config: GConfiguration, numberOfCaptures: Int, c: Option[SettingsCameraModel])(imageCount: Int): Try[Path] =
private def capture(camera: GCamera, config: GConfiguration, numberOfCaptures: Int, c: Option[SettingsCameraModel], sessionFolderName: String)(imageCount: Int): Try[Path] =
for {
_ <- Try(c.map(adjustSettings(config, _, imageCount, numberOfCaptures)))
imgFileOnCamera <- capturePhoto(camera, imageCount, numberOfCaptures)
imgPathOnComputer <- imgFileOnCamera.saveImageTo(appConfig.imageBasePath)
imgPathOnComputer <- imgFileOnCamera.saveImageTo(appConfig.imageBasePath.resolve(sessionFolderName))
_ <- imgFileOnCamera.close
} yield {
logger.info(s"[$imageCount/$numberOfCaptures] Saved image: ${imgPathOnComputer.toString}")
Expand All @@ -50,12 +55,22 @@ class EttlApp(appConfig: AppConfiguration, camera: GCamera, scheduler: Scheduler

logger.info(s"[$imageCount/$numberOfCaptures] Adjusting settings: [ss: ${c.shutterSpeedString}, i: ${c.iso}, a: ${c.aperture}]")
val ss = c.shutterSpeedString.map(setSS)
val i = c.iso.map(setI)
val a = c.aperture.map(setA)
val tryChanges: Try[List[Unit]] = List(ss, i, a).flatten.sequence
tryChanges.flatMap(changes => {
val i = c.iso.map(setI)
val a = c.aperture.map(setA)
List(ss, i, a).flatten.sequence.flatMap(changes => {
if (changes.nonEmpty) config.apply
else Try(())
})
}
}

private def nowToSessionId(now: Instant): String =
DateTimeFormatter.ofPattern("yyyy_MM_dd_HH_mm_ss").withZone(ZoneId.systemDefault()).format(now)

private def createSessionFolder(sessionFolderName: String): Try[Unit] =
appConfig.imageBasePath.resolve(sessionFolderName).toFile.mkdirs() match {
case true =>
Success(())
case false =>
Failure(new Exception(s"Could not create session folder: ${appConfig.imageBasePath.resolve(sessionFolderName).toAbsolutePath}"))
}
}
23 changes: 13 additions & 10 deletions src/test/scala/hu/szigyi/ettl/service/EttlAppSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import org.gphoto2.GPhotoException
import org.scalatest.freespec.AnyFreeSpec
import org.scalatest.matchers.should.Matchers

import java.time.Instant.{parse => instant}

import java.nio.file.Paths
import scala.concurrent.duration._
import scala.util.{Failure, Success, Try}
Expand All @@ -16,13 +18,13 @@ class EttlAppSpec extends AnyFreeSpec with Matchers {

"runEttl" - {
"Normal Scenario" - {
"set camera then capture images with custom settings and returns image paths" in {
"set camera then capture images with custom settings" in {
val camera = new DummyCamera(testing = true)
val result = new EttlApp(AppConfiguration(Paths.get("/"), "CR2"), camera, immediateScheduler)
val result = new EttlApp(AppConfiguration(Paths.get("/"), "CR2"), camera, immediateScheduler, instant("2021-03-19T19:00:00Z"))
.execute(Some(SettingsCameraModel(Some(1d / 100d), Some(400), Some(2.8))), 2, 10.millisecond)

result shouldBe a[Success[_]]
result.get shouldBe Seq(Paths.get("/IMG_0001.JPG"), Paths.get("/IMG_0002.JPG"))
result.get shouldBe Seq(Paths.get("/2021_03_19_19_00_00/IMG_0001.JPG"), Paths.get("/2021_03_19_19_00_00/IMG_0002.JPG"))
camera.adjustedCameraSettings.keys.toSeq should contain theSameElementsAs Seq(
"/imgsettings/imageformatsd",
"/imgsettings/iso",
Expand All @@ -32,18 +34,15 @@ class EttlAppSpec extends AnyFreeSpec with Matchers {
"/capturesettings/shutterspeed",
"/capturesettings/aperture"
)
camera.savedImages.map(_.toString) should contain theSameElementsAs Seq(
"/IMG_0001.JPG", "/IMG_0002.JPG"
)
}

"set camera then capture images and returns image paths" in {
"set camera then capture images without custom settings" in {
val camera = new DummyCamera(testing = true)
val result = new EttlApp(AppConfiguration(Paths.get("/"), "CR2"), camera, immediateScheduler)
val result = new EttlApp(AppConfiguration(Paths.get("/"), "CR2"), camera, immediateScheduler, instant("2021-03-19T19:00:00Z"))
.execute(None,2, 10.millisecond)

result shouldBe a[Success[_]]
result.get shouldBe Seq(Paths.get("/IMG_0001.JPG"), Paths.get("/IMG_0002.JPG"))
result.get shouldBe Seq(Paths.get("/2021_03_19_19_00_00/IMG_0001.JPG"), Paths.get("/2021_03_19_19_00_00/IMG_0002.JPG"))
camera.adjustedCameraSettings.keys.toSeq should contain theSameElementsAs Seq(
"/imgsettings/imageformatsd",
"/imgsettings/imageformat",
Expand All @@ -62,7 +61,7 @@ class EttlAppSpec extends AnyFreeSpec with Matchers {
override def newConfiguration: Try[GConfiguration] = throw new UnsupportedOperationException()

override def captureImage: Try[GFile] = throw new UnsupportedOperationException()
}, immediateScheduler).execute(None, 1, 10.millisecond)
}, immediateScheduler, instant("2021-03-19T19:00:00Z")).execute(None, 1, 10.millisecond)

result shouldBe a[Failure[_]]
result.failed.get.getMessage shouldBe "gp_camera_init failed with GP_ERROR_MODEL_NOT_FOUND #-105: Unknown model"
Expand All @@ -71,6 +70,10 @@ class EttlAppSpec extends AnyFreeSpec with Matchers {
"when capture settings are invalid and image cannot be taken" ignore {

}

"when cannot create session folder then fail fast and does not even try to connect to camera" ignore {

}
}
}
}

0 comments on commit 4e45fd2

Please sign in to comment.