diff --git a/cameras/__init__.py b/cameras/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/cameras/cameraclient.py b/cameras/cameraclient.py
new file mode 100644
index 0000000000000000000000000000000000000000..809bfaf6153db3c997bd9d6f9fb5629de1e93f78
--- /dev/null
+++ b/cameras/cameraclient.py
@@ -0,0 +1,138 @@
+from types import SimpleNamespace
+from .waiting_epics import PV, caput, caget
+from .utils import intify
+
+
+class CameraClient:
+
+    def __init__(self, name):
+        self.name = name
+        pv_roi_xmin = PV(name + ":REGIONX_START")
+        pv_roi_xmax = PV(name + ":REGIONX_END")
+        pv_roi_ymin = PV(name + ":REGIONY_START")
+        pv_roi_ymax = PV(name + ":REGIONY_END")
+
+        self.pvs = SimpleNamespace(
+            xmin=pv_roi_xmin,
+            xmax=pv_roi_xmax,
+            ymin=pv_roi_ymin,
+            ymax=pv_roi_ymax
+        )
+
+        self.max_roi = self.get_max_roi()
+
+    def __repr__(self):
+        tn = type(self).__name__
+        fn = self.name
+        return f"{tn}(\"{fn}\")"
+
+    @property
+    def status(self):
+        head = repr(self)
+        print(head)
+        print("-" * len(head))
+        channels = ("WARNCODE", "ERRCODE", "STATUSCODE", "BUSY", "CAMRATE", "FILERATE")
+        maxlen = max(len(ch) for ch in channels)
+        for ch in channels:
+            fch = self.name +":" + ch
+            val = caget(fch)
+
+            line = ch + ": "
+            line = line.ljust(maxlen + 2)
+            line += str(val)
+            print(line)
+
+        print("-" * len(head))
+        roi  = self.get_roi()
+        full = self.get_max_roi()
+        print(f"roi:  {roi}")
+        print(f"full: {full}")
+
+
+    def restart(self):
+        self.off()
+        self.offline()
+        self.init()
+        sleep(0.01)
+        self.running()
+
+    def offline(self):
+        caput(self.name + ":INIT", 0) # OFFLINE
+
+    def init(self):
+        caput(self.name + ":INIT", 1) # INIT
+
+    def off(self):
+        caput(self.name + ":CAMERA", 0) # OFF
+
+    def running(self):
+        caput(self.name + ":CAMERA", 1) # RUNNING
+
+    def reset_roi(self):
+        caput(self.name + ":RESETROI.PROC", 1) # Reset ROI
+
+    def set_parameters(self):
+        caput(self.name + ":SET_PARAM", 1) # Set Parameters
+
+    def clear_buffer(self):
+        caput(self.name + ":CLEARMEM", 1) # Clear Buffer
+
+    def get_max_roi(self):
+        current_roi = self.get_roi()
+        self.reset_roi()
+        max_roi = self.get_roi()
+        self._set_roi(*current_roi)
+        return max_roi
+
+    def get_roi(self):
+        xmin = self.pvs.xmin.get()
+        xmax = self.pvs.xmax.get()
+        ymin = self.pvs.ymin.get()
+        ymax = self.pvs.ymax.get()
+        return intify(xmin, xmax, ymin, ymax)
+
+
+    def set_roi(self, xmin, xmax, ymin, ymax, debug=False):
+        if debug:
+            asked = (xmin, xmax, ymin, ymax)
+
+        xminmin, xmaxmax, yminmin, ymaxmax = self.max_roi
+
+        xmin = max(xmin, xminmin)
+        xmax = min(xmax, xmaxmax)
+        ymin = max(ymin, yminmin)
+        ymax = min(ymax, ymaxmax)
+
+        ymindelta = ymin - yminmin
+        ymaxdelta = ymaxmax - ymax
+        ydelta = min(ymindelta, ymaxdelta)
+
+        ymin = yminmin + ydelta
+        ymax = ymaxmax - ydelta
+
+        if debug:
+            adjusted = (xmin, xmax, ymin, ymax)
+            print("   ", asked, "\n-> ", adjusted, ":", ydelta, ymindelta, ymaxdelta)
+
+        self._set_roi(xmin, xmax, ymin, ymax, debug=debug)
+
+
+    def _set_roi(self, xmin, xmax, ymin, ymax, debug=False):
+        xmin, xmax, ymin, ymax = intify(xmin, xmax, ymin, ymax)
+
+        self.off()
+
+        self.pvs.xmin.put(xmin)
+        self.pvs.xmax.put(xmax)
+        self.pvs.ymin.put(ymin)
+        self.pvs.ymax.put(ymax)
+
+        self.set_parameters()
+        self.clear_buffer()
+        self.running()
+
+        if debug:
+            print("-->", self.get_roi(), "\n")
+
+
+
diff --git a/cameras/utils.py b/cameras/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..c41648e76eb231ee43c7d5a39af32264c0da0e4c
--- /dev/null
+++ b/cameras/utils.py
@@ -0,0 +1,6 @@
+
+def intify(*args):
+    return [int(round(i)) for i in args]
+
+
+
diff --git a/cameras/waiting_epics.py b/cameras/waiting_epics.py
new file mode 100644
index 0000000000000000000000000000000000000000..03dd9cf8d0bcc97ce5fa038ab75074573c3d1f78
--- /dev/null
+++ b/cameras/waiting_epics.py
@@ -0,0 +1,14 @@
+import epics
+from epics import caget
+
+
+def caput(*args, wait=True, **kwargs):
+    return epics.caput(*args, wait=wait, **kwargs)
+
+
+class PV(epics.PV):
+
+    def put(self, *args, wait=True, **kwargs):
+        super().put(*args, wait=wait, **kwargs)
+
+
diff --git a/clicamesh.py b/clicamesh.py
new file mode 100755
index 0000000000000000000000000000000000000000..f70cde820d39355b990a050f1d20550eec3ae386
--- /dev/null
+++ b/clicamesh.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python
+
+import argparse
+from time import sleep
+
+from cameras.cameraclient import CameraClient
+
+
+def main():
+    commands = ["restart", "roi", "status", "test"]
+    commands.sort()
+    printable_commands = ", ".join(commands)
+
+    parser = argparse.ArgumentParser(description="CLI Cam (esh)")
+    parser.add_argument("-c", "--camera", help="camera name", default="SATES24-CAMS161-M1")
+    parser.add_argument("-v", "--verbose", help="verbose output", action="store_true")
+
+    subparsers = parser.add_subparsers(title="commands", dest="command", help=printable_commands)
+    subparser_restart = subparsers.add_parser("restart", description="Restart camera server")
+    subparser_roi     = subparsers.add_parser("roi", description="Set ROI")
+    subparser_status  = subparsers.add_parser("status", description="Print status")
+    subparser_test    = subparsers.add_parser("test", description="Test setting ROIs")
+
+    for sp in subparsers.choices.values():
+        sp.add_argument("-v", "--verbose", help="verbose output", action="store_true")
+
+    subparser_restart.add_argument("-r", "--reset", help="reset ROI", action="store_true")
+
+    subparser_roi.add_argument("xmin", type=int, help="x min")
+    subparser_roi.add_argument("xmax", type=int, help="x max")
+    subparser_roi.add_argument("ymin", type=int, help="y min")
+    subparser_roi.add_argument("ymax", type=int, help="y max")
+
+    clargs = parser.parse_args()
+    command = clargs.command
+    if command is None:
+        parser.print_help()
+        raise SystemExit(1)
+
+    camera = CameraClient(clargs.camera)
+
+    if clargs.verbose:
+        print(camera)
+        print(f"command: {command}")
+#        print(clargs)
+
+    if command == "restart":
+        do_restart(camera, clargs)
+    elif command == "roi":
+        do_roi(camera, clargs)
+    elif command == "test":
+        do_test(camera, clargs)
+    elif command == "status":
+        do_status(camera, clargs)
+    else:
+        print(f"nothing assigned to command: {command}")
+        parser.print_help()
+        raise SystemExit(2)
+
+
+
+def do_restart(camera, clargs):
+    if not clargs.reset:
+        roi = camera.get_roi()
+    camera.restart()
+    if not clargs.reset:
+        camera.set_roi(*roi, debug=clargs.verbose) #TODO: workaround! why does the first try always set to: [101, 2400, 100, 2061]?
+        camera.set_roi(*roi, debug=clargs.verbose)
+
+def do_roi(camera, clargs):
+    camera.set_roi(clargs.xmin, clargs.xmax, clargs.ymin, clargs.ymax, debug=clargs.verbose)
+
+def do_status(camera, clargs):
+    camera.status
+
+def do_test(camera, _clargs):
+    camera.reset_roi() # [1, 2560, 1, 2160]
+    print(camera.get_roi())
+
+    for i in (1, 10, 100, 200, 300, 999, 1000, 1001):
+        camera.set_roi(i, 1500, i, 1900, debug=True)
+
+
+
+
+
+if __name__ == "__main__":
+    main()
+
+    #CA client library tcp receive thread terminating due to a non-standard C++ exception
+    #FATAL: exception not rethrown
+    #Aborted (core dumped)
+
+    # "solved" by
+    sleep(0.0001)
+
+
+