diff --git a/.flake8 b/.flake8
new file mode 100644
index 0000000000000000000000000000000000000000..8dd399ab55bce8a4a1ddfd0c98170f771f6e5c35
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,3 @@
+[flake8]
+max-line-length = 88
+extend-ignore = E203
diff --git a/README.md b/README.md
index 2620539fb913381262bb3767e0c756bbbd2b64c6..f41979e081b658220547c68d932993c64e6b13e0 100644
--- a/README.md
+++ b/README.md
@@ -1,75 +1,132 @@
 # A YOLO-based fruit picking system for FarmBot
+
 ## Brief introduction
+
 https://pjreddie.com/darknet/yolo/
-## YOLO, implemented by Darknet
-Darknet is a deep learning framework created by the author of YOLO. It is written in C and provides nessary Python APIs for object detection. 
 
+## YOLO, implemented using Darknet
 
+Darknet is a deep learning framework created by the author of YOLO. It is
+written in C and provides necessary Python APIs for object detection.
 
 ### Detector Training
-Check the [instruction](https://github.com/AlexeyAB/darknet#how-to-train-to-detect-your-custom-objects) for how to train the YOLO detector.
 
-## Camera Calibration
-Think YOLO as a black box, it consumes photos that contains objects or not and outputs objects' positions in the pixel coordinate. However, we want to know their positions on the planting bed, namely global coordinate. That is why we need camera calibration. This step was done seperately on MATLAB and offered us the intrinsic matrix of the camera. You **do not need to redo** this unless you want to change to a new model of camera. Check MATLAB [instruction](https://ww2.mathworks.cn/help/vision/ug/using-the-single-camera-calibrator-app.html;jsessionid=1a474c11e3e6063620885c4ae708) for how to use the camera calibration app.
+Check the
+[instruction](https://github.com/AlexeyAB/darknet#how-to-train-to-detect-your-custom-objects)
+for how to train the YOLO detector.
 
-**Note**: The result have to be transferred to `struct`, a MATLAB data type, before saving.  
+## Camera Calibration
 
-If you follow the instruction above, you will end up with a MATLAB object, `cameraParameters`. If you save this to a .mat file and read it by Scipy, you will unfortunately end up with an empty dictionary. You should always use the function `toStruct` to convert it to a struct, which is close to Python dictionary, before saving it to a .mat file. Check the function [here](https://ww2.mathworks.cn/help/vision/ref/tostruct.html).
+Think YOLO as a black box, it consumes photos that contains objects or not and
+outputs objects' positions in the pixel coordinate. However, we want to know
+their positions on the planting bed, namely global coordinate. That is why we
+need camera calibration. This step was done separately on MATLAB and offered us
+the intrinsic matrix of the camera. You **do not need to redo** this unless you
+want to change to a new model of camera. Check MATLAB
+[instruction](https://ww2.mathworks.cn/help/vision/ug/using-the-single-camera-calibrator-app.html;jsessionid=1a474c11e3e6063620885c4ae708)
+for how to use the camera calibration app.
+
+**Note**: The result have to be transferred to `struct`, a MATLAB data type,
+before saving.
+
+If you follow the instruction above, you will end up with a MATLAB object,
+`cameraParameters`. If you save this to a .mat file and read it by SciPy, you
+will unfortunately end up with an empty dictionary. You should always use the
+function `toStruct` to convert it to a struct, which is close to Python
+dictionary, before saving it to a .mat file. Check the function
+[here](https://ww2.mathworks.cn/help/vision/ref/tostruct.html).
 
 ## Coordinate Tranform
-Pixel Coordinate $\Rightarrow^{1}$ Camera Coordinate $\Rightarrow^{2}$ Global Coordinate  
-- Pixel coordinate: Output bounding boxes from YOLO in the form of $<x, y, w,h>$. Here we only use $<x, y>$ currently. $w, h$ might be useful for estmating the size of obnjects in the future.
-- Camera coordinate: The coordinate system determined by the camera, whose original point is at the camera pin hole.
-- Gloabl coordinate: The coordinate system for Farmbot. The original point is at the upper right corner of the planting bed.  
 
-Note: Due to mechanical limitation, the orginal point of global coordinate system does not perfectly align with the planting bed's corner.
+Pixel Coordinate $\Rightarrow^{1}$ Camera Coordinate $\Rightarrow^{2}$ Global
+Coordinate
+
+- Pixel coordinate: Output bounding boxes from YOLO in the form of $<x, y,
+  w,h>$. Here we only use $<x, y>$ currently. $w, h$ might be useful for
+  estmating the size of object's in the future.
+- Camera coordinate: The coordinate system determined by the camera, whose
+  original point is at the camera pin hole.
+- Global coordinate: The coordinate system for Farmbot. The original point is at
+  the upper right corner of the planting bed.
+
+Note: Due to mechanical limitation, the original point of global coordinate
+system does not perfectly align with the planting bed's corner.
 
 ### 1. From Pixel to Camera Coordinate
 
 ### 2. From Pixel to Camera Coordinate
-The camera coordinate can be transformed to the gloabl one via a transistion and a rotation. From the picture, we can easily write the formula
-$$
 
-$$ 
+The camera coordinate can be transformed to the global one via a transition and
+a rotation. From the picture, we can easily write the formula
 
-## Install, Compile
-1. `git clone git@gitlab.control.lth.se:alexp/farmbot`
-2. `make` to download darknet and compile
-3. `conda create --name farm --file requirements.txt`, this will install all required python packages. Change the enviroment name to whatever you like. 
+## Setup
 
-**Note**: I assume conda, a package management software, has been installed before.
+1. `git clone git@gitlab.control.lth.se:robotlab/farmbot_yolo`
+1. Initialize submodules: `git submodules init && git submodules update`.
+1. `make` to compile
+1. Install anaconda using [official
+   installer](https://www.anaconda.com/products/distribution) and
+   [instructions](https://docs.conda.io/projects/conda/en/latest/user-guide/install/download.html).
+1. Set [conda-forge](https://conda-forge.org/) as the package repo: `conda config --add channels conda-forge && conda config --set channel_priority strict`
+1. `conda env create --name farm --f environment.yml`, this will install all
+   dependencies. Change the environment name to whatever you like.
+1. Install local package: `pip install -e .`
 
 ### Compile Darknet
-Check the [instruction](https://github.com/AlexeyAB/darknet#how-to-compile-on-linux-using-make) for how to use `Make` to compile on Linux. 
 
-In the top Makefile `LIBSO=1` is set before the Makefile in darknet is run. This will ensure libdarknet.so be generated, which will be used in darknet.py.
+**Already done in the above steps**
+
+Check the
+[instruction](https://github.com/AlexeyAB/darknet#how-to-compile-on-linux-using-make)
+for how to use `Make` to compile on Linux.
+
+In the top Makefile `LIBSO=1` is set before the Makefile in darknet is run. This
+will ensure libdarknet.so be generated, which will be used in darknet.py.
 
 ## Before starting the system
+
 **Always calibrate the position before using!**  
-The purpose is to reset the zero positions of x, y, z axis. This step should be done manually first and then use the webapp, i.e., the user should push the grantry and z axis to the upper right corner. Check the [official instruction](https://software.farm.bot/v14/FarmBot-Software/how-to-guides/axis-setup) for how to use the web app to set zeros. 
+The purpose is to reset the zero positions of x, y, z axis. This step should be
+done manually first and then use the webapp, i.e., the user should push the
+grantry and z axis to the upper right corner. Check the [official
+instruction](https://software.farm.bot/v14/FarmBot-Software/how-to-guides/axis-setup)
+for how to use the web app to set zeros.
 
 ## How to run this system?
-The software has three main modules: 
-1. `move.py`: drive FarmBot to move, take photos, and open/close the gripper
-2. `detection.py`: run darknet as a dynamic library for detecting, output bounding boxes
-3. `location.py`: input bounding boxes, transfer to real-world coordinate  
 
-We also provide `main.py` as a warpper for all the modules above. By runing it, you can make Farmbot automatically conduct the whole process. The three modules can also be run sperately, mostly for debugging purpose.
+The software has three main modules:
+
+1. `move`: drive FarmBot to move, take photos, and open/close the gripper
+2. `detection`: run darknet as a dynamic library for detecting, output
+   bounding boxes
+3. `location`: input bounding boxes, transfer to real-world coordinate
+
+We also provide `main.py` as a wrapper for all the modules above. By running it,
+you can make Farmbot automatically conduct the whole process. The three modules
+can also be run separately, mostly for debugging purpose.
+
+First go to `farmbot_yolo` and `conda activate <env>` to run the following scripts.
+`<env>` is the same as the one you created in _Install, Compile_
+
+### Move Farmbot, take photos, and open/close the gripper
 
-First go to `/src/` and `conda activate <env>` to run the following scripts. `<env>` is the same as the one you created in *Install, Compile*
-### Move Famrbot, take photos, and open/close the gripper
 ### YOLO detection
-All the arguments for file path are set to default. 
+
+All the arguments for file path are set to default.
+
 ```
-python ./detect.py --dont_show --ext_output --save_labels --input ../img --weights ../weights/yolov3-vattenhallen_best.weights  --config_file ../cfg/yolov3-vattenhallen-test.cfg --data_file ../data/vattenhallen.data
+python farmbot_yolo.detect --dont_show --ext_output --save_labels --input ../img --weights ../weights/yolov3-vattenhallen_best.weights  --config_file ../cfg/yolov3-vattenhallen-test.cfg --data_file ../data/vattenhallen.data
 ```
+
 ### Calculate location
+
 ```
-python location.py -v -cam ../static/camera_no_distortion.mat -loc ../img/locations/ -a ../img/annotations -o ../static/distance.txt -l ../log/location.log
+python farmbot_yolo.location -v -cam ../static/camera_no_distortion.mat -loc ../img/locations/ -a ../img/annotations -o ../static/distance.txt -l ../log/location.log
 ```
-All the arguments has default values, which means they can be all omitted if you don't change the document tree structure.
 
-### Scan the bed and pick
+All the arguments has default values, which means they can be all omitted if you
+don't change the document tree structure.
 
-重新生成requirement!!
+### Scan the bed and pick
 
+重新生成 requirement!!
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/environment.yml b/environment.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b8ba9190172463b878a081cbfd1152aa704214da
--- /dev/null
+++ b/environment.yml
@@ -0,0 +1,15 @@
+name: farm-dev
+channels:
+  - conda-forge
+dependencies:
+  - python==3.8
+  - black
+  - isort
+  - mypy
+  - flake8
+  - opencv
+  - paho-mqtt
+  - numpy
+  - scipy
+  - pandas
+  - requests
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..d4221ab736068c96608067d54e271d88386af3f0
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,31 @@
+[build-system]
+requires = ["setuptools>=61.0.0"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "farmbot_yolo"
+description = "farmbot object identification using YOLO/Darknet"
+version = "0.1.0"
+readme = "README.md"
+license = {text = "TODO"}
+authors = [
+    { name = "Ziliang Xiong", email = "13718722639leo@gmail.com" }
+]
+classifiers = [
+    "Programming Language :: Python :: 3",
+]
+requires-python = ">=3.8"
+dependencies = [
+    "requests",
+    "numpy",
+    "opencv-python",
+    "scipy",
+    "pandas",
+    "paho-mqtt",
+]
+
+[project.optional-dependencies]
+dev = ["black", "mypy", "flake8", "isort"]
+
+[tool.isort]
+profile = "black"
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 57c08ca1be2dc2a714502c11c5615bf3dc56bdb5..0000000000000000000000000000000000000000
--- a/requirements.txt
+++ /dev/null
@@ -1,81 +0,0 @@
-# This file may be used to create an environment using:
-# $ conda create --name <env> --file <this file>
-# platform: linux-64
-_libgcc_mutex=0.1=main
-_openmp_mutex=4.5=1_gnu
-blas=1.0=mkl
-bottleneck=1.3.2=py37heb32a55_1
-brotlipy=0.7.0=py37h27cfd23_1003
-bzip2=1.0.8=h7b6447c_0
-ca-certificates=2021.10.26=h06a4308_2
-cairo=1.16.0=hf32fb01_1
-certifi=2021.10.8=py37h06a4308_0
-cffi=1.14.5=py37h261ae71_0
-charset-normalizer=2.0.4=pyhd3eb1b0_0
-cryptography=35.0.0=py37hd23ed53_0
-ffmpeg=4.0=hcdf2ecd_0
-fontconfig=2.13.1=h6c09931_0
-freeglut=3.0.0=hf484d3e_5
-freetype=2.10.4=h5ab3b9f_0
-glib=2.69.1=h5202010_0
-graphite2=1.3.14=h23475e2_0
-h5py=2.8.0=py37h989c5e5_3
-harfbuzz=1.8.8=hffaf4a1_0
-hdf5=1.10.2=hba1933b_1
-icu=58.2=he6710b0_3
-idna=3.2=pyhd3eb1b0_0
-intel-openmp=2021.3.0=h06a4308_3350
-jasper=2.0.14=h07fcdf6_1
-jpeg=9d=h7f8727e_0
-ld_impl_linux-64=2.35.1=h7274673_9
-libffi=3.3=he6710b0_2
-libgcc-ng=9.3.0=h5101ec6_17
-libgfortran-ng=7.5.0=ha8ba4b0_17
-libgfortran4=7.5.0=ha8ba4b0_17
-libglu=9.0.0=hf484d3e_1
-libgomp=9.3.0=h5101ec6_17
-libopencv=3.4.2=hb342d67_1
-libopus=1.3.1=h7b6447c_0
-libpng=1.6.37=hbc83047_0
-libstdcxx-ng=9.3.0=hd4cf53a_17
-libtiff=4.2.0=h85742a9_0
-libuuid=1.0.3=h1bed415_2
-libvpx=1.7.0=h439df22_0
-libwebp-base=1.2.0=h27cfd23_0
-libxcb=1.14=h7b6447c_0
-libxml2=2.9.12=h03d6c58_0
-lz4-c=1.9.3=h295c915_1
-mkl=2021.3.0=h06a4308_520
-mkl-service=2.4.0=py37h7f8727e_0
-mkl_fft=1.3.0=py37h42c9631_2
-mkl_random=1.2.2=py37h51133e4_0
-ncurses=6.2=he6710b0_1
-numexpr=2.7.3=py37h22e1b3c_1
-numpy=1.20.3=py37hf144106_0
-numpy-base=1.20.3=py37h74d4b33_0
-opencv=3.4.2=py37h6fd60c2_1
-openssl=1.1.1l=h7f8727e_0
-paho-mqtt=1.6.1=pypi_0
-pandas=1.3.4=py37h8c16a72_0
-pcre=8.45=h295c915_0
-pip=21.0.1=py37h06a4308_0
-pixman=0.40.0=h7b6447c_0
-py-opencv=3.4.2=py37hb342d67_1
-pycparser=2.21=pyhd3eb1b0_0
-pyopenssl=21.0.0=pyhd3eb1b0_1
-pysocks=1.7.1=py37_1
-python=3.7.11=h12debd9_0
-python-dateutil=2.8.2=pyhd3eb1b0_0
-pytz=2021.3=pyhd3eb1b0_0
-readline=8.1=h27cfd23_0
-requests=2.26.0=pyhd3eb1b0_0
-scipy=1.7.1=py37h292c36d_2
-setuptools=58.0.4=py37h06a4308_0
-six=1.16.0=pyhd3eb1b0_0
-sqlite=3.36.0=hc218d9a_0
-tk=8.6.10=hbc83047_0
-urllib3=1.26.7=pyhd3eb1b0_0
-wheel=0.37.0=pyhd3eb1b0_1
-xz=5.2.5=h7b6447c_0
-zlib=1.2.11=h7b6447c_3
-zstd=1.4.9=haebb681_0
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000000000000000000000000000000000000..606849326a4002007fd42060b51e69a19c18675c
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,3 @@
+from setuptools import setup
+
+setup()
diff --git a/src/farmbot_yolo/__init__.py b/src/farmbot_yolo/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e5e182d25dd041b7a6ade7e7367f6a642e15ea35
--- /dev/null
+++ b/src/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/utils/client.py b/src/farmbot_yolo/client.py
old mode 100755
new mode 100644
similarity index 97%
rename from src/utils/client.py
rename to src/farmbot_yolo/client.py
index 296fcc467546b1e1da63d8072ff47207e96f3698..bac752a19d03878719beb79d852d23bfb24a2748
--- a/src/utils/client.py
+++ b/src/farmbot_yolo/client.py
@@ -1,245 +1,245 @@
-"""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 read_pin_request(pin_number, pin_mode="digital"):
-    modes = {"digital": 0, "analog": 1}
-
-    if pin_mode != "digital":
-        raise NotImplementedError()
-
-    return {
-        "kind": "rpc_request",
-        "args": {"label": ""},
-        "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}
-
-    if pin_mode != "digital":
-        raise NotImplementedError()
-
-    return {
-        "kind": "rpc_request",
-        "args": {"label": ""},
-        "body": [
-            {
-                "kind": "write_pin",
-                "args": {
-                    "pin_mode": modes[pin_mode] or (modes["digital"]),
-                    "pin_number": pin_number,
-                    "pin_value": pin_value,
-                },
-            }
-        ],
-    }
-
-
-def toggle_pin_request(pin_number):
-    return {
-        "kind": "rpc_request",
-        "args": {"label": ""},
-        "body": [
-            {
-                "kind": "toggle_pin",
-                "args": {
-                    "pin_number": pin_number,
-                },
-            }
-        ],
-    }
-
-
-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 read_pin(self, pin_number, pin_mode="digital"):
-        status_ok = self._blocking_request(
-            read_pin_request(pin_number, pin_mode=pin_mode)
-        )
-        logging.info(f"READ PIN (pin_number: {pin_number}) [{status_ok}]")
-
-    def write_pin(self, pin_number, pin_value, pin_mode="digital"):
-        status_ok = self._blocking_request(
-            write_pin_request(pin_number, pin_value, pin_mode=pin_mode)
-        )
-        logging.info(
-            f"WRITE PIN (pin_number: {pin_number}, pin_value: {pin_value}) [{status_ok}]"
-        )
-
-    def toggle_pin(self, pin_number):
-        status_ok = self._blocking_request(toggle_pin_request(pin_number))
-        logging.info(f"TOGGLE PIN (pin_number: {pin_number}) [{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 read_pin_request(pin_number, pin_mode="digital"):
+    modes = {"digital": 0, "analog": 1}
+
+    if pin_mode != "digital":
+        raise NotImplementedError()
+
+    return {
+        "kind": "rpc_request",
+        "args": {"label": ""},
+        "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}
+
+    if pin_mode != "digital":
+        raise NotImplementedError()
+
+    return {
+        "kind": "rpc_request",
+        "args": {"label": ""},
+        "body": [
+            {
+                "kind": "write_pin",
+                "args": {
+                    "pin_mode": modes[pin_mode] or (modes["digital"]),
+                    "pin_number": pin_number,
+                    "pin_value": pin_value,
+                },
+            }
+        ],
+    }
+
+
+def toggle_pin_request(pin_number):
+    return {
+        "kind": "rpc_request",
+        "args": {"label": ""},
+        "body": [
+            {
+                "kind": "toggle_pin",
+                "args": {
+                    "pin_number": pin_number,
+                },
+            }
+        ],
+    }
+
+
+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 read_pin(self, pin_number, pin_mode="digital"):
+        status_ok = self._blocking_request(
+            read_pin_request(pin_number, pin_mode=pin_mode)
+        )
+        logging.info(f"READ PIN (pin_number: {pin_number}) [{status_ok}]")
+
+    def write_pin(self, pin_number, pin_value, pin_mode="digital"):
+        status_ok = self._blocking_request(
+            write_pin_request(pin_number, pin_value, pin_mode=pin_mode)
+        )
+        logging.info(
+            f"WRITE PIN (pin_number: {pin_number}, pin_value: {pin_value}) [{status_ok}]"
+        )
+
+    def toggle_pin(self, pin_number):
+        status_ok = self._blocking_request(toggle_pin_request(pin_number))
+        logging.info(f"TOGGLE PIN (pin_number: {pin_number}) [{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/src/farmbot_yolo/darknet.py
similarity index 100%
rename from src/darknet.py
rename to src/farmbot_yolo/darknet.py
diff --git a/src/detect.py b/src/farmbot_yolo/detect.py
similarity index 100%
rename from src/detect.py
rename to src/farmbot_yolo/detect.py
diff --git a/src/utils/download.py b/src/farmbot_yolo/download.py
similarity index 98%
rename from src/utils/download.py
rename to src/farmbot_yolo/download.py
index 67caace59ae348c9a5181e23e9d0faebee39516b..fa732ab42316fd3914b69749259593835b913b12 100644
--- a/src/utils/download.py
+++ b/src/farmbot_yolo/download.py
@@ -6,7 +6,7 @@ from typing import List
 
 import requests
 
-import utils.creds as creds
+from farmbot_yolo import creds
 
 
 # note: download only returns 100 at a time!
diff --git a/src/gripper.py b/src/farmbot_yolo/gripper.py
old mode 100755
new mode 100644
similarity index 76%
rename from src/gripper.py
rename to src/farmbot_yolo/gripper.py
index 27b72215533b7f2414874aca6fb8068055dd86e7..b5f8fef872ed3e44b9aa95a7364980ea00de8c0e
--- a/src/gripper.py
+++ b/src/farmbot_yolo/gripper.py
@@ -2,14 +2,15 @@
 
 TODO: Integrate with FarmbotClient or FarmbotYoloClient
 """
-from utils.client import FarmbotClient
-from utils.creds import device_id
-from utils.creds import token
+from farmbot_yolo.client import FarmbotClient
+from farmbot_yolo.creds import device_id
+from farmbot_yolo.creds import token
 
 GRIPPER_PIN = 12
 GRIPPER_OPEN_STATE = 0
 GRIPPER_CLOSED_STATE = 1
 
+
 def gripper_open():
     """Manipulate gripper by setting pin to 1."""
     client = FarmbotClient(device_id, token)
@@ -22,3 +23,6 @@ def gripper_close():
     client = FarmbotClient(device_id, token)
     client.write_pin(GRIPPER_PIN, GRIPPER_CLOSED_STATE, pin_mode="digital")
     client.shutdown()
+
+
+# TODO: Add argumentparser and ifnamemain function
diff --git a/src/location.py b/src/farmbot_yolo/location.py
similarity index 100%
rename from src/location.py
rename to src/farmbot_yolo/location.py
diff --git a/src/main.py b/src/farmbot_yolo/main.py
similarity index 98%
rename from src/main.py
rename to src/farmbot_yolo/main.py
index d719b2a8023d4f8d9747b4c0ec0a6f81bd68707e..5295b4e6bd94d337197f2f567cd3d2e31ed4a3dd 100644
--- a/src/main.py
+++ b/src/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__)
 
@@ -183,4 +183,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/src/farmbot_yolo/move.py
similarity index 61%
rename from src/move.py
rename to src/farmbot_yolo/move.py
index afa038ffd45ee74d472ff4473201a08e631858fc..064cf5518240f03df8a79a0d1239cab629c89afb 100644
--- a/src/move.py
+++ b/src/farmbot_yolo/move.py
@@ -1,16 +1,17 @@
-'''
+"""
 Author: Ziliang Xiong
 This script is for all the functions that drive Farmbot to Move, including:
 1. Taking Photos 2. Move to an assigned point (x, y, z)
 3. Sweep the planting bed 4. Grip a target
 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 time import sleep, strftime, time
+
+# from serial import Serial, PARITY_NONE, STOPBITS_ONE, EIGHTBITS
 from requests.api import delete
 from typing import List
 from pathlib import Path
@@ -21,14 +22,14 @@ from datetime import timezone, datetime
 from dateutil.parser import parse
 from requests import get, delete
 
-import utils.creds as creds
-from utils.client import FarmbotClient
-
+import creds
+from client import FarmbotClient
 
 _SWEEEP_HEIGHT = 0
 
 Logger = getLogger(__name__)
 
+
 class Opts:
     def __init__(self, min_x, max_x, min_y, max_y, delta, offset, flag):
         self.min_x = min_x
@@ -38,11 +39,20 @@ class Opts:
         self.delta = delta
         self.offset = offset
         self.flag = flag
-    
 
-def scan(img_path: Path, location_path: Path, # smaller delta
-         min_x=0, max_x=1175, min_y=0, max_y=974, delta=300, offset=0, flag=True) -> List: #里面的数字需要重新测量
-    '''
+
+def scan(
+    img_path: Path,
+    location_path: Path,  # smaller delta
+    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
     The default value of x, y should be from the measurement of Farmbot
@@ -54,8 +64,9 @@ def scan(img_path: Path, location_path: Path, # smaller delta
            offset:
            flag: for degging, if true, don't actually drive FarmBot
     Output: none
-    '''
+    """
     opts = Opts(min_x, max_x, min_y, max_y, delta, offset, flag)
+    creds = read_credentials()
 
     pts = []
     sweep_y_negative = False
@@ -65,33 +76,33 @@ def scan(img_path: Path, location_path: Path, # smaller delta
             y_range = reversed(y_range)
         sweep_y_negative = not sweep_y_negative
         for y in y_range:
-            pts.append((x+opts.offset, y+opts.offset))
+            pts.append((x + opts.offset, y + opts.offset))
 
-    Logger.info('Moving pattern generated')
+    Logger.info("Moving pattern generated")
 
     if opts.flag:
-        Logger.info('Run without sweep')
+        Logger.info("Run without sweep")
         exit()
 
-    client = FarmbotClient(creds.device_id, creds.token)
-    client.move(0, 0, _SWEEEP_HEIGHT) # ensure moving from original 
+    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
-        #take_photo(img_path)
+        client.move(x, y, _SWEEEP_HEIGHT)  # move camera
+        # take_photo(img_path)
         client.take_photo()
     client.shutdown()
     # write to img/location
-    with open(path.join(location_path, "location.txt"), 'w') as f:
+    with open(path.join(location_path, "location.txt"), "w") as f:
         for postion in pts:
-            f.write('{} {} {}\n'.format(postion[0], postion[1], _SWEEEP_HEIGHT))
-    return None 
+            f.write("{} {} {}\n".format(postion[0], postion[1], _SWEEEP_HEIGHT))
+    return None
 
 
 def take_photo():
     client = FarmbotClient(creds.device_id, creds.token)
     client.take_photo()
     # download image
-    system('python ./utils/download.py')
+    system("python ./utils/download.py")
 
 
 # def take_photo(img_path: Path):
@@ -104,65 +115,59 @@ def take_photo():
 #             save_file.write(photo.read())
 
 
-def simple_move(x: int, y: int, z: int) -> None: 
-    '''
+def simple_move(x: int, y: int, z: int) -> None:
+    """
     Move to a place, if flag is true, take a picture
     Input: x, y,z: destination point
            photo: take a pic or not
-    '''
-    client = FarmbotClient(creds.device_id, creds.token)
-    client.move(x, y, z)  
+    """
+    creds = read_credentials()
+    client = FarmbotClient(creds["device_id"], creds["token"])
+    client.move(x, y, z)
     client.shutdown()
     return None
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     parser = ArgumentParser()
     parser.add_argument(
-        '-m',
-        '--mode',
+        "-m",
+        "--mode",
         type=int,
-        help='Mode for FarmBot, 1 for simple move with an assigned detination, 2 for scaning' 
+        help="Mode for FarmBot, 1 for simple move with an assigned detination, 2 for scaning",
     )
     parser.add_argument(
-        '-l',
-        '--log',
-        type=Path,
-        default='../log/move.log',
-        help='Path to the log file'
+        "-l", "--log", type=Path, default="../log/move.log", help="Path to the log file"
     )
     parser.add_argument(
-        '-p',
-        '--photo',
+        "-p",
+        "--photo",
         type=Path,
         default="../img",
-        help='Mode for FarmBot, 1 for simple move with an assigned detination, 2 for scaning' 
+        help="Mode for FarmBot, 1 for simple move with an assigned detination, 2 for scaning",
     )
     parser.add_argument(
-        '-loc',
-        '--locations',
+        "-loc",
+        "--locations",
         type=Path,
-        default='../img/locations/',
-        help='the path to txt files contains locations from encoders corresponds to each photo'
+        default="../img/locations/",
+        help="the path to txt files contains locations from encoders corresponds to each photo",
     )
-    parser.add_argument('-v', '--verbose', action='store_true', help='Verbose mode')
+    parser.add_argument("-v", "--verbose", action="store_true", help="Verbose mode")
     arguments = parser.parse_args()
-    
 
     if arguments.mode == 1:
-        Logger.info('Input the destination:')
-        destination_x = int(input('X:'))
-        destination_y = int(input('Y:'))
-        destination_z = int(input('Z:'))
+        Logger.info("Input the destination:")
+        destination_x = int(input("X:"))
+        destination_y = int(input("Y:"))
+        destination_z = int(input("Z:"))
         simple_move_start = time()
         simple_move(destination_x, destination_y, destination_z)
-        Logger.info(f'time cost {time()-simple_move_start}')
+        Logger.info(f"time cost {time()-simple_move_start}")
     elif arguments.mode == 2:
         scan(arguments.photo, arguments.locations, flag=False)
-        #take_photo(arguments.photo)
+        # take_photo(arguments.photo)
     elif arguments.mode == 3:
         take_photo()
     else:
-        Logger.error('Wrong mode number {arguments.mode}')
-
-
+        Logger.error("Wrong mode number {arguments.mode}")
diff --git a/src/farmbot_yolo/read_credentials.py b/src/farmbot_yolo/read_credentials.py
new file mode 100644
index 0000000000000000000000000000000000000000..6a310c218fd6b02fc87482c77f9d2a750d0979d8
--- /dev/null
+++ b/src/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/src/farmbot_yolo/request_token.py b/src/farmbot_yolo/request_token.py
new file mode 100644
index 0000000000000000000000000000000000000000..8c1622e25054401f12f4e89b1289d2b28de17c46
--- /dev/null
+++ b/src/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/src/farmbot_yolo/try.py
similarity index 100%
rename from src/try.py
rename to src/farmbot_yolo/try.py
diff --git a/src/utils/request_token.py b/src/utils/request_token.py
deleted file mode 100755
index ef793f927a00fa57635ec4f13c74816e0a939bbd..0000000000000000000000000000000000000000
--- a/src/utils/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'])