aboutsummaryrefslogtreecommitdiff
path: root/hsm-cam/Hsm/Cam.hs
blob: d1f9cd271d676b4bbc8be98a6ac15727ca8a61b5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeFamilies #-}

module Hsm.Cam
  ( Cam
  , capturePng
  , runCam
  )
where

import Codec.Picture (Image (Image), encodePng)
import Codec.Picture.Types (PixelRGB8)
import Control.Concurrent (MVar, forkIO, newEmptyMVar, putMVar, takeMVar)
import Control.Exception (mask_)
import Control.Monad.Extra (whenM)
import Control.Monad.Loops (iterateM_)
import Data.Bits ((.|.))
import Data.ByteString.Lazy (ByteString)
import Data.List ((!?))
import Data.Primitive.Ptr (readOffPtr)
import Data.Vector.Storable (generateM)
import Effectful (Dispatch (Static), DispatchOf, Eff, IOE, liftIO, (:>))
import Effectful.Dispatch.Static (SideEffects (WithSideEffects), StaticRep, evalStaticRep, getStaticRep, unsafeEff_)
import Effectful.Exception (bracket, bracket_)
import Foreign.C.String (peekCString)
import Foreign.C.Types (CSize (CSize))
import Foreign.Ptr (Ptr, castPtr, freeHaskellFunPtr, nullPtr)
import Hsm.Cam.FFI
  ( acquireCamera
  , allocateFrameBuffer
  , createRequest
  , frameHeight
  , frameWidth
  , freeFrameBuffer
  , getDmaBufferFd
  , makeLogger
  , makeRequestCallback
  , registerLogger
  , registerRequestCallback
  , releaseCamera
  , requestFrame
  , startCamera
  , startCameraManager
  , stopCamera
  , stopCameraManager
  )
import Hsm.Core.Bracket (bracketConst, bracketLiftIO_)
import Hsm.Log (Log, Severity (Attention, Info, Trace), getLevel, logMsg, makeLoggerIO)
import MMAP (mapShared, mkMmapFlags, mmap, munmap, protRead)
import System.Directory (doesFileExist, removeFile)
import System.Environment (setEnv)
import System.IO (IOMode (ReadWriteMode), hGetLine, withFile)
import System.Posix.Files (createNamedPipe, ownerReadMode, ownerWriteMode)
import Text.Read (readMaybe)

data Cam (a :: * -> *) (b :: *)

type instance DispatchOf Cam = Static WithSideEffects

data Rep = Rep
  { requestCallbackMVar :: MVar ()
  , dmaBuffer :: Ptr ()
  }

newtype instance StaticRep Cam
  = Cam Rep

-- RGB888 configuration for ov5647 sensor (Raspberry Pi Camera Module)
-- The following constants must be updated if either:
-- - Pixel format changes (e.g., to BGR, YUV, etc.)
-- - Camera module is replaced
frameLine :: Int
frameLine = frameWidth * 3

frameStride :: Int
frameStride = frameLine + 32

frameBufferLength :: Int
frameBufferLength = frameStride * frameHeight + 3072

capturePng :: (Log "cam" :> es, Cam :> es) => Eff es ByteString
capturePng = do
  Cam Rep{..} <- getStaticRep
  logMsg Trace "Requesting frame"
  unsafeEff_ . mask_ $ requestFrame >> takeMVar requestCallbackMVar
  logMsg Trace "Processing frame data"
  pixelVector <- unsafeEff_ . generateM (frameLine * frameHeight) $ mapPixel dmaBuffer
  logMsg Trace "Encoding PNG"
  return . encodePng $ Image @PixelRGB8 frameWidth frameHeight pixelVector
  where
    mapPixel dmaBuffer index = readOffPtr (castPtr dmaBuffer) offset
      where
        yIndex = index `div` frameLine
        xIndex = index `mod` frameLine
        offset = yIndex * frameStride + xIndex

-- Bidirectional mapping between libcamera's logging system and application logs.
-- All libcamera warnings and errors are elevated to the application's
-- 'Attention' level to ensure visibility.
data LibCameraSeverity
  = DEBUG
  | INFO
  | WARN
  | ERROR
  | FATAL
  deriving (Read, Show)

toLibCameraSeverity :: Severity -> LibCameraSeverity
toLibCameraSeverity =
  \case
    Trace -> DEBUG
    Info -> INFO
    Attention -> WARN

fromLibCameraSeverity :: LibCameraSeverity -> Severity
fromLibCameraSeverity =
  \case
    DEBUG -> Trace
    INFO -> Info
    _ -> Attention

