diff --git a/.gitignore b/.gitignore
index f2622e56f9efdb7bced341b635c049837c54fe5e..3041cdd6fdf6d57b130d817efce0261adcbc6467 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,8 +1,3 @@
-*.pyc
-# Created by https://www.toptal.com/developers/gitignore/api/python
-# Edit at https://www.toptal.com/developers/gitignore?templates=python
-
-weights/
 
 ### Python ###
 # Byte-compiled / optimized / DLL files
@@ -143,5 +138,7 @@ dmypy.json
 
 # Cython debug symbols
 cython_debug/
-weights/yolov3-veges_best.weights
-weights/yolov3-vattenhallen_best.weights
+
+# custom
+weights/
+creds.json
diff --git a/creds.example.json b/creds.example.json
new file mode 100644
index 0000000000000000000000000000000000000000..1271f97c31e3f14b81589449f19e2bf84bfbdee9
--- /dev/null
+++ b/creds.example.json
@@ -0,0 +1,4 @@
+{
+  "device_id": "device_NNNNN",
+  "token": ""
+}
diff --git a/farmbot_yolo/__init__.py b/farmbot_yolo/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e5e182d25dd041b7a6ade7e7367f6a642e15ea35
--- /dev/null
+++ b/farmbot_yolo/__init__.py
@@ -0,0 +1,6 @@
+from pathlib import Path
+
+HERE = Path(__file__).parent
+REPO = HERE.parent
+
+CREDS_PATH = REPO / "creds.json"
diff --git a/src/client.py b/farmbot_yolo/client.py
similarity index 97%
rename from src/client.py
rename to farmbot_yolo/client.py
index fbb44f6b886a2bb818f4b5f611af6797ee9789cc..4aa4344f18ed1381e7a08e7331f46078db7cd20f 100755
--- a/src/client.py
+++ b/farmbot_yolo/client.py
@@ -1,141 +1,141 @@
-'''
-Communicate with the server at the manufacturer
-Not useful for the local drive script
-'''
-import paho.mqtt.client as mqtt
-import json
-import time
-from uuid import uuid4 # 通用唯一标识符 ( Universally Unique Identifier )
-import logging #日志模块
-
-# values over max (and under min) will be clipped
-MAX_X = 2400
-MAX_Y = 1200
-MAX_Z = 469  # TODO test this one!
-
-def coord(x, y, z):
-  return {"kind": "coordinate", "args": {"x": x, "y": y, "z": z}} # 返回json 嵌套对象
-
-def move_request(x, y, z):
-  return {"kind": "rpc_request",  # 返回 json对象,对象内含数组
-          "args": {"label": ""},
-          "body": [{"kind": "move_absolute",
-                    "args": {"location": coord(x, y, z),
-                             "offset": coord(0, 0, 0),
-                             "speed": 100}}]}
-
-def take_photo_request():
-  return {"kind": "rpc_request",
-          "args": {"label": ""}, #label空着是为了在blocking_request中填上uuid,唯一识别码
-          "body": [{"kind": "take_photo", "args": {}}]}
-
-def clip(v, min_v, max_v):
-  if v < min_v: return min_v
-  if v > max_v: return max_v
-  return v
-
-class FarmbotClient(object):
-
-  def __init__(self, device_id, token):
-
-    self.device_id = device_id
-    self.client = mqtt.Client() # 类元素继承了另一个对象
-    self.client.username_pw_set(self.device_id, token) #传入 用户名和密码
-    self.client.on_connect = self._on_connect  #???
-    self.client.on_message = self._on_message
-
-    logging.basicConfig(level=logging.DEBUG,
-                        format="%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s",
-                        filename='farmbot_client.log',
-                        filemode='a')
-    console = logging.StreamHandler()
-    console.setLevel(logging.INFO)
-    console.setFormatter(logging.Formatter("%(asctime)s\t%(message)s"))
-    logging.getLogger('').addHandler(console)
-
-    self.connected = False
-    self.client.connect("clever-octopus.rmq.cloudamqp.com", 1883, 60)  #前面的url要运行按README.md中request_token.py 后面俩是TCP Port, Websocket Port
-    self.client.loop_start()
-    # 初始化函数里就会连接到服务器上,所以每次实例化一个新的client时,就已经连上了
-
-
-  def shutdown(self):
-    self.client.disconnect()
-    self.client.loop_stop()
-
-  def move(self, x, y, z):
-    x = clip(x, 0, MAX_X)
-    y = clip(y, 0, MAX_Y)
-    z = clip(z, 0, MAX_Z)
-    status_ok = self._blocking_request(move_request(x, y, z)) # 发请求
-    logging.info("MOVE (%s,%s,%s) [%s]", x, y, z, status_ok) #存日志,包括执行了什么“move x y z +返回值 ”
-
-  def take_photo(self):
-    # TODO: is this enough? it's issue a request for the photo, but is the actual capture async?
-    status_ok = self._blocking_request(take_photo_request())
-    logging.info("TAKE_PHOTO [%s]", status_ok)
-
-  def _blocking_request(self, request, retries_remaining=3):
-    if retries_remaining==0:
-      logging.error("< blocking request [%s] OUT OF RETRIES", request) #尝试3次,然后在日志中记录错误
-      return False
-
-    self._wait_for_connection() #在哪定义的?
-
-    # assign a new uuid for this attempt
-    self.pending_uuid = str(uuid4())
-    request['args']['label'] = self.pending_uuid #接收move_request函数的json对象
-    logging.debug("> blocking request [%s] retries=%d", request, retries_remaining)
-
-    # send request off 发送请求
-    self.rpc_status = None
-    self.client.publish("bot/" + self.device_id + "/from_clients", json.dumps(request))
-
-    # wait for response
-    timeout_counter = 600  # ~1min 等待1s
-    while self.rpc_status is None:          #这个self.rpc_status 是应答的flag
-      time.sleep(0.1)
-      timeout_counter -= 1
-      if timeout_counter == 0:
-        logging.warn("< blocking request TIMEOUT [%s]", request) #时间到了,无应答
-        return self._blocking_request(request, retries_remaining-1)
-    self.pending_uuid = None
-
-    # if it's ok, we're done!
-    if self.rpc_status == 'rpc_ok':
-      logging.debug("< blocking request OK [%s]", request)
-      return True
-
-    # if it's not ok, wait a bit and retry
-    if self.rpc_status == 'rpc_error':
-      logging.warn("< blocking request ERROR [%s]", request)
-      time.sleep(1)
-      return self._blocking_request(request, retries_remaining-1)
-
-    # unexpected state (???)
-    msg = "unexpected rpc_status [%s]" % self.rpc_status
-    logging.error(msg)
-    raise Exception(msg)
-
-
-  def _wait_for_connection(self):
-    # TODO: better way to do all this async event driven rather than with polling :/
-    timeout_counter = 600  # ~1min
-    while not self.connected: #用一个self.connected判断连上了没有,若没连上,等待
-      time.sleep(0.1)
-      timeout_counter -= 1
-      if timeout_counter == 0:
-        raise Exception("unable to connect")
-
-  def _on_connect(self, client, userdata, flags, rc):
-    logging.debug("> _on_connect")
-    self.client.subscribe("bot/" + self.device_id + "/from_device")
-    self.connected = True
-    logging.debug("< _on_connect")
-
-  def _on_message(self, client, userdata, msg):
-    resp = json.loads(msg.payload.decode())
-    if resp['args']['label'] != 'ping':
-      logging.debug("> _on_message [%s] [%s]", msg.topic, resp)
-    if msg.topic.endswith("/from_device") and resp['args']['label'] == self.pending_uuid:
-      self.rpc_status = resp['kind']
+'''
+Communicate with the server at the manufacturer
+Not useful for the local drive script
+'''
+import paho.mqtt.client as mqtt
+import json
+import time
+from uuid import uuid4 # 通用唯一标识符 ( Universally Unique Identifier )
+import logging #日志模块
+
+# values over max (and under min) will be clipped
+MAX_X = 2400
+MAX_Y = 1200
+MAX_Z = 469  # TODO test this one!
+
+def coord(x, y, z):
+  return {"kind": "coordinate", "args": {"x": x, "y": y, "z": z}} # 返回json 嵌套对象
+
+def move_request(x, y, z):
+  return {"kind": "rpc_request",  # 返回 json对象,对象内含数组
+          "args": {"label": ""},
+          "body": [{"kind": "move_absolute",
+                    "args": {"location": coord(x, y, z),
+                             "offset": coord(0, 0, 0),
+                             "speed": 100}}]}
+
+def take_photo_request():
+  return {"kind": "rpc_request",
+          "args": {"label": ""}, #label空着是为了在blocking_request中填上uuid,唯一识别码
+          "body": [{"kind": "take_photo", "args": {}}]}
+
+def clip(v, min_v, max_v):
+  if v < min_v: return min_v
+  if v > max_v: return max_v
+  return v
+
+class FarmbotClient(object):
+
+  def __init__(self, device_id, token):
+
+    self.device_id = device_id
+    self.client = mqtt.Client() # 类元素继承了另一个对象
+    self.client.username_pw_set(self.device_id, token) #传入 用户名和密码
+    self.client.on_connect = self._on_connect  #???
+    self.client.on_message = self._on_message
+
+    logging.basicConfig(level=logging.DEBUG,
+                        format="%(asctime)s\t%(name)s\t%(levelname)s\t%(message)s",
+                        filename='farmbot_client.log',
+                        filemode='a')
+    console = logging.StreamHandler()
+    console.setLevel(logging.INFO)
+    console.setFormatter(logging.Formatter("%(asctime)s\t%(message)s"))
+    logging.getLogger('').addHandler(console)
+
+    self.connected = False
+    self.client.connect("clever-octopus.rmq.cloudamqp.com", 1883, 60)  #前面的url要运行按README.md中request_token.py 后面俩是TCP Port, Websocket Port
+    self.client.loop_start()
+    # 初始化函数里就会连接到服务器上,所以每次实例化一个新的client时,就已经连上了
+
+
+  def shutdown(self):
+    self.client.disconnect()
+    self.client.loop_stop()
+
+  def move(self, x, y, z):
+    x = clip(x, 0, MAX_X)
+    y = clip(y, 0, MAX_Y)
+    z = clip(z, 0, MAX_Z)
+    status_ok = self._blocking_request(move_request(x, y, z)) # 发请求
+    logging.info("MOVE (%s,%s,%s) [%s]", x, y, z, status_ok) #存日志,包括执行了什么“move x y z +返回值 ”
+
+  def take_photo(self):
+    # TODO: is this enough? it's issue a request for the photo, but is the actual capture async?
+    status_ok = self._blocking_request(take_photo_request())
+    logging.info("TAKE_PHOTO [%s]", status_ok)
+
+  def _blocking_request(self, request, retries_remaining=3):
+    if retries_remaining==0:
+      logging.error("< blocking request [%s] OUT OF RETRIES", request) #尝试3次,然后在日志中记录错误
+      return False
+
+    self._wait_for_connection() #在哪定义的?
+
+    # assign a new uuid for this attempt
+    self.pending_uuid = str(uuid4())
+    request['args']['label'] = self.pending_uuid #接收move_request函数的json对象
+    logging.debug("> blocking request [%s] retries=%d", request, retries_remaining)
+
+    # send request off 发送请求
+    self.rpc_status = None
+    self.client.publish("bot/" + self.device_id + "/from_clients", json.dumps(request))
+
+    # wait for response
+    timeout_counter = 600  # ~1min 等待1s
+    while self.rpc_status is None:          #这个self.rpc_status 是应答的flag
+      time.sleep(0.1)
+      timeout_counter -= 1
+      if timeout_counter == 0:
+        logging.warn("< blocking request TIMEOUT [%s]", request) #时间到了,无应答
+        return self._blocking_request(request, retries_remaining-1)
+    self.pending_uuid = None
+
+    # if it's ok, we're done!
+    if self.rpc_status == 'rpc_ok':
+      logging.debug("< blocking request OK [%s]", request)
+      return True
+
+    # if it's not ok, wait a bit and retry
+    if self.rpc_status == 'rpc_error':
+      logging.warn("< blocking request ERROR [%s]", request)
+      time.sleep(1)
+      return self._blocking_request(request, retries_remaining-1)
+
+    # unexpected state (???)
+    msg = "unexpected rpc_status [%s]" % self.rpc_status
+    logging.error(msg)
+    raise Exception(msg)
+
+
+  def _wait_for_connection(self):
+    # TODO: better way to do all this async event driven rather than with polling :/
+    timeout_counter = 600  # ~1min
+    while not self.connected: #用一个self.connected判断连上了没有,若没连上,等待
+      time.sleep(0.1)
+      timeout_counter -= 1
+      if timeout_counter == 0:
+        raise Exception("unable to connect")
+
+  def _on_connect(self, client, userdata, flags, rc):
+    logging.debug("> _on_connect")
+    self.client.subscribe("bot/" + self.device_id + "/from_device")
+    self.connected = True
+    logging.debug("< _on_connect")
+
+  def _on_message(self, client, userdata, msg):
+    resp = json.loads(msg.payload.decode())
+    if resp['args']['label'] != 'ping':
+      logging.debug("> _on_message [%s] [%s]", msg.topic, resp)
+    if msg.topic.endswith("/from_device") and resp['args']['label'] == self.pending_uuid:
+      self.rpc_status = resp['kind']
diff --git a/src/darknet.py b/farmbot_yolo/darknet.py
similarity index 100%
rename from src/darknet.py
rename to farmbot_yolo/darknet.py
diff --git a/src/detect.py b/farmbot_yolo/detect.py
similarity index 100%
rename from src/detect.py
rename to farmbot_yolo/detect.py
diff --git a/src/gripper.py b/farmbot_yolo/gripper.py
similarity index 100%
rename from src/gripper.py
rename to farmbot_yolo/gripper.py
diff --git a/src/location.py b/farmbot_yolo/location.py
similarity index 100%
rename from src/location.py
rename to farmbot_yolo/location.py
diff --git a/src/main.py b/farmbot_yolo/main.py
similarity index 98%
rename from src/main.py
rename to farmbot_yolo/main.py
index c68b26834577376f28ee6adbdeb8bf66f9ee69db..3ae2d0fb5f624a9d5d75b607ea641982f252c25a 100644
--- a/src/main.py
+++ b/farmbot_yolo/main.py
@@ -13,9 +13,9 @@ from numpy import sqrt
 from pandas import DataFrame
 from gripper import gripper_close, gripper_open
 
