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'])