diff --git a/imgs/20220612/161620.jpg b/imgs/20220612/161620.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..10a66a2cc19d180608f0692835a07d49fb1086aa
Binary files /dev/null and b/imgs/20220612/161620.jpg differ
diff --git a/imgs/image.db b/imgs/image.db
new file mode 100644
index 0000000000000000000000000000000000000000..9e82a62e022d8472ff2ef6b8310e62eb7987dc70
Binary files /dev/null and b/imgs/image.db differ
diff --git a/src/gripper.py b/src/gripper.py
index aff5b9c44be8359d8ab606b02d396094f67e33ce..27b72215533b7f2414874aca6fb8068055dd86e7 100755
--- a/src/gripper.py
+++ b/src/gripper.py
@@ -2,9 +2,9 @@
 
 TODO: Integrate with FarmbotClient or FarmbotYoloClient
 """
-from client import FarmbotClient
-from creds import device_id
-from creds import token
+from utils.client import FarmbotClient
+from utils.creds import device_id
+from utils.creds import token
 
 GRIPPER_PIN = 12
 GRIPPER_OPEN_STATE = 0
diff --git a/src/imgs/20220612/161646.jpg b/src/imgs/20220612/161646.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..9960fdf894c0a8643f94b6145d02ff200d002830
Binary files /dev/null and b/src/imgs/20220612/161646.jpg differ
diff --git a/src/imgs/20220612/161937.jpg b/src/imgs/20220612/161937.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..3ea63ebdd449234c49ee086df68481156476d001
Binary files /dev/null and b/src/imgs/20220612/161937.jpg differ
diff --git a/src/main.py b/src/main.py
index c68b26834577376f28ee6adbdeb8bf66f9ee69db..d719b2a8023d4f8d9747b4c0ec0a6f81bd68707e 100644
--- a/src/main.py
+++ b/src/main.py
@@ -48,7 +48,6 @@ def remove_overlap(table_coordinate:DataFrame, tolerance=50.00)->DataFrame:
     return table_coordinate              
 
 
-
 def remove_temp(path: Path)-> None:
     '''
     Clean temporary files, i.e., photos, location.txt, annotations
diff --git a/src/move.py b/src/move.py
index 3768b918c06a894d58bd0311faac64012f0eed35..afa038ffd45ee74d472ff4473201a08e631858fc 100644
--- a/src/move.py
+++ b/src/move.py
@@ -8,6 +8,7 @@ Note: it is for remote server, can ben replaced by a local script
 from argparse import ArgumentParser
 from logging import getLogger
 from os import path, makedirs, system
+import sys
 from time  import sleep, strftime, time
 #from serial import Serial, PARITY_NONE, STOPBITS_ONE, EIGHTBITS 
 from requests.api import delete
@@ -20,8 +21,8 @@ from datetime import timezone, datetime
 from dateutil.parser import parse
 from requests import get, delete
 
-import creds
-from client import FarmbotClient
+import utils.creds as creds
+from utils.client import FarmbotClient
 
 
 _SWEEEP_HEIGHT = 0
@@ -40,7 +41,7 @@ class Opts:
     
 
 def scan(img_path: Path, location_path: Path, # smaller delta
-         min_x=0, max_x=1300, min_y=0, max_y=1000, delta=1000, offset=0, flag=True) -> List: #里面的数字需要重新测量
+         min_x=0, max_x=1175, min_y=0, max_y=974, delta=300, offset=0, flag=True) -> List: #里面的数字需要重新测量
     '''
     scan the bed at a certain height, first move along x axis, then y, like a zig zag;
     Taking pictures and record the location of the camera that corresponds to the picture
@@ -76,7 +77,8 @@ def scan(img_path: Path, location_path: Path, # smaller delta
     client.move(0, 0, _SWEEEP_HEIGHT) # ensure moving from original 
     for x, y in pts:
         client.move(x, y, _SWEEEP_HEIGHT) # move camera
-        take_photo(img_path)
+        #take_photo(img_path)
+        client.take_photo()
     client.shutdown()
     # write to img/location
     with open(path.join(location_path, "location.txt"), 'w') as f:
@@ -85,14 +87,21 @@ def scan(img_path: Path, location_path: Path, # smaller delta
     return None 
 
 
-def take_photo(img_path: Path):
-    HERE = path.dirname(__file__)
-    IMG_DIR = path.join(HERE, img_path)
+def take_photo():
+    client = FarmbotClient(creds.device_id, creds.token)
+    client.take_photo()
+    # download image
+    system('python ./utils/download.py')
+
+
+# def take_photo(img_path: Path):
+#     HERE = path.dirname(__file__)
+#     IMG_DIR = path.join(HERE, img_path)
 
-    with request.urlopen('http://localhost:8080/?action=snapshot') as photo:
-        filename = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") + ".jpg"
-        with open(path.join(IMG_DIR, filename), mode="wb") as save_file:
-            save_file.write(photo.read())
+#     with request.urlopen('http://localhost:8080/?action=snapshot') as photo:
+#         filename = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") + ".jpg"
+#         with open(path.join(IMG_DIR, filename), mode="wb") as save_file:
+#             save_file.write(photo.read())
 
 
 def simple_move(x: int, y: int, z: int) -> None: 
@@ -145,12 +154,14 @@ if __name__ == '__main__':
         destination_x = int(input('X:'))
         destination_y = int(input('Y:'))
         destination_z = int(input('Z:'))
-        photo = True if input('Take a photo or not?[Y/N]:') == 'Y' else False 
         simple_move_start = time()
-        simple_move(destination_x, destination_y, destination_z, photo)
+        simple_move(destination_x, destination_y, destination_z)
         Logger.info(f'time cost {time()-simple_move_start}')
     elif arguments.mode == 2:
         scan(arguments.photo, arguments.locations, flag=False)
+        #take_photo(arguments.photo)
+    elif arguments.mode == 3:
+        take_photo()
     else:
         Logger.error('Wrong mode number {arguments.mode}')
 
diff --git a/src/client.py b/src/utils/client.py
similarity index 96%
rename from src/client.py
rename to src/utils/client.py
index 22f34607d44c4b73a66ebc2fff10fcbf865dc4d7..8768294e2024a1470054068fbbb991d74c1cef33 100755
--- a/src/client.py
+++ b/src/utils/client.py
@@ -33,12 +33,12 @@ def read_pin_request(pin_number, pin_mode="digital"):
 
   return {"kind": "rpc_request",
           "args": {"label": ""},
-          "body": [{"kind": "read_pin"
+          "body": [{"kind": "read_pin",
                     "args": {
                       "label": "pin" + str(pin_number),
                       "pin_mode": modes[pin_mode] or (modes["digital"]),
                       "pin_number": pin_number
-                    }
+                    }}]}
 
 def write_pin_request(pin_number, pin_value, pin_mode="digital"):
   modes = {"digital": 0, "analog": 1}
@@ -58,7 +58,7 @@ def write_pin_request(pin_number, pin_value, pin_mode="digital"):
                   ]
           }
 
-def toggle_pin_request(pin_number)
+def toggle_pin_request(pin_number):
   return {"kind": "rpc_request",
           "args": {"label": ""},
           "body": [{"kind": "toggle_pin",
diff --git a/src/utils/creds.py b/src/utils/creds.py
new file mode 100644
index 0000000000000000000000000000000000000000..46be62284eae0f0b7881a135b128d16fc5663ef8
--- /dev/null
+++ b/src/utils/creds.py
@@ -0,0 +1,2 @@
+device_id="***REMOVED***"
+token="***REMOVED***"
diff --git a/src/utils/download.py b/src/utils/download.py
new file mode 100644
index 0000000000000000000000000000000000000000..f47961cde83b142e87ed77492d22e2e148a09930
--- /dev/null
+++ b/src/utils/download.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python3
+
+from datetime import timezone
+from dateutil.parser import parse
+from image_db import ImageDB
+import utils.creds as creds
+import json
+import os
+import requests
+import sys
+import time
+
+# note: download only returns 100 at a time!
+# note: we are currently ignoreing placeholders
+
+REQUEST_HEADERS = {'Authorization': 'Bearer ' + creds.token, 'content-type': "application/json"}
+
+image_db = ImageDB()
+image_db.create_if_required()
+
+while True:
+  response = requests.get('https://my.farmbot.io/api/images', headers=REQUEST_HEADERS)
+  images = response.json()
+  print("#images", len(images))
+  if len(images) == 0:
+    exit()
+
+  at_least_one_dup = False
+  for image_info in images:
+
+    if image_db.has_record_for_farmbot_id(image_info['id']):
+      print("IGNORE! dup", image_info['id'])
+      requests.delete("https://my.farmbot.io/api/images/%d" % image_info['id'],
+                      headers=REQUEST_HEADERS)
+      at_least_one_dup = True
+      continue
+
+    if 'placehold.it' in image_info['attachment_url']:
+      print("IGNORE! placeholder", image_info['id'])
+      continue
+
+    # convert date time of capture from UTC To AEDT and extract
+    # a simple string version for local image filename
+    dts = parse(image_info['attachment_processed_at'])
+    dts = dts.replace(tzinfo=timezone.utc).astimezone(tz=None)
+    local_img_dir = "imgs/%s" % dts.strftime("%Y%m%d")
+    if not os.path.exists(local_img_dir):
+      os.makedirs(local_img_dir)
+    local_img_name = "%s/%s.jpg" % (local_img_dir, dts.strftime("%H%M%S"))
+    print(">", local_img_name)
+
+    # download image from google storage and save locally
+    captured_img_name = image_info['meta']['name']
+    if captured_img_name.startswith("/tmp/images"):
+      req = requests.get(image_info['attachment_url'], allow_redirects=True)
+      open(local_img_name, 'wb').write(req.content)
+
+    # add entry to db
+    image_db.insert(image_info, dts, local_img_name)
+
+    # post delete from cloud storage
+    requests.delete("https://my.farmbot.io/api/images/%d" % image_info['id'],
+                    headers=REQUEST_HEADERS)
+
+  if at_least_one_dup:
+    print("only at least one dup; give DELETEs a chance to work")
+    time.sleep(2)
\ No newline at end of file
diff --git a/src/utils/image_db.py b/src/utils/image_db.py
new file mode 100644
index 0000000000000000000000000000000000000000..6153ba57d06a8ad250e27949ac8e0236819cade4
--- /dev/null
+++ b/src/utils/image_db.py
@@ -0,0 +1,100 @@
+# image db helper
+
+#from calculate_detections import Detection
+import sqlite3
+import json
+
+class ImageDB(object):
+  def __init__(self, image_db_file='/home/bot/farmbot/farmbot_yolo/imgs/image.db', check_same_thread=True):
+    self.conn = sqlite3.connect(image_db_file, check_same_thread=check_same_thread)
+
+  def create_if_required(self):
+    # called once to create db
+    c = self.conn.cursor()
+    try:
+      c.execute('''create table imgs (
+                        id integer primary key autoincrement,
+                        farmbot_id integer,
+                        capture_time text,
+                        x integer,
+                        y integer,
+                        z integer,
+                        api_response text,
+                        filename text,
+                        detections_run integer
+                   )''')
+    except sqlite3.OperationalError:
+      # assume table already exists? clumsy...
+      pass
+    try:
+      c.execute('''create table detections (
+                        id integer primary key autoincrement,
+                        img_id integer,
+                        theta integer,
+                        entity text,
+                        score real,
+                        x0 integer,
+                        y0 integer,
+                        x1 integer,
+                        y1 integer
+              )''')
+    except sqlite3.OperationalError:
+      # assume table already exists? clumsy...
+      pass
+
+
+  def has_record_for_farmbot_id(self, farmbot_id):
+    c = self.conn.cursor()
+    c.execute("select farmbot_id from imgs where farmbot_id=?", (farmbot_id,))
+    return c.fetchone() is not None
+
+  def imgs_for_coords(self, x, y, z):
+    c = self.conn.cursor()
+    if z is None:
+      c.execute("select id, x, y, z, filename from imgs where x=? and y=? order by capture_time", (x, y, ))
+    else:
+      c.execute("select id, x, y, z, filename from imgs where x=? and y=? and z=? order by capture_time", (x, y, z, ))
+    return c.fetchall()
+
+  def img_id_for_filename(self, filename):
+    c = self.conn.cursor()
+    c.execute("select id from imgs where filename=?", (filename,))
+    f = c.fetchone()
+    if f is None: return f
+    return f[0]
+
+  def insert(self, api_response, dts, filename):
+    farmbot_id = api_response['id']
+    capture_time = dts.strftime("%Y-%m-%d %H:%M:%S")
+    x, y, z = map(int, [api_response['meta'][c] for c in ['x', 'y', 'z']])
+    c = self.conn.cursor()
+    insert_values = (farmbot_id, capture_time, x, y, z, json.dumps(api_response), filename)
+    c.execute("insert into imgs (farmbot_id, capture_time, x, y, z, api_response, filename) values (?,?,?,?,?,?,?)", insert_values)
+    self.conn.commit()
+
+  def x_y_counts(self, min_c=1):
+    c = self.conn.cursor()
+    c.execute("select x, y, count(*) as c from imgs group by x, y having c >= ?", (min_c,))
+    records = c.fetchall()
+    return sorted(records, key=lambda r: -r[2])  # return sorted by freq
+
+  def img_ids_without_detections(self):
+    c = self.conn.cursor()
+    c.execute("select id, filename from imgs where detections_run is null")
+    return c.fetchall()
+
+  def insert_detections(self, img_id, detections):
+    c = self.conn.cursor()
+    if len(detections) > 0:
+      values = [(img_id, d.theta, d.entity, d.score, d.x0, d.y0, d.x1, d.y1) for d in detections]
+      c.executemany("insert into detections (img_id, theta, entity, score, x0, y0, x1, y1) values (?,?,?,?,?,?,?,?)", values)
+    c.execute("update imgs set detections_run=1 where id=?", (img_id,))
+    self.conn.commit()
+
+  def detections_for_img(self, filename):
+    c = self.conn.cursor()
+    c.execute("""select d.theta, d.entity, d.score, d.x0, d.y0, d.x1, d.y1
+                 from imgs i join detections d on i.id=d.img_id
+                 where i.filename=?
+                 order by score desc""", (filename,))
+    #return list(map(Detection._make, c.fetchall()))
\ No newline at end of file
diff --git a/src/request_token.py b/src/utils/request_token.py
similarity index 100%
rename from src/request_token.py
rename to src/utils/request_token.py