-from move import *
-from detect import *
-from location import *
+from farmbot_yolo.move import *
+from farmbot_yolo.detect import *
+from farmbot_yolo.location import *
 
 _LOG = getLogger(__name__)
 
@@ -184,4 +184,4 @@ if __name__ == '__main__':
         basicConfig(filename=arguments.log, level=DEBUG)
     else:
         basicConfig(filename=arguments.log, level=INFO)
-    main(arguments)
\ No newline at end of file
+    main(arguments)
diff --git a/src/move.py b/farmbot_yolo/move.py
similarity index 95%
rename from src/move.py
rename to farmbot_yolo/move.py
index 3768b918c06a894d58bd0311faac64012f0eed35..85eeba094f630110b2bee97ae25dcb72061daa83 100644
--- a/src/move.py
+++ b/farmbot_yolo/move.py
@@ -20,7 +20,7 @@ from datetime import timezone, datetime
 from dateutil.parser import parse
 from requests import get, delete
 
-import creds
+from creds import read_credentials
 from client import FarmbotClient
 
 
@@ -55,6 +55,7 @@ def scan(img_path: Path, location_path: Path, # smaller delta
     Output: none
     '''
     opts = Opts(min_x, max_x, min_y, max_y, delta, offset, flag)
+    creds = read_credentials()
 
     pts = []
     sweep_y_negative = False
@@ -72,7 +73,7 @@ def scan(img_path: Path, location_path: Path, # smaller delta
         Logger.info('Run without sweep')
         exit()
 
-    client = FarmbotClient(creds.device_id, creds.token)
+    client = FarmbotClient(creds["device_id"], creds["token"])
     client.move(0, 0, _SWEEEP_HEIGHT) # ensure moving from original 
     for x, y in pts:
         client.move(x, y, _SWEEEP_HEIGHT) # move camera
@@ -101,7 +102,8 @@ def simple_move(x: int, y: int, z: int) -> None:
     Input: x, y,z: destination point
            photo: take a pic or not
     '''
-    client = FarmbotClient(creds.device_id, creds.token)
+    creds = read_credentials()
+    client = FarmbotClient(creds["device_id"], creds["token"])
     client.move(x, y, z)  
     client.shutdown()
     return None
diff --git a/farmbot_yolo/read_credentials.py b/farmbot_yolo/read_credentials.py
new file mode 100644
index 0000000000000000000000000000000000000000..6a310c218fd6b02fc87482c77f9d2a750d0979d8
--- /dev/null
+++ b/farmbot_yolo/read_credentials.py
@@ -0,0 +1,13 @@
+import json
+from typing import Dict
+
+from farmbot_yolo import CREDS_PATH
+
+def read_credentials() -> Dict[str]:
+    if not CREDS_PATH.exists():
+        raise RuntimeError("No creds.json found in project root. Run python -m farmbot_yolo.get_credentials.")
+
+    with CREDS_PATH.open(mode="r") as fp:
+        creds_dict = json.load(fp)
+
+    return creds_dict
diff --git a/farmbot_yolo/request_token.py b/farmbot_yolo/request_token.py
new file mode 100755
index 0000000000000000000000000000000000000000..8c1622e25054401f12f4e89b1289d2b28de17c46
--- /dev/null
+++ b/farmbot_yolo/request_token.py
@@ -0,0 +1,37 @@
+"""Request a token to store in creds.json."""
+
+import argparse
+import json
+from urllib import request
+
+from farmbot_yolo import CREDS_PATH
+
+
+def request_token(email: str, password: str):
+    auth_info = {'user': {'email': email, 'password': password}}
+
+    req = request.Request('https://my.farmbot.io/api/tokens')
+    req.add_header('Content-Type', 'application/json')
+    response = request.urlopen(req, data=json.dumps(auth_info).encode('utf-8'))
+
+    token_info = json.loads(response.read().decode())
+
+    print("mqtt host [%s]" % token_info['token']['unencoded']['mqtt'])
+
+    creds_dict = {"device_id": token_info["token"]["unencoded"]["bot"],
+                  "token": token_info['token']['encoded']}
+
+    print("rewriting creds.json")
+    with CREDS_PATH.open(mode="w") as fp:
+        json.dump(creds_dict, fp)
+
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
+    parser.add_argument('--email', type=str, help="user email for token request")
+    parser.add_argument('--password', type=str, help="user password for token request")
+    opts = parser.parse_args()
+
+    print("opts %s" % opts)
+
+    request_token(opts.email, opts.password)
diff --git a/src/try.py b/farmbot_yolo/try.py
similarity index 100%
rename from src/try.py
rename to farmbot_yolo/try.py
diff --git a/src/creds.py b/src/creds.py
deleted file mode 100644
index 46be62284eae0f0b7881a135b128d16fc5663ef8..0000000000000000000000000000000000000000
--- a/src/creds.py
+++ /dev/null
@@ -1,2 +0,0 @@
-device_id="***REMOVED***"
-token="***REMOVED***"
diff --git a/src/request_token.py b/src/request_token.py
deleted file mode 100755
index ef793f927a00fa57635ec4f13c74816e0a939bbd..0000000000000000000000000000000000000000
--- a/src/request_token.py
+++ /dev/null
@@ -1,28 +0,0 @@
-#!/usr/bin/env python3
-
-# request a token (for creds.py)
-
-import argparse
-import json
-from urllib import request
-
-parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
-parser.add_argument('--email', type=str, help="user email for token request")
-parser.add_argument('--password', type=str, help="user password for token request")
-opts = parser.parse_args()
-print("opts %s" % opts)
-
-auth_info = {'user': {'email': opts.email, 'password': opts.password }}
-
-req = request.Request('https://my.farmbot.io/api/tokens')
-req.add_header('Content-Type', 'application/json')
-response = request.urlopen(req, data=json.dumps(auth_info).encode('utf-8'))
-
-token_info = json.loads(response.read().decode())
-
-print("mqtt host [%s]" % token_info['token']['unencoded']['mqtt'])
-
-print("rewriting creds.py")
-with open("creds.py", "w") as f:
-  f.write("device_id=\"%s\"\n" % token_info['token']['unencoded']['bot'])
-  f.write("token=\"%s\"\n" % token_info['token']['encoded'])