diff --git a/recipes-app/fofb-daqcapt/files/daqarchiver b/recipes-app/fofb-daqcapt/files/daqarchiver
new file mode 100755
index 0000000000000000000000000000000000000000..268fac80bbbd76c7199e65e3c7cc00cb51ea3bc9
--- /dev/null
+++ b/recipes-app/fofb-daqcapt/files/daqarchiver
@@ -0,0 +1,263 @@
+#!/usr/bin/env python3
+import numpy as np
+import deviceaccess as da
+import logging
+import os
+import struct
+import mmap
+import multiprocessing as mp
+import argparse
+import datetime
+
+###################################################################################################
+#    LOGGER
+###################################################################################################
+# Install a BasicLogger
+logger = logging.getLogger("FofbArchiver")
+sh=logging.StreamHandler()
+sh.setLevel(logging.DEBUG)
+sh.setFormatter(logging.Formatter("%(processName)s-%(process)d  %(levelname)s: %(message)s"))
+
+
+###################################################################################################
+#    DEVICE ACCESS
+###################################################################################################
+da.setDMapFilePath("/opt/fofb/opcua-server/devices.dmap")
+app = da.Device("APPUIO")
+app.open()
+actbuffer = app.getScalarRegisterAccessor(np.int32, "APP.daq_0.ACTIVE_BUF")
+
+
+###################################################################################################
+#    INTERRUPT HANDLING
+###################################################################################################
+
+def irq_wait(irqfd):
+    """
+    Re-arm and wait on a UIO IRQ
+    """
+
+    logger.debug("Re-Arm IRQ")
+    os.write( irqfd, struct.pack( "I", 1 ) )
+
+    logger.debug("Wait IRQ")
+    try:
+        r = os.read(irqfd, 4)
+    except KeyboardInterrupt:
+        logger.warning("Waiting IRQ stopped by user")
+        return False
+
+    logger.debug("Reads IRQ: {}".format(int.from_bytes(r, "little")))
+
+    return True
+
+def irq_disable(irqfd):
+    logger.debug("Disable IRQ")
+    os.write( irqfd, struct.pack( "I", 0 ) )
+
+def irq_flush(irqfd):
+    """
+    Flush all IRQ until reads nothing.
+    """
+    os.set_blocking(irqfd, False)
+    r=1
+    while True:
+        logger.debug("Flushing IRQ...")
+        os.write( irqfd, struct.pack( "I", 1 ) )
+        try:
+            r = os.read(irqfd, 4)
+        except BlockingIOError:
+            break
+
+    os.set_blocking(irqfd, True)
+
+###################################################################################################
+#    DAQ HANDLING
+###################################################################################################
+
+def daq_configure():
+    """
+    Configure DAQ engine to use the continuous region (region 0)
+    """
+
+    logger.info("Configure DAQ")
+
+    app.write("APP.daq_0.DOUBLE_BUF_ENA", np.uint(1))
+    app.write("APP.daq_0.TAB_SEL", np.int(1), 0)
+
+
+def daq_enable(ena):
+    if ena:
+        logger.info("Enable DAQ, region 0")
+        app.write("APP.daq_0.ENABLE", np.int(1))
+        app.write("APP.DAQ_CONTROL", np.int(0))
+    else:
+        logger.info("Disable DAQ, all regions")
+        app.write("APP.daq_0.ENABLE", np.int(0))
+        app.write("APP.DAQ_CONTROL", np.int(2))
+
+
+def daq_trigger():
+    logger.info("Fire a DAQ trigger")
+    app.write("APP.DAQ_CONTROL", np.uint32(1))
+    app.write("APP.DAQ_CONTROL", np.uint32(0))
+
+def daq_status():
+    # Read registers with one bit per region
+    for reg in ["DOUBLE_BUF_ENA", "ACTIVE_BUF", "ENABLE"]:
+        vals = app.read("APP.daq_0."+reg, np.uint32)
+        logger.info("{:20}  {:10} {:10}".format(reg, vals&1, (vals>>1)&1))
+
+
+    # Read registers with one element per region
+    for reg in ["TAB_SEL", "STROBE_CNT", "SENT_BURST_CNT", "FIFO_STATUS"]:
+        vals = app.read("APP.daq_0."+reg, np.uint32)
+        logger.info("{:20}  {:10} {:10}".format(reg, *vals))
+
+def daq_current_buffer():
+    actbuffer.read()
+    return actbuffer[0]&1
+
+
+###################################################################################################
+#    DDR HANDLING
+###################################################################################################
+
+daqmmap = mmap.mmap(os.open("/dev/ddrpl", os.O_RDONLY | os.O_SYNC),
+            0x2000_0000, mmap.MAP_SHARED, (mmap.PROT_READ)) # | mmap.PROT_WRITE))
+
+daqmem = [np.ndarray(0x10000000, buffer=daqmmap, dtype='b', offset=o) for o in [0x00000000, 0x10000000]]
+
+###################################################################################################
+#    TASKERS
+###################################################################################################
+
+def tasker_filer(filepath, buf):
+    logger.info("Start a filer tasker.")
+
+    t1 = datetime.datetime.now()
+
+    # Dump to file
+    try:
+        with open(filepath, "wb") as fd:
+            fd.write(buf.tobytes())
+    except KeyboardInterrupt:
+        logger.warning("File writing stopped by user on '{}'.".format(filepath))
+        return
+
+    t2 = datetime.datetime.now()
+    logger.debug("Writing time: {}".format((t2-t1).total_seconds()))
+
+###################################################################################################
+#    ARCHIVER LOOP
+###################################################################################################
+
+def archiver_loop(path):
+
+    logger.info("Start an archiver loop")
+    irq_filepath = "/dev/ddrpl"
+
+    logger.debug("Opening FD on {}".format(irq_filepath))
+    irqfd = os.open(irq_filepath, os.O_RDWR | os.O_CLOEXEC)
+
+    # Prepare reception buffer
+
+    rbuf = np.empty((4, daqmem[0].size), dtype='b')
+    idxbuf = 0
+
+    # Flush
+    irq_flush(irqfd)
+
+    # Ignore first IRQ, but note the buffer
+    logger.debug("Wait for first IRQ...")
+    if not irq_wait(irqfd):
+        logger.error("Error while waiting first IRQ.")
+        os.close(irqfd)
+        return False
+    tp=datetime.datetime.now()
+
+    nbuf = daq_current_buffer()
+    logger.debug("Current buffer is {}, will read it on next IRQ.".format(nbuf))
+
+
+    try:
+        while True:
+            if not irq_wait(irqfd):
+                logger.error("Error while waiting IRQ.")
+                break
+            t0 = datetime.datetime.now()
+            logger.debug("IRQ period : {}".format((t0-tp).total_seconds()))
+            tp=t0
+
+            # Copy to buffer
+            rbuf[idxbuf] = daqmem[nbuf]
+            t1 = datetime.datetime.now()
+            logger.debug("Copy time: {}".format((t1-t0).total_seconds()))
+
+            # Launch job on rbuf
+            p = mp.Process(
+                    name="archiver_filer",
+                    target=tasker_filer,
+                    args=(
+                        path+"/Archive_{}.bin".format(t0.strftime("%Y%m%d_%H%M%S")),
+                        rbuf[idxbuf],
+                        ))
+            p.start()
+
+
+            # Increment buffer pointers
+            idxbuf = (idxbuf+1)%4
+            nbuf = (nbuf+1)%2
+
+    except KeyboardInterrupt:
+        logger.warning("Archiver loop ended by user.")
+        os.close(irqfd)
+
+
+
+###################################################################################################
+#    CLI INTERFACE
+###################################################################################################
+
+if __name__ == '__main__':
+
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--config", action="store_true", help="Config DAQ.")
+    parser.add_argument("--stop", action="store_true", help="Stop DAQ.")
+    parser.add_argument("--start", action="store_true", help="Start DAQ.")
+    parser.add_argument("--arch", action="store_true", help="Start an archiver loop")
+    parser.add_argument("-S", action="store_true", help="Display DAQ status and quit.")
+    parser.add_argument("--path", type=str, help="Path to write file", default="./")
+    parser.add_argument("--log", help="Log level", default="INFO")
+
+    args = parser.parse_args()
+
+    #### Install logger
+    # Remove handler previously attached
+    for hdlr in logger.handlers:
+        logger.removeHandler(hdlr)
+    # Attach this handler
+    logger.addHandler(sh)
+    logger.setLevel(getattr(logging, args.log.upper()))
+
+
+    if args.S:
+        daq_status()
+        exit(0)
+
+    if args.arch:
+        archiver_loop(args.path)
+
+        exit(0)
+
+    if args.config:
+        daq_configure()
+
+    if args.stop:
+        daq_enable(False)
+
+    if args.start:
+        daq_enable(True)
+        daq_trigger()
+
+
diff --git a/recipes-app/fofb-daqcapt/files/daqcapt b/recipes-app/fofb-daqcapt/files/daqcapt
new file mode 100755
index 0000000000000000000000000000000000000000..f631c654451a12ede4a22562e723e72f0f76a483
--- /dev/null
+++ b/recipes-app/fofb-daqcapt/files/daqcapt
@@ -0,0 +1,115 @@
+#!/usr/bin/env python3
+import time
+import argparse
+import numpy as np
+import os, mmap, ctypes
+import glob
+import deviceaccess
+
+# DAQ memory zone
+daqmem = mmap.mmap(os.open("/dev/ddrpl", os.O_RDWR | os.O_SYNC),
+            0x40000000, mmap.MAP_SHARED, (mmap.PROT_READ | mmap.PROT_WRITE))
+
+# DAQ device
+deviceaccess.setDMapFilePath("/opt/fofb/map/devices.dmap")
+appdev = deviceaccess.Device("APPUIO")
+appdev.open()
+
+# Number of Bytes Per Sample
+NBPS=8
+
+
+def configure(ena, N):
+    """
+    Configure DAQ regions.
+
+    ena: int
+        bitsel for enabling region (one bit per region)
+    N: int, list(int)
+        Number of samples. If int, same number for both region.
+    """
+
+    # Select tab 1 for both regions
+    appdev.write("APP.daq_0.TAB_SEL", np.int(1), 0)
+    appdev.write("APP.daq_0.TAB_SEL", np.int(1), 1)
+
+    # Set N samples for both regions
+    if type(N) == int:
+        appdev.write("APP.daq_0.SAMPLES", np.int(N), 0)
+        appdev.write("APP.daq_0.SAMPLES", np.int(N), 1)
+    else:
+        appdev.write("APP.daq_0.SAMPLES", np.int(N[0]), 0)
+        appdev.write("APP.daq_0.SAMPLES", np.int(N[1]), 1)
+
+    # Enable regions
+    appdev.write("APP.daq_0.ENABLE", np.int(ena))
+
+def trigger():
+    appdev.write("APP.DAQ_CONTROL", np.int(1))
+    appdev.write("APP.DAQ_CONTROL", np.int(0))
+
+def stop(b=True):
+    appdev.write("APP.DAQ_CONTROL", np.int(3*b))
+
+def status():
+
+    for r in ["ENABLE", "ACTIVE_BUF"]:
+        print("{:>20} {:10}".format(r,
+            appdev.read("APP.daq_0."+r, np.dtype('u4'))))
+
+    for r in ["TAB_SEL", "STROBE_CNT", "SAMPLES", "FIFO_STATUS", "SENT_BURST_CNT", "TRG_CNT_BUF0", "TRG_CNT_BUF1"]:
+        r0, r1 =appdev.read("APP.daq_0."+r, np.dtype('u4'))
+        print("{:>20} {:10} {:10}".format(r, r0, r1))
+
+def write_capture(path="./", region=[0,1]):
+    d=time.strftime("%m%d_%H%M%S")
+
+    for r, b, o in zip((0,1), NBPS*appdev.read("APP.daq_0.SAMPLES", np.dtype('u4')), [0,0x20000000]):
+        if r in region:
+            with open(path+"DAQCAPT_{}_R{}.bin".format(d, r), 'wb') as fp:
+                fp.write(daqmem[o:o+b])
+
+    return path+"DAQCAPT_{}_R{}.bin".format(d, r)
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser(description="DAQ capture and status")
+
+    parser.add_argument("--configure", action="store_true", help="Configure")
+    parser.add_argument("--stop", action="store_true", help="Send stop signal")
+    parser.add_argument("-t", action="store_true", help="Trigger DAQ")
+    parser.add_argument("-c", action="store_true", help="Dump memory to file")
+    parser.add_argument("-R", type=int, choices=(0,1), help="Region selector, default is both")
+
+    parser.add_argument("-N", type=int, default=1024, help="Number of samples to capture")
+    parser.add_argument("--path", type=str, default="./", help="Output path")
+
+    parser.add_argument("-s", action="store_true", help="Print status (after other actions)")
+
+    args=parser.parse_args()
+
+
+    if args.configure:
+        if args.R is None:
+            configure(3, args.N)
+        else:
+            configure(int(args.R)+1, args.N)
+
+    if args.stop:
+        stop()
+        exit(0)
+    else:
+        stop(False)
+
+    if args.t:
+        trigger()
+
+    if args.c:
+        if args.R is None:
+            fn=write_capture(args.path)
+        else:
+            fn=write_capture(args.path, [args.R, ])
+        print(fn)
+
+    if args.s:
+        status()
+
diff --git a/recipes-app/fofb-daqcapt/fofb-daqcapt_1.0.bb b/recipes-app/fofb-daqcapt/fofb-daqcapt_1.0.bb
new file mode 100644
index 0000000000000000000000000000000000000000..2e37e7fc8954eef6ed22aad8121ad391557535ef
--- /dev/null
+++ b/recipes-app/fofb-daqcapt/fofb-daqcapt_1.0.bb
@@ -0,0 +1,19 @@
+SUMMARY = "Simple python tool to use DESY DAQ"
+SECTION = "opt"
+LICENSE = "MIT"
+LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"
+
+SRC_URI = " file://daqcapt"
+
+RDEPENDS_${PN}=" deviceaccess-mapfiles deviceaccess-python-bindings"
+
+FILES_${PN}+="/usr/bin/daqcapt"
+FILES_${PN}+="/usr/bin/daqarchiver"
+
+
+do_install() {
+    # Add FPGA version reader and FoFb Configurator
+    install -d ${D}/usr/bin/
+    install -m 0755 ${WORKDIR}/daqcapt ${D}/usr/bin/daqcapt
+    install -m 0755 ${WORKDIR}/daqcapt ${D}/usr/bin/daqarchiver
+}
diff --git a/recipes-core/images/zup-image-soleil-fofb.bb b/recipes-core/images/zup-image-soleil-fofb.bb
index 5692de2e09c7731bcf4b15d64d6f99e863550ebc..c69ef873190a671cc7cddc3ead6916470bf18134 100644
--- a/recipes-core/images/zup-image-soleil-fofb.bb
+++ b/recipes-core/images/zup-image-soleil-fofb.bb
@@ -28,6 +28,7 @@ IMAGE_INSTALL_append = " deviceaccess"
 IMAGE_INSTALL_append = " deviceaccess-python-bindings"
 IMAGE_INSTALL_append = " fofb-opcua-server"
 IMAGE_INSTALL_append = " fofb-init"
+IMAGE_INSTALL_append = " fofb-daqcapt"
 IMAGE_INSTALL_append = " nfs-utils"
 
 # We do not need that