runCam :: (IOE :> es, Log "cam" :> es, Log "libcamera" :> es) => Eff (Cam : es) a -> Eff es a
runCam action = do
  requestCallbackMVar <- liftIO newEmptyMVar
  bracketConst loggerAlloc loggerDealloc
    . bracketConst (requestCallbackAlloc requestCallbackMVar) requestCallbackDealloc
    . bracket_ logCaptureAlloc logCaptureDealloc
    . bracketLiftIO_ startCameraManager stopCameraManager
    . bracketLiftIO_ acquireCamera releaseCamera
    . bracketLiftIO_ allocateFrameBuffer freeFrameBuffer
    . bracketLiftIO_ startCamera stopCamera
    . bracketLiftIO_ createRequest (return ())
    . bracket mapDmaBuffer unmapDmaBuffer
    $ \dmaBuffer -> evalStaticRep (Cam Rep{..}) action
  where
    loggerAlloc = do
      logMsg @"cam" Info "Registering FFI logger"
      loggerIO <- makeLoggerIO @"cam"
      loggerFFI <- liftIO . makeLogger $ \severity message -> peekCString message >>= loggerIO (toEnum severity)
      liftIO $ registerLogger loggerFFI
      return loggerFFI
    loggerDealloc loggerFFI = do
      logMsg @"cam" Info "Unregistering FFI logger"
      liftIO $ freeHaskellFunPtr loggerFFI
    requestCallbackAlloc requestCallbackMVar = do
      logMsg @"cam" Info "Registering FFI request callback"
      requestCallbackFFI <- liftIO . makeRequestCallback $ putMVar requestCallbackMVar ()
      liftIO $ registerRequestCallback requestCallbackFFI
      return requestCallbackFFI
    requestCallbackDealloc requestCallbackFFI = do
      logMsg @"cam" Info "Unregistering FFI request callback"
      liftIO $ freeHaskellFunPtr requestCallbackFFI
    -- We use a named pipe (FIFO) to intercept libcamera's log output. The environment
    -- variables `LIBCAMERA_LOG_FILE` and `LIBCAMERA_LOG_LEVELS` configure libcamera
    -- to write logs to the FIFO with appropriate severity filtering.
    --
    -- A dedicated thread reads from the FIFO, parses log severity levels, and
    -- forwards messages to the application's logger with proper level mapping.
    logCaptureFifo = "/tmp/hsm-cam-libcamera.fifo"
    logCaptureClear = liftIO . whenM (doesFileExist logCaptureFifo) $ removeFile logCaptureFifo
    logCaptureSetEnvVar key value = do
      logMsg @"cam" Info $ "Setting env variable: " <> key <> "=" <> value
      liftIO $ setEnv key value
    logCaptureAlloc = do
      logCaptureClear
      logMsg @"cam" Info $ "Creating libcamera log capture FIFO at: " <> logCaptureFifo
      liftIO . createNamedPipe logCaptureFifo $ ownerReadMode .|. ownerWriteMode
      libCameraSeverity <- toLibCameraSeverity <$> getLevel @"libcamera"
      logCaptureSetEnvVar "LIBCAMERA_LOG_FILE" logCaptureFifo
      logCaptureSetEnvVar "LIBCAMERA_LOG_LEVELS" $ "*:" <> show libCameraSeverity
      loggerIO <- makeLoggerIO @"libcamera"
      logMsg @"cam" Info "Starting libcamera log capture"
      -- Thread handles multiline logs by maintaining severity state between lines.
      -- When a new line doesn't contain a parsable severity level, the previous
      -- line's level is reused to ensure continuous log context.
      liftIO . forkIO . withFile logCaptureFifo ReadWriteMode $ \handle ->
        flip iterateM_ DEBUG $ \previousSeverity -> do
          logLine <- hGetLine handle
          flip (maybe $ return previousSeverity) (words logLine !? 2 >>= readMaybe) $ \severity -> do
            loggerIO (fromLibCameraSeverity severity) logLine
            return severity
    logCaptureDealloc = do
      logMsg @"cam" Info "Removing libcamera log capture FIFO"
      logCaptureClear
    -- Memory maps the camera's DMA buffer for frame access
    mapSize = CSize $ toEnum frameBufferLength
    mapFlags = mkMmapFlags mapShared mempty
    mapDmaBuffer = do
      logMsg @"cam" Info "Mapping DMA buffer"
      liftIO $ getDmaBufferFd >>= \dmaBufferFd -> mmap nullPtr mapSize protRead mapFlags dmaBufferFd 0
    unmapDmaBuffer dmaBuffer = do
      logMsg @"cam" Info "Unmapping DMA buffer"
      liftIO $ munmap dmaBuffer mapSize