diff --git a/.github/workflows/build_backend.yml b/.github/workflows/build_backend.yml index 0331b4c8..e78c948a 100644 --- a/.github/workflows/build_backend.yml +++ b/.github/workflows/build_backend.yml @@ -8,7 +8,11 @@ on: push jobs: test: runs-on: ubuntu-latest - container: coderbot/coderbot-ci:3.9-bullseye-slim + container: + image: ghcr.io/coderbotorg/coderbot-ci:stub-latest + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GHCR_READ }} steps: - uses: actions/checkout@v3 # Checking out the repo - run: pip install -r docker/stub/requirements.txt @@ -21,13 +25,16 @@ jobs: echo "test complete" - run: | export PYTHONPATH=./stub:./coderbot:./test + export CODERBOT_CLOUD_API_ENDPOINT=http://localhost:5000 python3 coderbot/main.py > coderbot.log & - sleep 30 + sleep 60 + cat coderbot.log + curl http://localhost:5000/api/v1/openapi.json apt-get install -y python3-venv mkdir -p schemathesis python3 -m venv schemathesis . schemathesis/bin/activate - pip install schemathesis + pip install schemathesis==3.24.3 st run --endpoint 'activities' --hypothesis-max-examples=10 --request-timeout=20 http://localhost:5000/api/v1/openapi.json #st run --endpoint 'media' --hypothesis-max-examples=10 --request-timeout=20 http://localhost:5000/api/v1/openapi.json st run --endpoint 'control/speak' --hypothesis-max-examples=10 --request-timeout=20 http://localhost:5000/api/v1/openapi.json diff --git a/coderbot/activity.py b/coderbot/activity.py index 19e3d102..79c87a81 100644 --- a/coderbot/activity.py +++ b/coderbot/activity.py @@ -1,6 +1,16 @@ +import logging +import uuid from tinydb import TinyDB, Query from threading import Lock +from datetime import datetime # Programs and Activities databases + +ACTIVITY_STATUS_DELETED = "deleted" +ACTIVITY_STATUS_ACTIVE = "active" +ACTIVITY_KIND_STOCK = "stock" +ACTIVITY_KIND_USER = "user" + + class Activities(): _instance = None @@ -14,11 +24,16 @@ def __init__(self): self.activities = TinyDB("data/activities.json") self.query = Query() self.lock = Lock() + self.permanentlyRemoveDeletedActivities() - def load(self, name, default): + def load(self, id, default, active_only=True): with self.lock: - if name and default is None: - activities = self.activities.search(self.query.name == name) + if id and default is None: + activities = [] + if active_only: + activities = self.activities.search((self.query.id == id) & (self.query.status == ACTIVITY_STATUS_ACTIVE)) + else: + activities = self.activities.search(self.query.id == id) if len(activities) > 0: return activities[0] elif default is not None: @@ -27,25 +42,45 @@ def load(self, name, default): return None return None - def save(self, name, activity): + def save(self, activity): + if activity.get("id") is None: + activity["id"] = str(uuid.uuid4()) with self.lock: # if saved activity is "default", reset existing default activity to "non-default" if activity.get("default", False) is True: self.activities.update({'default': False}) - if self.activities.search(self.query.name == name) == []: + if self.activities.search(self.query.id == activity.get("id")) == []: self.activities.insert(activity) else: - self.activities.update(activity, self.query.name == activity["name"]) + self.activities.update(activity, self.query.id == activity.get("id")) + activity = self.activities.search(self.query.id == activity.get("id"))[0] + logging.info("updating/creating activity - id: %s, name: %s", str(activity.get("id")), str(activity.get("name"))) + return activity - def delete(self, name): + def delete(self, id, logical = True): with self.lock: - activities = self.activities.search(self.query.name == name) + activities = self.activities.search(self.query.id == id) if len(activities) > 0: activity = activities[0] if activity.get("default", False) is True: - self.activities.update({'default': True}, self.query.stock == True) - self.activities.remove(self.query.name == activity["name"]) + self.activities.update({'default': True}, self.query.kind == ACTIVITY_KIND_STOCK) + if logical: + activity["status"] = ACTIVITY_STATUS_DELETED + activity["modified"] = datetime.now().isoformat() + self.activities.update(activity, self.query.id == id) + else: + self.activities.remove(self.query.id == id) - def list(self): + def permanentlyRemoveDeletedActivities(self): + for a in self.list(active_only=False): + logging.info("checking: " + a["id"]) + if a["status"] == ACTIVITY_STATUS_DELETED: + logging.info("deleting: " + a["name"]) + self.delete(a["id"], logical=False) + + def list(self, active_only = True): with self.lock: - return self.activities.all() + if active_only: + return self.activities.search(self.query.status == ACTIVITY_STATUS_ACTIVE) + else: + return self.activities.all() diff --git a/coderbot/api.py b/coderbot/api.py index 88052aff..c150f44e 100644 --- a/coderbot/api.py +++ b/coderbot/api.py @@ -7,6 +7,7 @@ import os import subprocess import urllib +from datetime import datetime import connexion import picamera @@ -21,25 +22,14 @@ from runtime_test import run_test from musicPackages import MusicPackageManager from program import Program, ProgramEngine +from motion import Motion +from cloud.sync import CloudManager from balena import Balena from coderbot import CoderBot BUTTON_PIN = 16 -config = Config.read() -bot = CoderBot.get_instance(motor_trim_factor=float(config.get('move_motor_trim', 1.0)), - motor_max_power=int(config.get('motor_max_power', 100)), - motor_min_power=int(config.get('motor_min_power', 0)), - hw_version=config.get('hardware_version'), - pid_params=(float(config.get('pid_kp', 1.0)), - float(config.get('pid_kd', 0.1)), - float(config.get('pid_ki', 0.01)), - float(config.get('pid_max_speed', 200)), - float(config.get('pid_sample_time', 0.01)))) -audio_device = Audio.get_instance() -cam = Camera.get_instance() - def get_serial(): """ Extract serial from cpuinfo file @@ -77,7 +67,7 @@ def get_status(): internet_status = False try: - urllib.request.urlopen("https://coderbot.org") + urllib.request.urlopen("https://coderCoderBot.get_instance().org") internet_status = True except Exception: pass @@ -114,48 +104,48 @@ def get_info(): ## Robot control def stop(): - bot.stop() - return 200 + CoderBot.get_instance().stop() + return {} def move(body): speed=body.get("speed") elapse=body.get("elapse") distance=body.get("distance") if (speed is None or speed == 0) or (elapse is not None and distance is not None): - return 400 - bot.move(speed=speed, elapse=elapse, distance=distance) - return 200 + return None, 400 + CoderBot.get_instance().move(speed=speed, elapse=elapse, distance=distance) + return {} def turn(body): speed=body.get("speed") elapse=body.get("elapse") distance=body.get("distance") if (speed is None or speed == 0) or (elapse is not None and distance is not None): - return 400 - bot.turn(speed=speed, elapse=elapse, distance=distance) - return 200 + return None, 400 + CoderBot.get_instance().turn(speed=speed, elapse=elapse, distance=distance) + return {} def takePhoto(): try: - cam.photo_take() - audio_device.say(config.get("sound_shutter")) - return 200 + Camera.get_instance().photo_take() + Audio.get_instance().say(settings.get("sound_shutter")) + return {} except Exception as e: logging.warning("Error: %s", e) def recVideo(): try: - cam.video_rec() - audio_device.say(config.get("sound_shutter")) - return 200 + Camera.get_instance().video_rec() + Audio.get_instance().say(settings.get("sound_shutter")) + return {} except Exception as e: logging.warning("Error: %s", e) def stopVideo(): try: - cam.video_stop() - audio_device.say(config.get("sound_shutter")) - return 200 + Camera.get_instance().video_stop() + Audio.get_instance().say(settings.get("sound_shutter")) + return {} except Exception as e: logging.warning("Error: %s", e) @@ -163,25 +153,25 @@ def speak(body): text = body.get("text", "") locale = body.get("locale", "") logging.debug("say: " + text + " in: " + locale) - audio_device.say(text, locale) - return 200 + Audio.get_instance().say(text, locale) + return {} def reset(): Balena.get_instance().purge() - return 200 + return {} def halt(): - audio_device.say(what=config.get("sound_stop")) + Audio.get_instance().say(what=settings.get("sound_stop")) Balena.get_instance().shutdown() - return 200 + return {} def restart(): Balena.get_instance().restart() def reboot(): - audio_device.say(what=config.get("sound_stop")) + Audio.get_instance().say(what=settings.get("sound_stop")) Balena.get_instance().reboot() - return 200 + return {} def video_stream(a_cam): while True: @@ -198,7 +188,7 @@ def streamVideo(): h.add('Age', 0) h.add('Cache-Control', 'no-cache, private') h.add('Pragma', 'no-cache') - return Response(video_stream(cam), headers=h, mimetype="multipart/x-mixed-replace; boundary=--BOUNDARYSTRING") + return Response(video_stream(Camera.get_instance()), headers=h, mimetype="multipart/x-mixed-replace; boundary=--BOUNDARYSTRING") except Exception: pass @@ -206,31 +196,31 @@ def listPhotos(): """ Expose the list of taken photos """ - return cam.get_photo_list() + return Camera.get_instance().get_photo_list() def getPhoto(name): mimetype = {'jpg': 'image/jpeg', 'mp4': 'video/mp4'} try: - media_file = cam.get_photo_file(name) + media_file = Camera.get_instance().get_photo_file(name) return send_file(media_file, mimetype=mimetype.get(name[:-3], 'image/jpeg'), max_age=0) except picamera.exc.PiCameraError as e: logging.error("Error: %s", str(e)) - return 503 + return {"exception": str(e)}, 503 except FileNotFoundError: - return 404 + return None, 404 def savePhoto(name, body): try: - cam.update_photo({"name": name, "tag": body.get("tag")}) + Camera.get_instance().update_photo({"name": name, "tag": body.get("tag")}) except FileNotFoundError: - return 404 + return None, 404 def deletePhoto(name): logging.debug("photo delete") try: - cam.delete_photo(name) + Camera.get_instance().delete_photo(name) except FileNotFoundError: - return 404 + return None, 404 def restoreSettings(): Config.restore() @@ -241,14 +231,14 @@ def loadSettings(): def saveSettings(body): Config.write(body) - return 200 + return Config.write(body) def updateFromPackage(): os.system('sudo bash /home/pi/clean-update.sh') file_to_upload = connexion.request.files['file_to_upload'] file_to_upload.save(os.path.join('/home/pi/', 'update.tar')) os.system('sudo reboot') - return 200 + return {} def listMusicPackages(): """ @@ -283,45 +273,67 @@ def deleteMusicPackage(name): """ musicPkg = MusicPackageManager.get_instance() musicPkg.deletePackage(name) - return 200 + return {} ## Programs -def saveProgram(name, body): +def saveNewProgram(body): overwrite = body.get("overwrite") - existing_program = prog_engine.load(name) + name = body["name"] + existing_program = prog_engine.load_by_name(name) logging.info("saving - name: %s, body: %s", name, str(existing_program)) if existing_program is not None and not overwrite: return "askOverwrite" - elif existing_program is not None and existing_program.is_default() == True: + elif existing_program is not None and existing_program.is_stock() == True: + return "defaultCannotOverwrite", 400 + program = Program(name=body.get("name"), code=body.get("code"), dom_code=body.get("dom_code"), modified=datetime.now(), status="active") + program_db_entry = prog_engine.save(program) + return program_db_entry + +def saveProgram(id, body): + overwrite = body.get("overwrite") + name = body.get("name", None) + existing_program = prog_engine.load(id) + if existing_program is None: + return {}, 404 + logging.info("saving - id: %s - name: %s - existing: %s", id, name, str(existing_program is not None)) + if existing_program is not None and existing_program.is_stock() == True: return "defaultCannotOverwrite", 400 - program = Program(name=body.get("name"), code=body.get("code"), dom_code=body.get("dom_code")) + program = Program( + id=existing_program._id, + name=existing_program._name, + code=body.get("code"), + dom_code=body.get("dom_code"), + modified=datetime.now(), + status="active") prog_engine.save(program) - return 200 + return program.as_dict() -def loadProgram(name): - existing_program = prog_engine.load(name) +def loadProgram(id): + existing_program = prog_engine.load(id) if existing_program: return existing_program.as_dict(), 200 else: - return 404 + return None, 404 -def deleteProgram(name): - prog_engine.delete(name) +def deleteProgram(id): + prog_engine.delete(id, logical=True) def listPrograms(): - return prog_engine.prog_list() + return prog_engine.prog_list(active_only=True) -def runProgram(name, body): +def runProgram(id): """ Execute the given program """ logging.debug("program_exec") - code = body.get('code') - prog = prog_engine.create(name, code) - return prog.execute() + prog = prog_engine.load(id) + if prog is not None: + return prog.execute() + else: + return {}, 404 -def stopProgram(name): +def stopProgram(id): """ Stop the program execution """ @@ -331,7 +343,7 @@ def stopProgram(name): prog.stop() return "ok" -def statusProgram(name): +def statusProgram(id): """ Expose the program status """ @@ -344,19 +356,20 @@ def statusProgram(name): ## Activities -def saveActivity(name, body): +def saveActivity(id, body): activity = body - activities.save(activity.get("name"), activity) + activity["id"] = id + return activities.save(activity) def saveAsNewActivity(body): activity = body - activities.save(activity.get("name"), activity) + return activities.save(activity) -def loadActivity(name=None, default=None): - return activities.load(name, default) +def loadActivity(id=None, default=None): + return activities.load(id, default) -def deleteActivity(name): - activities.delete(name), 200 +def deleteActivity(id): + activities.delete(id), 200 def listActivities(): return activities.list() @@ -387,7 +400,7 @@ def trainCNNModel(body): cnn.train_new_model(model_name=body.get("model_name"), architecture=body.get("architecture"), image_tags=body.get("image_tags"), - photos_meta=cam.get_photo_list(), + photos_meta=Camera.get_instance().get_photo_list(), training_steps=body.get("training_steps"), learning_rate=body.get("learning_rate")) @@ -403,4 +416,30 @@ def deleteCNNModel(name): cnn = CNNManager.get_instance() model_status = cnn.delete_model(model_name=name) - return model_status \ No newline at end of file + return model_status + +def cloudSyncRequest(): + CloudManager.get_instance().sync() + return {} + +def cloudSyncStatus(): + return CloudManager.get_instance().sync_status() + +def cloudRegistrationRequest(body): + CloudManager.get_instance().register(body) + return {} + +def cloudRegistrationDelete(): + CloudManager.get_instance().unregister() + return {} + +def cloudRegistrationStatus(): + registration = Config.read().get("cloud").get('registration', {}) + return { + "registered": CloudManager.get_instance().registration_status(), + "name": registration.get('name', ""), + "description": registration.get('description', ""), + "org_id": registration.get('org_id', ""), + "org_name": registration.get('org_name', ""), + "org_description": registration.get('org_description', "") + } diff --git a/coderbot/audio.py b/coderbot/audio.py index ed1a469e..f67f99ff 100644 --- a/coderbot/audio.py +++ b/coderbot/audio.py @@ -46,12 +46,12 @@ class Audio: _instance = None @classmethod - def get_instance(cls): + def get_instance(cls, settings=None): if cls._instance is None: - cls._instance = Audio() + cls._instance = Audio(settings) return cls._instance - def __init__(self): + def __init__(self, settings): self.pa = pyaudio.PyAudio() try: self.stream_in = self.MicrophoneStream(FORMAT, RATE, CHUNK) diff --git a/coderbot/camera.py b/coderbot/camera.py index 8866b9fd..5247206e 100644 --- a/coderbot/camera.py +++ b/coderbot/camera.py @@ -52,30 +52,30 @@ class Camera(object): _instance = None @classmethod - def get_instance(cls): + def get_instance(cls, settings=None): if cls._instance is None: - cls._instance = Camera() + cls._instance = Camera(settings) #cls._instance.start() return cls._instance - def __init__(self): + def __init__(self, settings): logging.info("starting camera") cam_props = {"width":640, "height":512, - "cv_image_factor": config.Config.get().get("cv_image_factor"), - "exposure_mode": config.Config.get().get("camera_exposure_mode"), - "framerate": config.Config.get().get("camera_framerate"), - "bitrate": config.Config.get().get("camera_jpeg_bitrate"), - "jpeg_quality": int(config.Config.get().get("camera_jpeg_quality"))} + "cv_image_factor": settings.get("cv_image_factor"), + "exposure_mode": settings.get("camera_exposure_mode"), + "framerate": settings.get("camera_framerate"), + "bitrate": settings.get("camera_jpeg_bitrate"), + "jpeg_quality": int(settings.get("camera_jpeg_quality"))} self._camera = camera.Camera(props=cam_props) self.recording = False self.video_start_time = time.time() + 8640000 self._image_time = 0 - self._cv_image_factor = int(config.Config.get().get("cv_image_factor", 4)) - self._image_refresh_timeout = float(config.Config.get().get("camera_refresh_timeout", 0.1)) - self._color_object_size_min = int(config.Config.get().get("camera_color_object_size_min", 80)) / (self._cv_image_factor * self._cv_image_factor) - self._color_object_size_max = int(config.Config.get().get("camera_color_object_size_max", 32000)) / (self._cv_image_factor * self._cv_image_factor) - self._path_object_size_min = int(config.Config.get().get("camera_path_object_size_min", 80)) / (self._cv_image_factor * self._cv_image_factor) - self._path_object_size_max = int(config.Config.get().get("camera_path_object_size_max", 32000)) / (self._cv_image_factor * self._cv_image_factor) + self._cv_image_factor = int(settings.get("cv_image_factor", 4)) + self._image_refresh_timeout = float(settings.get("camera_refresh_timeout", 0.1)) + self._color_object_size_min = int(settings.get("camera_color_object_size_min", 80)) / (self._cv_image_factor * self._cv_image_factor) + self._color_object_size_max = int(settings.get("camera_color_object_size_max", 32000)) / (self._cv_image_factor * self._cv_image_factor) + self._path_object_size_min = int(settings.get("camera_path_object_size_min", 80)) / (self._cv_image_factor * self._cv_image_factor) + self._path_object_size_max = int(settings.get("camera_path_object_size_max", 32000)) / (self._cv_image_factor * self._cv_image_factor) self.load_photo_metadata() if not self._photos: self._photos = [] @@ -86,7 +86,7 @@ def __init__(self): self.save_photo_metadata() self._cnn_classifiers = {} - cnn_model = config.Config.get().get("cnn_default_model", "") + cnn_model = settings.get("cnn_default_model", "") if cnn_model != "": try: self._cnn_classifiers[cnn_model] = CNNManager.get_instance().load_model(cnn_model) diff --git a/coderbot/cloud/sync.py b/coderbot/cloud/sync.py new file mode 100644 index 00000000..93ed8779 --- /dev/null +++ b/coderbot/cloud/sync.py @@ -0,0 +1,460 @@ +# Sync CoderBot configuration with remote Cloud configuration +# +# For all configuration entities (settings, activities, programs): +# check sync mode (upstream, downstream, both) +# if up: +# compare entity, if different, push changes +# if down: +# compare entity, if different, pull changes +# if both: +# compare entity, if different, take most recent and push/pull changes +# + +import os +import threading +from datetime import datetime, timezone +import logging +import json +from time import sleep + +from config import Config +from activity import Activities, ACTIVITY_KIND_USER, ACTIVITY_KIND_STOCK, ACTIVITY_STATUS_ACTIVE, ACTIVITY_STATUS_DELETED +from program import ProgramEngine +import program +import activity + +import cloud_api_robot_client +from cloud_api_robot_client.apis.tags import robot_sync_api +from cloud_api_robot_client.model.activity import Activity +from cloud_api_robot_client.model.program import Program +from cloud_api_robot_client.model.robot_data import RobotData +from cloud_api_robot_client.model.setting import Setting +from cloud_api_robot_client.model.robot_register_data import RobotRegisterData +from cloud_api_robot_client.model.robot_credentials import RobotCredentials + +SYNC_UPSTREAM = 'u' +SYNC_DOWNSTREAM = 'd' +SYNC_BIDIRECTIONAL = 'b' +SYNC_DISABLED = 'n' + +ENTITY_KIND_USER = "user" +ENTITY_KIND_STOCK = "stock" + +AUTH_FILE = "data/auth.json" + +class CloudManager(threading.Thread): + _instance = None + + _auth = {} + + @classmethod + def get_auth(cls): + return cls._auth + + @classmethod + def read_auth(cls): + with open(AUTH_FILE, 'r') as f: + cls._auth = json.load(f) + f.close() + return cls._auth + + @classmethod + def write_auth(cls, auth): + cls._auth = auth + f = open(AUTH_FILE, 'w') + json.dump(cls._auth, f) + return cls._auth + + @classmethod + def get_instance(cls): + if cls._instance is None: + cls._instance = CloudManager() + return cls._instance + + def __init__(self): + self._syncing = False + self._sync_status = { + "settings": "", + "activities": "", + "programs": "" + } + threading.Thread.__init__(self) + # Defining the host is optional and defaults to https://api.coderbot.org/api/v1 + # See configuration.py for a list of all supported configuration parameters. + self.configuration = cloud_api_robot_client.Configuration( + host = os.getenv("CODERBOT_CLOUD_API_ENDPOINT") + "/api/v1", + ) + try: + self.read_auth() + except FileNotFoundError: + self.write_auth({}) + self.start() + + def register(self, registration_request): + logging.info("register.check.token") + token = self.get_auth().get("token") + if token: + logging.warn("register.check.token_already_there") + return + reg_otp = registration_request.get("otp") + try: + self._sync_status["registration"] = "registering" + logging.info("register.get_token") + with cloud_api_robot_client.ApiClient(self.configuration) as api_client: + api_instance = robot_sync_api.RobotSyncApi(api_client) + body = RobotRegisterData( + otp=reg_otp, + ) + api_response = api_instance.register_robot(body=body) + logging.info(api_response.body) + token = api_response.body.get("token") + self.write_auth({"token":token}) + self._sync_status["registration"] = "registered" + except cloud_api_robot_client.ApiException as e: + logging.warn("Exception when calling register_robot RobotSyncApi: %s\n" % e) + raise + + def unregister(self): + self.write_auth({ "token": None }) + + def registration_status(self): + return self._auth.get("token") is not None + + def run(self): + while(True): + sync_period = int(Config.read().get("cloud").get("sync_period", "60")) + self.sync() + sleep(sync_period) + + def syncing(self): + return self._syncing + + def sync_status(self): + return self._sync_status + + def sync(self): + if self._syncing == True: + return + self._syncing = True + + settings = Config.read() + cloud_settings = settings.get("cloud") + logging.info("run.sync.begin") + sync_modes = cloud_settings.get("sync_modes", {"settings": "n", "activities": "n", "programs": "n"}) + + token = self.get_auth().get("token") + if token is not None: + try: + self.configuration.access_token = token + + # Enter a context with an instance of the API client + with cloud_api_robot_client.ApiClient(self.configuration) as api_client: + # Create an instance of the API class + api_instance = robot_sync_api.RobotSyncApi(api_client) + + self.sync_settings(api_instance, sync_modes["settings"]) + self.sync_activities(api_instance, sync_modes["activities"]) + self.sync_programs(api_instance, sync_modes["programs"]) + except Exception as e: + logging.warn("run.sync.api_not_available: " + str(e)) + raise e + + logging.info("run.sync.end") + self._syncing = False + + # def get_token_or_register(self, cloud_settings): + # logging.info("run.check.token") + # token = self.get_auth().get("token") + # reg_otp = cloud_settings.get("reg_otp") + # try: + # if token is None and reg_otp is not None: + # self._sync_status["registration"] = "registering" + # logging.info("run.get_token_or_register.get_token") + # with cloud_api_robßot_client.ApiClient(self.configuration) as api_client: + # api_instance = robot_sync_api.RobotSyncApi(api_client) + # body = RobotRegisterData( + # otp=reg_otp, + # ) + # api_response = api_instance.register_robot(body=body) + # logging.info(api_response.body) + # token = api_response.body.get("token") + # self.write_auth({"token":token}) + # self._sync_status["registration"] = "registered" + # return token + # except cloud_api_robot_client.ApiException as e: + # logging.warn("Exception when calling register_robot RobotSyncApi: %s\n" % e) + + # sync settings + def sync_settings(self, api_instance, sync_mode): + # Sync settings is different from syncing other entities, like activities and programs, since there can only + # be a single entity for each robot (both device and cloud twin). + # So the algo is simpler and favorites the "stock" entity over the "user" entity, if available. + try: + self._sync_status["settings"] = "syncing" + api_response = api_instance.get_robot_setting() + cloud_setting_object = api_response.body + cloud_setting = json.loads(cloud_setting_object.get('data')) + # sync only the "settings" and "cloud" sections, do not sync "network" + config = Config.read() + local_setting = { + "settings": config.get("settings") + } + local_most_recent = datetime.fromisoformat(cloud_setting_object.get("modified")).timestamp() < Config.modified() + cloud_kind_user = cloud_setting_object.get("kind") == ENTITY_KIND_USER + # logging.info(f"cloud_kind_user: {cloud_kind_user}, cloud_setting != local_setting: {cloud_setting != local_setting}, local_most_recent: {local_most_recent}, sync_mode in [SYNC_UPSTREAM, SYNC_BIDIRECTIONAL]: {sync_mode in [SYNC_UPSTREAM, SYNC_BIDIRECTIONAL]}") + # logging.info("settings.syncing: " + cloud_setting_object.get("id", "") + " name: " + cloud_setting_object.get("name", "")) + if cloud_kind_user and cloud_setting != local_setting and local_most_recent and sync_mode in [SYNC_UPSTREAM, SYNC_BIDIRECTIONAL]: + body = Setting( + id = cloud_setting_object.get('id'), + org_id = cloud_setting_object.get('org_id'), + name = cloud_setting_object.get('name'), + description = cloud_setting_object.get('description'), + data = json.dumps(local_setting), + kind = cloud_setting_object.get("kind"), + modified = datetime.now().isoformat(), + status = cloud_setting_object.get('status'), + ) + api_response = api_instance.set_robot_setting(body) + logging.info("settings.upstream") + elif cloud_setting != local_setting and sync_mode in [SYNC_DOWNSTREAM, SYNC_BIDIRECTIONAL]: # setting, down + config["settings"] = cloud_setting["settings"] + Config.write(config) + logging.info("settings.downstream") + self._sync_status["settings"] = "synced" + except cloud_api_robot_client.ApiException as e: + logging.warn("Exception when calling settings RobotSyncApi: %s\n", e) + self._sync_status["registration"] = "failed" + + def sync_activities(self, api_instance, sync_mode): + try: + self._sync_status["activities"] = "syncing" + activities_local_user = list() + activities_local_stock = list() + activities_local_to_be_deleted = list() + activities_local_map = {} + for a in Activities.get_instance().list(active_only=False): + if a.get("kind") == ACTIVITY_KIND_USER: + if a.get("status") == ACTIVITY_STATUS_ACTIVE: + activities_local_user.append(a) + if a.get("id") is not None: + activities_local_map[a.get("id")] = a + elif a.get("status") == ACTIVITY_STATUS_DELETED: + activities_local_to_be_deleted.append(a) + else: + activities_local_stock.append(a) + if a.get("id") is not None: + activities_local_map[a.get("id")] = a + # Get robot activities + api_response = api_instance.get_robot_activities() + cloud_activities = api_response.body + # cloud activities + activities_cloud_map = {} + for a in cloud_activities: + logging.info("cloud_activities %s, %s", str(a.get("status")), a.get("id")) + if a.get("status") == ACTIVITY_STATUS_ACTIVE: + activities_cloud_map[a.get("id")] = a + + # loop through local + for al in activities_local_user: + logging.info("activities.syncing: " + str(al.get("id")) + " name: " + str(al.get("name"))) + ac = activities_cloud_map.get(al.get("id")) + ac_al_equals = (ac is not None and ac.get("id") == al.get("id")) + + if ac is not None and not ac_al_equals: + al["modified"] = al.get("modified", datetime.now(tz=timezone.utc).isoformat()) + local_activity_more_recent = datetime.fromisoformat(ac.get("modified")).timestamp() < datetime.fromisoformat(al.get("modified")).timestamp() + if sync_mode == SYNC_UPSTREAM or (local_activity_more_recent and sync_mode == SYNC_BIDIRECTIONAL): + ac["data"] = al.get("data") + ac["modified"] = al.get("modified") + body = Activity( + id=ac.get("id"), + org_id=ac.get("org_id"), + name=al.get("name"), + description=al.get("description"), + data=json.dumps(al.get("data")), + kind = al.get("kind", ENTITY_KIND_STOCK), + modified=al.get("modified").isoformat(), + status='active', + ) + #logging.info("run.activities.cloud.saving") + api_response = api_instance.set_robot_activity(ac.get("id"), body) + logging.info("activities.update.upstream: " + al.get("name")) + elif sync_mode == "d" or (not local_activity_more_recent and sync_mode == SYNC_BIDIRECTIONAL): + al["data"] = ac.get("data") + al["modified"] = ac.get("modified") + Activities.get_instance().save(al) + logging.info("activities.update.downstream: " + al.get("id")) + elif ac is None and sync_mode in [SYNC_UPSTREAM, SYNC_BIDIRECTIONAL]: + body = Activity( + id=al.get("id"), + org_id="", + name=al.get("name"), + description=al.get("description"), + data=json.dumps(al), + kind=al.get("kind", ENTITY_KIND_STOCK), + modified=al.get("modified", datetime.now(tz=timezone.utc).isoformat()), + status="active", + ) + logging.info("activities.create.upstream - id %s, name: %s", al.get("id"), al.get("name")) + api_response = api_instance.create_robot_activity(body=body) + al["id"] = api_response.body["id"] + al["org_id"] = api_response.body["org_id"] + Activities.get_instance().save(al) + #logging.info("activities.create.upstream: " + al.get("name")) + elif ac is None and sync_mode in [SYNC_DOWNSTREAM]: + Activities.get_instance().delete(al.get("id")) + logging.info("activities.delete.downstream: " + al.get("name")) + + for k, ac in activities_cloud_map.items(): + if activities_local_map.get(k) is None and sync_mode in [SYNC_DOWNSTREAM, SYNC_BIDIRECTIONAL]: + logging.info("activities.create.downstream: " + ac.get("name")) + activity = json.loads(ac.get("data")) + activity["id"] = ac.get("id") + activity["org_id"] = ac.get("org_id") + activity["name"] = ac.get("name") + activity["description"] = ac.get("description") + activity["kind"] = ac.get("kind") + activity["status"] = ac.get("status") + Activities.get_instance().save(activity) + + # manage local user activities to be deleted locally and upstream + for al in activities_local_to_be_deleted: + if al.get("id") is not None: + logging.info("activities.delete.upstream: " + al.get("name")) + api_response = api_instance.delete_robot_activity(path_params={"activity_id":al.get("id")}) + # delete locally permanently + Activities.get_instance().delete(al.get("id"), logical=False) + + # manage local stock activities to be deleted locally + # for al in activities_local_stock: + # # logging.info("activities.check.stock.locally: " + al.get("name") + " id: " + str(al.get("id"))) + # if al.get("id") is not None and activities_cloud_map.get(al.get("id")) is None: + # logging.info("activities.delete.stock.locally: " + al.get("name")) + # # delete locally permanently + # Activities.get_instance().delete(al.get("id"), logical=False) + + self._sync_status["activities"] = "synced" + except cloud_api_robot_client.ApiException as e: + logging.warn("Exception when calling activities RobotSyncApi: %s\n" % e) + self._sync_status["activities"] = "failed" + + def sync_programs(self, api_instance, sync_mode): + try: + self._sync_status["programs"] = "syncing" + programs_local_user = list() + programs_local_stock = list() + programs_local_map = {} + programs_local_to_be_deleted = list() + for p in ProgramEngine.get_instance().prog_list(active_only=False): + if p.get("kind") == program.PROGRAM_KIND_USER: + if p.get("status") == program.PROGRAM_STATUS_ACTIVE: + programs_local_user.append(p) + if p.get("id") is not None: + programs_local_map[p.get("id")] = p + elif p.get("status") == program.PROGRAM_STATUS_DELETED: + programs_local_to_be_deleted.append(p) + else: + programs_local_stock.append(p) + if p.get("id") is not None: + programs_local_map[p.get("id")] = p + + # Get cloud programs + api_response = api_instance.get_robot_programs() + cloud_programs = api_response.body + # cloud programs in a map id : program + programs_cloud_map = {} # programs_cloud_map + for p in cloud_programs: + if p.get("status") == program.PROGRAM_STATUS_ACTIVE: + programs_cloud_map[p.get("id")] = p + + # sync local user programs + # manage user programs present locally and in "active" status + for pl in programs_local_user: + pc = programs_cloud_map.get(pl.get("id")) + pc_pl_equals = (pc is not None and + pc.get("id") == pl.get("id")) + logging.info("programs.syncing: " + str(pl.get("id")) + " name: " + pl.get("name")) + + if pc is not None and not pc_pl_equals: + # cloud program exists and is different from local + pl["modified"] = pl.get("modified", datetime.now(tz=timezone.utc).isoformat()) + local_program_more_recent = datetime.fromisoformat(pc.get("modified")).timestamp() < datetime.fromisoformat(pl.get("modified")).timestamp() + if sync_mode == SYNC_UPSTREAM or (local_program_more_recent and sync_mode == SYNC_BIDIRECTIONAL) and not to_be_deleted: + # cloud program exists and is less recent + pc["data"] = pl.get("data") + pc["modified"] = pl.get("modified") + body = Program( + id=pc.get("id"), + org_id=pc.get("org_id"), + name=pl.get("name"), + description=pl.get("description"), + code=pl.get("code"), + dom_code=pl.get("dom_code"), + kind=pl.get("kind"), + modified=pl.get("modified").isoformat(), + status='active', + ) + #logging.info("run.activities.cloud.saving") + api_response = api_instance.set_robot_program(pc.get("id"), body) + logging.info("programs.update.upstream: " + pl.get("name")) + elif sync_mode == SYNC_DOWNSTREAM or (not local_program_more_recent and sync_mode == SYNC_BIDIRECTIONAL): + # cloud program exists and is more recent + pl["data"] = pc.get("data") + pl["modified"] = pc.get("modified") + ProgramEngine.get_instance().save(program.Program.from_dict(pl)) + logging.info("programs.update.downstream: " + pl.get("name")) + elif pc is None and sync_mode in [SYNC_UPSTREAM, SYNC_BIDIRECTIONAL]: + # cloud program does not exist + body = Program( + id=pl.get("id"), + org_id="", + name=pl.get("name"), + description=pl.get("description", ""), + code=pl.get("code"), + dom_code=pl.get("dom_code"), + kind=pl.get("kind"), + modified=pl.get("modified", datetime.now(tz=timezone.utc).isoformat()), + status="active", + ) + api_response = api_instance.create_robot_program(body=body) + pl["id"] = api_response.body["id"] + pl["org_id"] = api_response.body["org_id"] + ProgramEngine.get_instance().save(program.Program.from_dict(pl)) + logging.info("programs.create.upstream: " + pl.get("name")) + elif pc is None and sync_mode in [SYNC_DOWNSTREAM]: + # cloud program does not exist, delete locally since sync_mode is downstream + ProgramEngine.get_instance().delete(pl.get("id")) + logging.info("programs.delete.downstream: " + pl.get("name")) + + # manage cloud programs not present locally in "active" status + for k, pc in programs_cloud_map.items(): + if programs_local_map.get(k) is None and sync_mode in [SYNC_DOWNSTREAM, SYNC_BIDIRECTIONAL]: + pl = program.Program(name=pc.get("name"), + code=pc.get("code"), + dom_code=pc.get("dom_code"), + kind=pc.get("kind"), + id=pc.get("id"), + modified=datetime.fromisoformat(pc.get("modified")), + status=pc.get("status")) + ProgramEngine.get_instance().save(pl) + logging.info("programs.create.downstream: " + pc.get("name")) + + # manage local user programs to be deleted locally and upstream + for pl in programs_local_to_be_deleted: + if pl.get("id") is not None: + logging.info("programs.delete.upstream: " + pl.get("name")) + api_response = api_instance.delete_robot_program(path_params={"program_id":pl.get("id")}) + # delete locally permanently + ProgramEngine.get_instance().delete(pl.get("id"), logical=False) + + # manage local stock programs to be deleted locally + # for pl in programs_local_stock: + # # logging.info("programs.check.stock.locally: " + pl.get("name") + " id: " + str(pl.get("id"))) + # if pl.get("id") is not None and programs_cloud_map.get(pl.get("id")) is None: + # logging.info("programs.delete.stock.locally: " + pl.get("name")) + # # delete locally permanently + # ProgramEngine.get_instance().delete(pl.get("name"), logical=False) + self._sync_status["programs"] = "synced" + except cloud_api_robot_client.ApiException as e: + logging.warn("Exception when calling programs RobotSyncApi: %s\n" % e) + self._sync_status["programs"] = "failed" \ No newline at end of file diff --git a/coderbot/cnn/cnn_manager.py b/coderbot/cnn/cnn_manager.py index 5ed012d2..6bd16db6 100644 --- a/coderbot/cnn/cnn_manager.py +++ b/coderbot/cnn/cnn_manager.py @@ -44,13 +44,13 @@ class CNNManager(object): instance = None @classmethod - def get_instance(cls): + def get_instance(cls, settings=None): if cls.instance is None: - cls.instance = CNNManager() + cls.instance = CNNManager(settings) return cls.instance - def __init__(self): + def __init__(self, settings): try: f = open(MODEL_METADATA, "r") self._models = json.load(f) diff --git a/coderbot/coderbot.py b/coderbot/coderbot.py index 37877506..44d4d31b 100644 --- a/coderbot/coderbot.py +++ b/coderbot/coderbot.py @@ -101,7 +101,7 @@ class CoderBot(object): # pylint: disable=too-many-instance-attributes - def __init__(self, motor_trim_factor=1.0, motor_min_power=0, motor_max_power=100, hw_version="5", pid_params=(0.8, 0.1, 0.01, 200, 0.01)): + def __init__(self, settings, motor_trim_factor=1.0, motor_min_power=0, motor_max_power=100, hw_version="5", pid_params=(0.8, 0.1, 0.01, 200, 0.01)): try: self._mpu = mpu.AccelGyroMag() logging.info("MPU available") @@ -157,9 +157,9 @@ def exit(self): s.cancel() @classmethod - def get_instance(cls, motor_trim_factor=1.0, motor_max_power=100, motor_min_power=0, hw_version="5", pid_params=(0.8, 0.1, 0.01, 200, 0.01)): + def get_instance(cls, settings=None, motor_trim_factor=1.0, motor_max_power=100, motor_min_power=0, hw_version="5", pid_params=(0.8, 0.1, 0.01, 200, 0.01)): if not cls.the_bot: - cls.the_bot = CoderBot(motor_trim_factor=motor_trim_factor, motor_max_power= motor_max_power, motor_min_power=motor_min_power, hw_version=hw_version, pid_params=pid_params) + cls.the_bot = CoderBot(settings=settings, motor_trim_factor=motor_trim_factor, motor_max_power= motor_max_power, motor_min_power=motor_min_power, hw_version=hw_version, pid_params=pid_params) return cls.the_bot def get_motor_power(self, speed): diff --git a/coderbot/config.py b/coderbot/config.py index 071dd7da..a2098973 100644 --- a/coderbot/config.py +++ b/coderbot/config.py @@ -52,3 +52,6 @@ def restore(cls): with open(CONFIG_DEFAULT_FILE) as f: cls.write(json.loads(f.read())) + @classmethod + def modified(cls): + return os.stat(CONFIG_FILE).st_mtime \ No newline at end of file diff --git a/coderbot/cv/image.py b/coderbot/cv/image.py index 24dd9efb..e9c6f12e 100644 --- a/coderbot/cv/image.py +++ b/coderbot/cv/image.py @@ -39,8 +39,7 @@ class Image(): _aruco_dict = cv2.aruco.Dictionary_get(cv2.aruco.DICT_ARUCO_ORIGINAL) _aruco_parameters = cv2.aruco.DetectorParameters_create() - #_face_cascade = cv2.CascadeClassifier('/usr/local/share/OpenCV/haarcascades/haarcascade_frontalface_default.xml') - _face_cascade = cv2.CascadeClassifier('/usr/share/opencv/lbpcascades/lbpcascade_frontalface.xml') + _face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml') def __init__(self, array): self._data = array @@ -85,7 +84,8 @@ def get_transform(cls, image_size_x): return tx def find_faces(self): - faces = self._face_cascade.detectMultiScale(self._data) + gray = cv2.cvtColor(self._data, cv2.COLOR_BGR2GRAY) + faces = self._face_cascade.detectMultiScale(gray) return faces def filter_color(self, color): diff --git a/coderbot/main.py b/coderbot/main.py index edcb53d6..fb001fb4 100644 --- a/coderbot/main.py +++ b/coderbot/main.py @@ -7,8 +7,9 @@ import logging.handlers import picamera import connexion - -from flask_cors import CORS +from connexion.options import SwaggerUIOptions +from connexion.middleware import MiddlewarePosition +from starlette.middleware.cors import CORSMiddleware from camera import Camera from motion import Motion @@ -18,36 +19,34 @@ from cnn.cnn_manager import CNNManager from event import EventManager from coderbot import CoderBot +from cloud.sync import CloudManager # Logging configuration logger = logging.getLogger() logger.setLevel(os.environ.get("LOGLEVEL", "INFO")) -# sh = logging.StreamHandler() -# formatter = logging.Formatter('%(message)s') -# sh.setFormatter(formatter) -# logger.addHandler(sh) ## (Connexion) Flask app configuration - # Serve a custom version of the swagger ui (Jinja2 templates) based on the default one # from the folder 'swagger-ui'. Clone the 'swagger-ui' repository inside the backend folder -options = {"swagger_ui": False} -connexionApp = connexion.App(__name__, options=options) - -# Connexion wraps FlaskApp, so app becomes connexionApp.app -app = connexionApp.app -# Access-Control-Allow-Origin -CORS(app) -app.debug = False +swagger_ui_options = SwaggerUIOptions(swagger_ui=True) +app = connexion.App(__name__, swagger_ui_options=swagger_ui_options) +app.add_middleware( + CORSMiddleware, + position=MiddlewarePosition.BEFORE_EXCEPTION, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) app.prog_engine = ProgramEngine.get_instance() ## New API and web application # API v1 is defined in v1.yml and its methods are in api.py -connexionApp.add_api('v1.yml') +app.add_api('v1.yml') def button_pushed(): - if app.bot_config.get('button_func') == "startstop": + if app.settings.get('button_func') == "startstop": prog = app.prog_engine.get_current_prog() if prog and prog.is_running(): prog.end() @@ -66,38 +65,53 @@ def run_server(): cam = None try: try: - app.bot_config = Config.read() - - bot = CoderBot.get_instance() - + settings = Config.read().get("settings") + # if settings.get("id") is None: + # settings["id"] = str(uuid.uuid4()) # init uuid for local settings + # Config.write() + + app.settings = settings + network_settings = Config.read().get("network") + + bot = CoderBot.get_instance(settings=settings, motor_trim_factor=float(settings.get('move_motor_trim', 1.0)), + motor_max_power=int(settings.get('motor_max_power', 100)), + motor_min_power=int(settings.get('motor_min_power', 0)), + hw_version=settings.get('hardware_version'), + pid_params=(float(settings.get('pid_kp', 1.0)), + float(settings.get('pid_kd', 0.1)), + float(settings.get('pid_ki', 0.01)), + float(settings.get('pid_max_speed', 200)), + float(settings.get('pid_sample_time', 0.01)))) try: - audio_device = Audio.get_instance() - audio_device.set_volume(int(app.bot_config.get('audio_volume_level')), 100) - audio_device.say(app.bot_config.get("sound_start")) + audio_device = Audio.get_instance(settings) + audio_device.set_volume(int(settings.get('audio_volume_level')), 100) + audio_device.say(settings.get("sound_start")) except Exception: logging.warning("Audio not present") - try: - cam = Camera.get_instance() - Motion.get_instance() + cam = Camera.get_instance(settings) + Motion.get_instance(settings) except picamera.exc.PiCameraError: logging.warning("Camera not present") - CNNManager.get_instance() + CNNManager.get_instance(settings) EventManager.get_instance("coderbot") - if app.bot_config.get('load_at_start') and app.bot_config.get('load_at_start'): - prog = app.prog_engine.load(app.bot_config.get('load_at_start')) + if settings.get('load_at_start') and settings.get('load_at_start'): + prog = app.prog_engine.load(settings.get('load_at_start')) prog.execute() + + CloudManager.get_instance() + except ValueError as e: - app.bot_config = {} + settings = {} logging.error(e) bot.set_callback(bot.GPIOS.PIN_PUSHBUTTON, button_pushed, 100) remove_doreset_file() - app.run(host="0.0.0.0", port=5000, debug=False, use_reloader=False, threaded=True) + app.run(host="0.0.0.0", port=5000) finally: if cam: cam.exit() diff --git a/coderbot/motion.py b/coderbot/motion.py index 95d66563..8fa5c14d 100644 --- a/coderbot/motion.py +++ b/coderbot/motion.py @@ -43,9 +43,9 @@ class Motion: # pylint: disable=too-many-instance-attributes - def __init__(self): - self.bot = CoderBot.get_instance() - self.cam = Camera.get_instance() + def __init__(self, settings): + self.bot = CoderBot.get_instance(settings) + self.cam = Camera.get_instance(settings) self.track_len = 2 self.detect_interval = 5 self.tracks = [] @@ -59,20 +59,19 @@ def __init__(self): self.target_dist = 0.0 self.delta_angle = 0.0 self.target_angle = 0.0 - cfg = Config.get() - self.power_angles = [[15, (int(cfg.get("move_power_angle_1")), -1)], - [4, (int(cfg.get("move_power_angle_2")), 0.05)], - [1, (int(cfg.get("move_power_angle_3")), 0.02)], + self.power_angles = [[15, (int(settings.get("move_power_angle_1")), -1)], + [4, (int(settings.get("move_power_angle_2")), 0.05)], + [1, (int(settings.get("move_power_angle_3")), 0.02)], [0, (0, 0)]] - self.image_width = 640 / int(cfg.get("cv_image_factor")) - self.image_heigth = 480 / int(cfg.get("cv_image_factor")) + self.image_width = 640 / int(settings.get("cv_image_factor")) + self.image_heigth = 480 / int(settings.get("cv_image_factor")) self.transform = image.Image.get_transform(self.image_width) _motion = None @classmethod - def get_instance(cls): + def get_instance(cls, settings=None): if not cls._motion: - cls._motion = Motion() + cls._motion = Motion(settings) return cls._motion def move(self, dist): diff --git a/coderbot/music.py b/coderbot/music.py index 0544b468..be1ac222 100644 --- a/coderbot/music.py +++ b/coderbot/music.py @@ -29,6 +29,7 @@ import os import sox import time +import logging class Music: _instance = None @@ -42,17 +43,17 @@ class Music: @classmethod - def get_instance(cls,managerPackage): + def get_instance(cls, managerPackage, settings=None): if cls._instance is None: - cls._instance = Music(managerPackage) + cls._instance = Music(managerPackage, settings) return cls._instance - def __init__(self,managerPackage): + def __init__(self, managerPackage, settings): #os.putenv('AUDIODRIVER', 'alsa') #os.putenv('AUDIODEV', 'hw:1,0') self.managerPackage = managerPackage - print("We have create a class: MUSICAL") + logging.info("We have created a class: MUSICAL") def test(self): tfm = sox.Transformer() @@ -71,7 +72,7 @@ def play_pause(self, duration): # @para alteration: if it is a diesis or a bemolle # @param time: duration of the note in seconds def play_note(self, note, instrument='piano', alteration='none', duration=1.0): - print(note) + logging.info("play_note: %s", note) tfm = sox.Transformer() duration = float(duration) @@ -85,7 +86,7 @@ def play_note(self, note, instrument='piano', alteration='none', duration=1.0): if note in self.noteDict : shift = self.noteDict[note]+ alt else: - print('note not exist') + logging.error('note does not exist') return tfm.pitch(shift, quick=False) @@ -93,7 +94,7 @@ def play_note(self, note, instrument='piano', alteration='none', duration=1.0): if self.managerPackage.isPackageAvailable(instrument): tfm.preview('./sounds/notes/' + instrument + '/audio.wav') else: - print("no instrument:"+str(instrument)+" present in this coderbot!") + logging.error("no instrument:"+str(instrument)+" present in this coderbot!") def play_animal(self, instrument, note='G2', alteration='none', duration=1.0): tfm = sox.Transformer() @@ -138,13 +139,13 @@ def play_animal(self, instrument, note='G2', alteration='none', duration=1.0): if note in self.noteDict : shift = self.noteDict[note]+ alt else: - print('note not exist') + logging.error('note does not exist') return if self.managerPackage.isPackageAvailable(instrument): tfm.preview('./sounds/notes/' + instrument + '/audio.wav') else: - print("no animal verse:"+str(instrument)+" present in this coderbot!") + logging.error("no animal verse:"+str(instrument)+" present in this coderbot!") return tfm.pitch(shift, quick=False) tfm.trim(0.0, end_time=0.5*duration) diff --git a/coderbot/musicPackages.py b/coderbot/musicPackages.py index e671b7e7..a96c9ba3 100644 --- a/coderbot/musicPackages.py +++ b/coderbot/musicPackages.py @@ -68,7 +68,7 @@ def addInterface(self, musicPackageInterface): class MusicPackageInterface: - def __init__(self,interfaceName,available,icon): + def __init__(self, interfaceName, available,icon): self.interfaceName = interfaceName self.available = available self.icon = icon @@ -86,12 +86,12 @@ class MusicPackageManager: _instance = None @classmethod - def get_instance(cls): + def get_instance(cls, settings=None): if cls._instance is None: - cls._instance = MusicPackageManager() + cls._instance = MusicPackageManager(settings) return cls._instance - def __init__(self): + def __init__(self, settings): self.packages = dict() with open('./sounds/notes/music_package.json') as json_file: data = json.load(json_file) diff --git a/coderbot/program.py b/coderbot/program.py index d76395fd..0cf275ff 100644 --- a/coderbot/program.py +++ b/coderbot/program.py @@ -20,10 +20,12 @@ import os import threading import json +import uuid import shutil import logging - +from datetime import datetime import math + from tinydb import TinyDB, Query from threading import Lock @@ -42,6 +44,10 @@ PROGRAM_SUFFIX = ".json" PROGRAMS_DB = "data/programs.json" PROGRAMS_PATH_DEFAULTS = "defaults/programs/" +PROGRAM_STATUS_ACTIVE = "active" +PROGRAM_STATUS_DELETED = "deleted" +PROGRAM_KIND_STOCK = "stock" +PROGRAM_KIND_USER = "user" musicPackageManager = musicPackages.MusicPackageManager.get_instance() @@ -75,67 +81,94 @@ class ProgramEngine: _instance = None - def __init__(self): + def __init__(self, settings): self._program = None self._log = "" self._programs = TinyDB(PROGRAMS_DB) + # initialise DB from default programs query = Query() self.lock = Lock() for dirname, dirnames, filenames, in os.walk(PROGRAMS_PATH_DEFAULTS): - dirnames for filename in filenames: if PROGRAM_PREFIX in filename: program_name = filename[len(PROGRAM_PREFIX):-len(PROGRAM_SUFFIX)] - logging.info("adding program %s in path %s as default %r", program_name, dirname, ("default" in dirname)) - with open(os.path.join(dirname, filename), "r") as f: - program_dict = json.load(f) - program_dict["default"] = "default" in dirname - program = Program.from_dict(program_dict) - self.save(program) + if self.load_by_name(program_name) is None: + logging.info("adding program %s in path %s as default %r", program_name, dirname, ("default" in dirname)) + with open(os.path.join(dirname, filename), "r") as f: + program_dict = json.load(f) + program_dict["kind"] = PROGRAM_KIND_STOCK + program_dict["status"] = PROGRAM_STATUS_ACTIVE + program = Program.from_dict(program_dict) + self.save(program) @classmethod - def get_instance(cls): + def get_instance(cls, settings=None): if not cls._instance: - cls._instance = ProgramEngine() + cls._instance = ProgramEngine(settings) return cls._instance - def prog_list(self): - return self._programs.all() + def prog_list(self, active_only = True): + programs = None + query = Query() + if active_only: + programs = self._programs.search(query.status == PROGRAM_STATUS_ACTIVE) + else: + programs = self._programs.all() + return programs def save(self, program): with self.lock: query = Query() + program._modified = datetime.now() self._program = program program_db_entry = self._program.as_dict() - if self._programs.search(query.name == program.name) != []: - self._programs.update(program_db_entry, query.name == program.name) + if self._programs.search(query.id == program._id) != []: + self._programs.update(program_db_entry, query.id == program._id) else: self._programs.insert(program_db_entry) + return program_db_entry - def load(self, name): + def load(self, id, active_only=True): with self.lock: query = Query() - program_db_entries = self._programs.search(query.name == name) + program_db_entries = None + if active_only: + program_db_entries = self._programs.search((query.id == id) & (query.status == PROGRAM_STATUS_ACTIVE)) + else: + program_db_entries = self._programs.search(query.id == id) if len(program_db_entries) > 0: prog_db_entry = program_db_entries[0] - logging.debug(prog_db_entry) + #logging.debug(prog_db_entry) self._program = Program.from_dict(prog_db_entry) return self._program return None - def delete(self, name): - with self.lock: + def load_by_name(self, name): + with self.lock: + program = None query = Query() - program_db_entries = self._programs.search(query.name == name) - self._programs.remove(query.name == name) + programs = self._programs.search((query.name == name) & (query.status == PROGRAM_STATUS_ACTIVE)) + if len(programs) > 0: + program = Program.from_dict(programs[0]) + return program - def create(self, name, code): - self._program = Program(name, code) - return self._program + def delete(self, id, logical = True): + with self.lock: + query = Query() + program_db_entries = self._programs.search(query.id == id) + if len(program_db_entries) > 0: + program_db_entry = program_db_entries[0] + if logical: + program_db_entry["status"] = PROGRAM_STATUS_DELETED + program_db_entry["modified"] = datetime.now().isoformat() + self._programs.update(program_db_entry, query.id == id) + else: + self._programs.remove(query.id == id) + return None - def is_running(self, name): - return self._program.is_running() and self._program.name == name + def is_running(self, id): + return self._program.is_running() and self._program.id == id def check_end(self): return self._program.check_end() @@ -159,12 +192,16 @@ class Program: def dom_code(self): return self._dom_code - def __init__(self, name, code=None, dom_code=None, default=False): + def __init__(self, name, id=None, description=None, code=None, dom_code=None, kind=PROGRAM_KIND_USER, modified=None, status=None): self._thread = None - self.name = name + self._id = id if id is not None else str(uuid.uuid4()) + self._name = name + self._description = description self._dom_code = dom_code self._code = code - self._default = default + self._kind = kind + self._modified = modified + self._status = status def execute(self, options={}): if self._running: @@ -195,8 +232,8 @@ def check_end(self): def is_running(self): return self._running - def is_default(self): - return self._default + def is_stock(self): + return self._kind == PROGRAM_KIND_STOCK def run(self, *args): options = args[0] @@ -238,12 +275,25 @@ def run(self, *args): self._running = False + @property + def name(self): + return self._name + def as_dict(self): - return {'name': self.name, + return {'name': self._name, 'dom_code': self._dom_code, 'code': self._code, - 'default': self._default} + 'kind': self._kind, + 'id': self._id, + 'modified': self._modified.isoformat(), + 'status': self._status} @classmethod def from_dict(cls, amap): - return Program(name=amap['name'], dom_code=amap['dom_code'], code=amap['code'], default=amap.get('default', False)) + return Program(name=amap['name'], + dom_code=amap['dom_code'], + code=amap['code'], + kind=amap.get('kind', PROGRAM_KIND_USER), + id=amap.get('id', None), + modified=datetime.fromisoformat(amap.get('modified', datetime.now().isoformat())), + status=amap.get('status', None),) diff --git a/coderbot/v1.yml b/coderbot/v1.yml index a3c8872f..86677cdd 100644 --- a/coderbot/v1.yml +++ b/coderbot/v1.yml @@ -30,6 +30,10 @@ paths: responses: 200: description: "ok" + content: + application/json: + schema: + $ref: '#/components/schemas/Settings' tags: - CoderBot configuration /settings/restore: @@ -151,6 +155,27 @@ paths: description: "ok" /programs: + post: + operationId: "api.saveNewProgram" + summary: "Save a new program" + tags: + - Program management + requestBody: + description: Program object + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Program' + responses: + 200: + description: "ok" + content: + application/json: + schema: + $ref: '#/components/schemas/Program' + 400: + description: "Failed to save the program" get: operationId: "api.listPrograms" summary: "Get the list of all the saved programs" @@ -160,12 +185,12 @@ paths: 200: description: "ok" - /programs/{name}: + /programs/{id}: get: operationId: "api.loadProgram" summary: "Get the program with the specified name" parameters: - - name: name + - name: id in: path required: true schema: @@ -179,7 +204,7 @@ paths: operationId: "api.deleteProgram" summary: "Delete a program" parameters: - - name: name + - name: id in: path required: true schema: @@ -195,7 +220,7 @@ paths: tags: - Program management parameters: - - name: name + - name: id in: path required: true schema: @@ -213,37 +238,30 @@ paths: 400: description: "Failed to save the program" - /programs/{name}/run: + /programs/{id}/run: post: operationId: "api.runProgram" summary: "Execute the given program" tags: - Program management parameters: - - name: name + - name: id in: path required: true schema: type: string - requestBody: - description: Program object - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Program' responses: 200: description: "ok" - /programs/{name}/status: + /programs/{id}/status: get: operationId: "api.statusProgram" summary: "Get the status of the given program" tags: - Program management parameters: - - name: name + - name: id in: path required: true schema: @@ -252,14 +270,14 @@ paths: 200: description: "ok" - /programs/{name}/stop: + /programs/{id}/stop: patch: operationId: "api.stopProgram" summary: "Stop the given program" tags: - Program management parameters: - - name: name + - name: id in: path required: true schema: @@ -277,6 +295,13 @@ paths: responses: 200: description: "ok" + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Activity' + post: operationId: "api.saveAsNewActivity" summary: "Save a new activity" @@ -292,14 +317,18 @@ paths: responses: 200: description: "ok" + content: + application/json: + schema: + $ref: '#/components/schemas/Activity' 400: description: "Failed to save the activity" - /activities/{name}: + /activities/{id}: get: operationId: "api.loadActivity" summary: "Get the activity with the specified name" parameters: - - name: name + - name: id in: path required: true schema: @@ -314,11 +343,16 @@ paths: responses: 200: description: "ok" + content: + application/json: + schema: + $ref: '#/components/schemas/Activity' + put: operationId: "api.saveActivity" - summary: "Save the activity with the specified name" + summary: "Save the activity with the specified id" parameters: - - name: name + - name: id in: path required: true schema: @@ -335,13 +369,18 @@ paths: responses: 200: description: "ok" + content: + application/json: + schema: + $ref: '#/components/schemas/Activity' + delete: operationId: "api.deleteActivity" summary: "Delete an activity" tags: - Activity management parameters: - - name: name + - name: id in: path required: true schema: @@ -606,6 +645,70 @@ paths: 200: description: "CNN Model deleted" + /cloud/registration: + post: + operationId: "api.cloudRegistrationRequest" + summary: "request a Registration activity" + tags: + - Cloud Sync + requestBody: + description: Registration Request parameters + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CloudRegistrationRequest' + responses: + 200: + description: "Cloud Sync request" + get: + operationId: "api.cloudRegistrationStatus" + summary: "get status of a Registration activity" + tags: + - Cloud Sync + responses: + 200: + description: "Cloud Sync request" + content: + application/json: + schema: + $ref: '#/components/schemas/CloudRegistrationStatus' + + delete: + operationId: "api.cloudRegistrationDelete" + summary: "get status of a Registration activity" + tags: + - Cloud Sync + responses: + 200: + description: "Cloud Sync request" + content: + application/json: + schema: + $ref: '#/components/schemas/CloudRegistrationStatus' + + /cloud/sync: + post: + operationId: "api.cloudSyncRequest" + summary: "request a Sync activity" + tags: + - Cloud Sync + responses: + 200: + description: "Cloud Sync request" + get: + operationId: "api.cloudSyncStatus" + summary: "request a Sync activity" + tags: + - Cloud Sync + responses: + 200: + description: "Cloud Sync request" + content: + application/json: + schema: + $ref: '#/components/schemas/CloudSyncStatus' + components: schemas: MoveParamsElapse: @@ -684,6 +787,11 @@ components: Program: type: object properties: + id: + type: string + #pattern: '^[[:xdigit:]]{8}(?:\-[[:xdigit:]]{4}){3}\-[[:xdigit:]]{12}$' + minLength: 36 + maxLength: 36 name: type: string pattern: '^[a-zA-ZA-zÀ-ú0-9-_ ]+$' @@ -705,6 +813,11 @@ components: Activity: type: object properties: + id: + type: string + #pattern: '^[[:xdigit:]]{8}(?:\-[[:xdigit:]]{4}){3}\-[[:xdigit:]]{12}$/ + minLength: 36 + maxLength: 36 name: type: string minLength: 1 @@ -715,11 +828,55 @@ components: maxLength: 256 default: type: boolean - stock: - type: boolean + kind: + type: string required: - name - description - default - - stock - + - kind + + CloudRegistrationRequest: + type: object + properties: + name: + type: string + minLength: 4 + maxLength: 64 + description: + type: string + minLength: 4 + maxLength: 256 + otp: + type: string + minLength: 8 + maxLength: 8 + + CloudRegistrationStatus: + type: object + properties: + name: + type: string + minLength: 1 + maxLength: 64 + description: + type: string + minLength: 0 + maxLength: 256 + org_id: + type: string + org_name: + type: string + org_description: + type: string + + CloudSyncStatus: + type: object + properties: + settings: + type: string + activities: + type: string + programs: + type: string + diff --git a/defaults/config.json b/defaults/config.json index c206cc0a..6c43815a 100644 --- a/defaults/config.json +++ b/defaults/config.json @@ -1,54 +1,62 @@ { - "move_power_angle_3":"60", - "cnn_default_model":"generic_fast_low", - "prog_maxblocks":"-1", - "camera_jpeg_quality":"5", - "show_page_control":"true", - "camera_framerate":"30", - "prog_scrollbars":"true", - "move_fw_speed":"100", - "motor_min_power":"0", - "motor_max_power":"100", - "prog_level":"adv", - "move_motor_trim":"1", - "cv_image_factor":"2", - "move_power_angle_1":"45", - "camera_path_object_size_min":"4000", - "button_func":"none", - "camera_color_object_size_min":"4000", - "camera_jpeg_bitrate":"1000000", - "move_fw_elapse":"1", - "show_control_move_commands":"true", - "camera_color_object_size_max":"160000", - "show_page_prefs":"true", - "camera_exposure_mode":"auto", - "ctrl_tr_elapse":"-1", - "show_page_program":"true", - "move_tr_elapse":"0.5", - "camera_path_object_size_max":"160000", - "sound_shutter":"$shutter", - "ctrl_fw_elapse":"-1", - "sound_stop":"$shutdown", - "ctrl_tr_speed":"80", - "ctrl_fw_speed":"100", - "move_tr_speed":"85", - "move_power_angle_2":"60", - "ctrl_hud_image":"", - "load_at_start":"", - "sound_start":"$startup", - "hardware_version":"5", - "audio_volume_level":"100", - "wifi_mode":"ap", - "wifi_ssid":"coderbot", - "wifi_psk":"coderbot", - "packages_installed":"", - "admin_password":"", - "pid_kp":"8.0", - "pid_kd":"0.0", - "pid_ki":"0.0", - "pid_max_speed":"200", - "pid_sample_time":"0.05", - "movement_use_mpu": "false", - "movement_use_motion": "false", - "movement_use_encoder": "true" -} + "settings":{ + "ctrl_hud_image":"", + "cv_image_factor":"2", + "camera_color_object_size_max":"160000", + "camera_color_object_size_min":"4000", + "camera_exposure_mode":"auto", + "camera_framerate":"30", + "camera_jpeg_bitrate":"1000000", + "camera_jpeg_quality":"5", + "camera_path_object_size_max":"160000", + "camera_path_object_size_min":"4000", + "cnn_default_model":"generic_fast_low", + "move_power_angle_1":"45", + "move_power_angle_2":"60", + "move_power_angle_3":"60", + "button_func":"none", + "move_motor_trim":"1", + "motor_min_power":"0", + "motor_max_power":"100", + "sound_start":"$startup", + "sound_stop":"$shutdown", + "sound_shutter":"$shutter", + "load_at_start":"", + "prog_level":"adv", + "move_fw_elapse":"1", + "move_fw_speed":"100", + "move_tr_elapse":"0.5", + "move_tr_speed":"85", + "pid_kp":"8.0", + "pid_kd":"0.0", + "pid_ki":"0.0", + "pid_max_speed":"200", + "pid_sample_time":"0.05", + "ctrl_fw_elapse":"-1", + "ctrl_fw_speed":"100", + "ctrl_tr_elapse":"-1", + "ctrl_tr_speed":"80", + "audio_volume_level":"100", + "admin_password":"", + "hardware_version":"5", + "prog_scrollbars":"true", + "movement_use_mpu":"false", + "movement_use_motion":"false", + "movement_use_encoder":"true", + "locale":"browser" + }, + "network":{ + "wifi_mode":"ap", + "wifi_ssid":"coderbot", + "wifi_psk":"coderbot" + }, + "cloud":{ + "sync_modes":{ + "activities":"n", + "programs":"n", + "settings":"n" + }, + "sync_period":"10", + "reg_otp":"AB1234CD" + } +} \ No newline at end of file diff --git a/defaults/programs/program_demo_ar_tags.json b/defaults/programs/program_demo_ar_tags.json index 329f45d9..901ff1bc 100644 --- a/defaults/programs/program_demo_ar_tags.json +++ b/defaults/programs/program_demo_ar_tags.json @@ -1 +1 @@ -{"dom_code": "tag_mapcodelistacodespositionsWHILETRUEtag_mapcodescodestag_mappositionspositionstag_mapGTcodes0GTGETLASTGETFIRSTpositions150codeGETFIRSTcodescodesEQcode1FORWARD1001LEFT1001EQcode2FORWARD1003EQcode3FORWARD1001RIGHT1001EQcode42RIGHT1000.5LEFT1000.5itSono arrivato!EQcode5itAttenzione!", "code": "tag_map = None\ncode = None\nlista = None\ncodes = None\npositions = None\n\n\nwhile True:\n get_prog_eng().check_end()\n tag_map = get_cam().find_ar_code()\n codes = tag_map.get('codes')\n positions = tag_map.get('positions')\n if len(codes) > 0:\n if positions[0][-1] > 150:\n code = codes[0]\n get_cam().set_text(codes)\n if code == 1:\n get_bot().forward(speed=100, elapse=1)\n get_bot().left(speed=100, elapse=1)\n elif code == 2:\n get_bot().forward(speed=100, elapse=3)\n elif code == 3:\n get_bot().forward(speed=100, elapse=1)\n get_bot().right(speed=100, elapse=1)\n elif code == 4:\n for count in range(2):\n get_prog_eng().check_end()\n get_bot().right(speed=100, elapse=0.5)\n get_bot().left(speed=100, elapse=0.5)\n get_audio().say('Sono arrivato!', locale=\"it\")\n elif code == 5:\n get_audio().say('Attenzione!', locale=\"it\")\n get_cam().set_text('')\n", "name": "ar_bot"} \ No newline at end of file +{"id": "5b4cf283-1666-4ada-87c8-234ff0ed37e8", "dom_code": "tag_mapcodelistacodespositionsWHILETRUEtag_mapcodescodestag_mappositionspositionstag_mapGTcodes0GTGETLASTGETFIRSTpositions150codeGETFIRSTcodescodesEQcode1FORWARD1001LEFT1001EQcode2FORWARD1003EQcode3FORWARD1001RIGHT1001EQcode42RIGHT1000.5LEFT1000.5itSono arrivato!EQcode5itAttenzione!", "code": "tag_map = None\ncode = None\nlista = None\ncodes = None\npositions = None\n\n\nwhile True:\n get_prog_eng().check_end()\n tag_map = get_cam().find_ar_code()\n codes = tag_map.get('codes')\n positions = tag_map.get('positions')\n if len(codes) > 0:\n if positions[0][-1] > 150:\n code = codes[0]\n get_cam().set_text(codes)\n if code == 1:\n get_bot().forward(speed=100, elapse=1)\n get_bot().left(speed=100, elapse=1)\n elif code == 2:\n get_bot().forward(speed=100, elapse=3)\n elif code == 3:\n get_bot().forward(speed=100, elapse=1)\n get_bot().right(speed=100, elapse=1)\n elif code == 4:\n for count in range(2):\n get_prog_eng().check_end()\n get_bot().right(speed=100, elapse=0.5)\n get_bot().left(speed=100, elapse=0.5)\n get_audio().say('Sono arrivato!', locale=\"it\")\n elif code == 5:\n get_audio().say('Attenzione!', locale=\"it\")\n get_cam().set_text('')\n", "name": "demo_ar_tags"} \ No newline at end of file diff --git a/defaults/programs/program_demo_cat_follower.json b/defaults/programs/program_demo_cat_follower.json index 2c744cc5..bf72dae0 100644 --- a/defaults/programs/program_demo_cat_follower.json +++ b/defaults/programs/program_demo_cat_follower.json @@ -1 +1 @@ -{"name": "cat_follower", "dom_code": "objectclasspositionpos_xWHILETRUEobjectGETFIRSTgeneric_object_detectclassGETFIRSTobjectEQclasscatpositionGETFROM_STARTobject3pos_xDIVIDEADDGETFROM_STARTposition1GETFROM_STARTposition32classLTpos_x40LEFT600.1GTpos_x60RIGHT600.1FORWARD1000.2object", "code": "object2 = None\nclass2 = None\nposition = None\npos_x = None\n\n\nwhile True:\n get_prog_eng().check_end()\n object2 = get_cam().cnn_detect_objects(\"generic_object_detect\")[0]\n class2 = object2[0]\n if class2 == 'cat':\n position = object2[2]\n pos_x = (position[0] + position[2]) / 2\n get_cam().set_text(class2)\n if pos_x < 40:\n get_bot().left(speed=60, elapse=0.1)\n elif pos_x > 60:\n get_bot().right(speed=60, elapse=0.1)\n else:\n get_bot().forward(speed=100, elapse=0.2)\n else:\n get_cam().set_text(object2)\n"} \ No newline at end of file +{"id": "a4e429dd-b5b4-4f5f-8c2e-5e5f5d557aff", "name": "demo_cat_follower", "dom_code": "objectclasspositionpos_xWHILETRUEobjectGETFIRSTgeneric_object_detectclassGETFIRSTobjectEQclasscatpositionGETFROM_STARTobject3pos_xDIVIDEADDGETFROM_STARTposition1GETFROM_STARTposition32classLTpos_x40LEFT600.1GTpos_x60RIGHT600.1FORWARD1000.2object", "code": "object2 = None\nclass2 = None\nposition = None\npos_x = None\n\n\nwhile True:\n get_prog_eng().check_end()\n object2 = get_cam().cnn_detect_objects(\"generic_object_detect\")[0]\n class2 = object2[0]\n if class2 == 'cat':\n position = object2[2]\n pos_x = (position[0] + position[2]) / 2\n get_cam().set_text(class2)\n if pos_x < 40:\n get_bot().left(speed=60, elapse=0.1)\n elif pos_x > 60:\n get_bot().right(speed=60, elapse=0.1)\n else:\n get_bot().forward(speed=100, elapse=0.2)\n else:\n get_cam().set_text(object2)\n"} \ No newline at end of file diff --git a/defaults/programs/program_demo_color_seeker.json b/defaults/programs/program_demo_color_seeker.json index 75c14d28..c5eae413 100644 --- a/defaults/programs/program_demo_color_seeker.json +++ b/defaults/programs/program_demo_color_seeker.json @@ -1 +1 @@ -{"dom_code": "WHILETRUEdistDIST#96b73cangleANGLE#96b73cDistance: dist angle: angleGTdist32FORWARD1000.1ANDLTdist28GTEdist0BACKWARD1000.1GTangle5RIGHT300.1LTangle-5LEFT300.1", "code": "dist = None\nangle = None\n\n\nwhile True:\n get_prog_eng().check_end()\n dist = get_cam().find_color('#96b73c')[0]\n angle = get_cam().find_color('#96b73c')[1]\n get_cam().set_text(''.join([str(temp_value) for temp_value in ['Distance: ', dist, ' angle: ', angle]]))\n if dist > 32:\n get_bot().forward(speed=100, elapse=0.1)\n elif dist < 28 and dist >= 0:\n get_bot().backward(speed=100, elapse=0.1)\n if angle > 5:\n get_bot().right(speed=30, elapse=0.1)\n elif angle < -5:\n get_bot().left(speed=30, elapse=0.1)\n", "name": "colour_seeker"} \ No newline at end of file +{"id": "d1601ee4-bc8c-49f4-a79e-0015b7c7b1d7", "dom_code": "WHILETRUEdistDIST#96b73cangleANGLE#96b73cDistance: dist angle: angleGTdist32FORWARD1000.1ANDLTdist28GTEdist0BACKWARD1000.1GTangle5RIGHT300.1LTangle-5LEFT300.1", "code": "dist = None\nangle = None\n\n\nwhile True:\n get_prog_eng().check_end()\n dist = get_cam().find_color('#96b73c')[0]\n angle = get_cam().find_color('#96b73c')[1]\n get_cam().set_text(''.join([str(temp_value) for temp_value in ['Distance: ', dist, ' angle: ', angle]]))\n if dist > 32:\n get_bot().forward(speed=100, elapse=0.1)\n elif dist < 28 and dist >= 0:\n get_bot().backward(speed=100, elapse=0.1)\n if angle > 5:\n get_bot().right(speed=30, elapse=0.1)\n elif angle < -5:\n get_bot().left(speed=30, elapse=0.1)\n", "name": "demo_color_seeker"} \ No newline at end of file diff --git a/defaults/programs/program_demo_io_ext.json b/defaults/programs/program_demo_io_ext.json index 3265bfcb..91632161 100644 --- a/defaults/programs/program_demo_io_ext.json +++ b/defaults/programs/program_demo_io_ext.json @@ -1 +1 @@ -{"name": "test_io_ext", "dom_code": "Analog_Input_1WHILETRUEAnalog_Input_10Analog Input 1: Analog_Input_1GTAnalog_Input_11000TRUE0FALSE", "code": "Analog_Input_1 = None\n\n\nwhile True:\n get_prog_eng().check_end()\n Analog_Input_1 = get_atmega().get_input(0)\n get_cam().set_text('Analog Input 1: ' + str(Analog_Input_1))\n if Analog_Input_1 > 100:\n get_atmega().set_output(0, True)\n else:\n get_atmega().set_output(0, False)\n"} \ No newline at end of file +{"id": "30e8ea4b-967d-41bb-8d6a-67e12f7f0008", "name": "demo_io_ext", "dom_code": "Analog_Input_1WHILETRUEAnalog_Input_10Analog Input 1: Analog_Input_1GTAnalog_Input_11000TRUE0FALSE", "code": "Analog_Input_1 = None\n\n\nwhile True:\n get_prog_eng().check_end()\n Analog_Input_1 = get_atmega().get_input(0)\n get_cam().set_text('Analog Input 1: ' + str(Analog_Input_1))\n if Analog_Input_1 > 100:\n get_atmega().set_output(0, True)\n else:\n get_atmega().set_output(0, False)\n"} \ No newline at end of file diff --git a/defaults/programs/program_demo_line_follower.json b/defaults/programs/program_demo_line_follower.json index da7ba861..2dc76af8 100644 --- a/defaults/programs/program_demo_line_follower.json +++ b/defaults/programs/program_demo_line_follower.json @@ -1 +1 @@ -{"dom_code": "line_x_1listaline_x_listline_x_2ar_codear_code_listWHILETRUEline_x_listline_x_1GETFIRSTline_x_listar_code_listcodesar_code_listar_codeGETFIRSTar_code_listar_code0ar_codeEQar_code18080-110001000EQar_code26060-120001550EQar_code66060-115502000GTEline_x_10GTline_x_16040-40-1MINUSline_x_150MINUSline_x_150LTline_x_140-4040-1MINUS50line_x_1MINUS50line_x_15050-1150150", "code": "line_x_1 = None\nlista = None\nline_x_list = None\nline_x_2 = None\nar_code = None\nar_code_list = None\n\n\nwhile True:\n get_prog_eng().check_end()\n line_x_list = get_cam().find_line()\n line_x_1 = line_x_list[0]\n ar_code_list = get_cam().find_ar_code().get('codes')\n if not not len(ar_code_list):\n ar_code = ar_code_list[0]\n else:\n ar_code = 0\n get_cam().set_text(ar_code)\n if ar_code == 1:\n get_bot().motor_control(speed_left=80, speed_right=80, elapse=-1, steps_left=1000, steps_right=1000)\n elif ar_code == 2:\n get_bot().motor_control(speed_left=60, speed_right=60, elapse=-1, steps_left=2000, steps_right=1550)\n elif ar_code == 6:\n get_bot().motor_control(speed_left=60, speed_right=60, elapse=-1, steps_left=1550, steps_right=2000)\n else:\n if line_x_1 >= 0:\n if line_x_1 > 60:\n get_bot().motor_control(speed_left=40, speed_right=-40, elapse=-1, steps_left=line_x_1 - 50, steps_right=line_x_1 - 50)\n elif line_x_1 < 40:\n get_bot().motor_control(speed_left=-40, speed_right=40, elapse=-1, steps_left=50 - line_x_1, steps_right=50 - line_x_1)\n else:\n get_bot().motor_control(speed_left=50, speed_right=50, elapse=-1, steps_left=150, steps_right=150)\n", "name": "line_follower"} \ No newline at end of file +{"id": "f893bc1c-9d22-4bbb-ac3c-05637c49d307", "dom_code": "line_x_1listaline_x_listline_x_2ar_codear_code_listWHILETRUEline_x_listline_x_1GETFIRSTline_x_listar_code_listcodesar_code_listar_codeGETFIRSTar_code_listar_code0ar_codeEQar_code18080-110001000EQar_code26060-120001550EQar_code66060-115502000GTEline_x_10GTline_x_16040-40-1MINUSline_x_150MINUSline_x_150LTline_x_140-4040-1MINUS50line_x_1MINUS50line_x_15050-1150150", "code": "line_x_1 = None\nlista = None\nline_x_list = None\nline_x_2 = None\nar_code = None\nar_code_list = None\n\n\nwhile True:\n get_prog_eng().check_end()\n line_x_list = get_cam().find_line()\n line_x_1 = line_x_list[0]\n ar_code_list = get_cam().find_ar_code().get('codes')\n if not not len(ar_code_list):\n ar_code = ar_code_list[0]\n else:\n ar_code = 0\n get_cam().set_text(ar_code)\n if ar_code == 1:\n get_bot().motor_control(speed_left=80, speed_right=80, elapse=-1, steps_left=1000, steps_right=1000)\n elif ar_code == 2:\n get_bot().motor_control(speed_left=60, speed_right=60, elapse=-1, steps_left=2000, steps_right=1550)\n elif ar_code == 6:\n get_bot().motor_control(speed_left=60, speed_right=60, elapse=-1, steps_left=1550, steps_right=2000)\n else:\n if line_x_1 >= 0:\n if line_x_1 > 60:\n get_bot().motor_control(speed_left=40, speed_right=-40, elapse=-1, steps_left=line_x_1 - 50, steps_right=line_x_1 - 50)\n elif line_x_1 < 40:\n get_bot().motor_control(speed_left=-40, speed_right=40, elapse=-1, steps_left=50 - line_x_1, steps_right=50 - line_x_1)\n else:\n get_bot().motor_control(speed_left=50, speed_right=50, elapse=-1, steps_left=150, steps_right=150)\n", "name": "demo_line_follower"} \ No newline at end of file diff --git a/defaults/programs/program_demo_obstacle_avoidance.json b/defaults/programs/program_demo_obstacle_avoidance.json index ab3e4a8e..899ffe38 100644 --- a/defaults/programs/program_demo_obstacle_avoidance.json +++ b/defaults/programs/program_demo_obstacle_avoidance.json @@ -1 +1 @@ -{"name": "demo_obstacle_avoidance", "code": "while True:\n get_prog_eng().check_end()\n if get_bot().get_sonar_distance(0) < 15:\n get_bot().right(speed=100, elapse=0.5)\n else:\n get_bot().forward(speed=100, elapse=1)\n", "dom_code": "WHILETRUELT015RIGHT1000.5FORWARD1001"} \ No newline at end of file +{"id": "0cfd0dd1-7ec3-4f34-9720-41f42b0209e1", "name": "demo_obstacle_avoidance", "code": "while True:\n get_prog_eng().check_end()\n if get_bot().get_sonar_distance(0) < 15:\n get_bot().right(speed=100, elapse=0.5)\n else:\n get_bot().forward(speed=100, elapse=1)\n", "dom_code": "WHILETRUELT015RIGHT1000.5FORWARD1001"} \ No newline at end of file diff --git a/defaults/programs/program_demo_roboetologist.json b/defaults/programs/program_demo_roboetologist.json index b53ba7e2..1a50d542 100644 --- a/defaults/programs/program_demo_roboetologist.json +++ b/defaults/programs/program_demo_roboetologist.json @@ -1 +1 @@ -{"name":"roboetologia","dom_code":"attesavelocita_maxvar_marciaIndietrovar_giravoltevar_ritiratavelocitastancoScodinzolaDescrivi questa funzione...3RIGHT50MULTIPLY100velocita1100velocita_max0.30.2attesaLEFT50MULTIPLY100velocita1100velocita_max0.50.2attesaRIGHT50MULTIPLY100velocita1100velocita_max0.30.2attesaEvita_OstacoliDescrivi questa funzione...15LT020BACKWARD50MULTIPLY100velocita1100velocita_max0.50.2attesaRIGHT50MULTIPLY100velocita1100velocita_max0.50.2attesaLT120LEFT50MULTIPLY100velocita1100velocita_max0.50.2attesaLT220RIGHT50MULTIPLY100velocita1100velocita_max0.50.2attesaFORWARD50MULTIPLY100velocita1100velocita_max0.50.2attesaattesa0.2velocita_max80var_marciaIndietro0var_giravolte0var_ritirata0stanco0velocita1WHILETRUEstanco1var_marciaIndietro1var_giravolte1var_ritirata1EQstanco5stanco0velocita1GTstanco3velocita0.5EQvar_ritirata4var_ritirata0EQvar_giravolte8var_giravolte05EQvar_marciaIndietro6var_marciaIndietro0GiravolteDescrivi questa funzione...RIGHT50MULTIPLY100velocita1100velocita_max20.2attesaPatugliamentoDescrivi questa funzione...3RIGHT50MULTIPLY100velocita1100velocita_max10.5Ritirata_e_fugaDescrivi questa funzione...BACKWARD50MULTIPLY100velocita1100velocita_max10.2attesaLEFT50MULTIPLY100velocita1100velocita_max30.2attesaFORWARD50MULTIPLY100velocita1100velocita_max10.2attesaMarciaIndietro_e_ScartoDescrivi questa funzione...BACKWARD50MULTIPLY100velocita1100velocita_max10.2attesaRIGHT50MULTIPLY100velocita1100velocita_max10.2attesa","code":"from numbers import Number\n\nattesa = None\nvelocita_max = None\nvar_marciaIndietro = None\nvar_giravolte = None\nvar_ritirata = None\nvelocita = None\nstanco = None\n\n# Descrivi questa funzione...\ndef Scodinzola():\n global attesa, velocita_max, var_marciaIndietro, var_giravolte, var_ritirata, velocita, stanco\n get_prog_eng().check_end()\n for count in range(3):\n get_prog_eng().check_end()\n get_bot().right(speed=min(max(100 * velocita, 1), velocita_max), elapse=0.3)\n get_bot().sleep(attesa)\n get_bot().left(speed=min(max(100 * velocita, 1), velocita_max), elapse=0.5)\n get_bot().sleep(attesa)\n get_bot().right(speed=min(max(100 * velocita, 1), velocita_max), elapse=0.3)\n get_bot().sleep(attesa)\n\n# Descrivi questa funzione...\ndef Evita_Ostacoli():\n global attesa, velocita_max, var_marciaIndietro, var_giravolte, var_ritirata, velocita, stanco\n get_prog_eng().check_end()\n for count2 in range(15):\n get_prog_eng().check_end()\n if get_bot().get_sonar_distance(0) < 20:\n get_bot().backward(speed=min(max(100 * velocita, 1), velocita_max), elapse=0.5)\n get_bot().sleep(attesa)\n get_bot().right(speed=min(max(100 * velocita, 1), velocita_max), elapse=0.5)\n get_bot().sleep(attesa)\n elif get_bot().get_sonar_distance(1) < 20:\n get_bot().left(speed=min(max(100 * velocita, 1), velocita_max), elapse=0.5)\n get_bot().sleep(attesa)\n elif get_bot().get_sonar_distance(2) < 20:\n get_bot().right(speed=min(max(100 * velocita, 1), velocita_max), elapse=0.5)\n get_bot().sleep(attesa)\n else:\n get_bot().forward(speed=min(max(100 * velocita, 1), velocita_max), elapse=0.5)\n get_bot().sleep(attesa)\n\n# Descrivi questa funzione...\ndef Giravolte():\n global attesa, velocita_max, var_marciaIndietro, var_giravolte, var_ritirata, velocita, stanco\n get_prog_eng().check_end()\n get_bot().right(speed=min(max(100 * velocita, 1), velocita_max), elapse=2)\n get_bot().sleep(attesa)\n\n# Descrivi questa funzione...\ndef Patugliamento():\n global attesa, velocita_max, var_marciaIndietro, var_giravolte, var_ritirata, velocita, stanco\n get_prog_eng().check_end()\n for count3 in range(3):\n get_prog_eng().check_end()\n get_bot().right(speed=min(max(100 * velocita, 1), velocita_max), elapse=1)\n get_bot().sleep(0.5)\n\n# Descrivi questa funzione...\ndef Ritirata_e_fuga():\n global attesa, velocita_max, var_marciaIndietro, var_giravolte, var_ritirata, velocita, stanco\n get_prog_eng().check_end()\n get_bot().backward(speed=min(max(100 * velocita, 1), velocita_max), elapse=1)\n get_bot().sleep(attesa)\n get_bot().left(speed=min(max(100 * velocita, 1), velocita_max), elapse=3)\n get_bot().sleep(attesa)\n get_bot().forward(speed=min(max(100 * velocita, 1), velocita_max), elapse=1)\n get_bot().sleep(attesa)\n\n# Descrivi questa funzione...\ndef MarciaIndietro_e_Scarto():\n global attesa, velocita_max, var_marciaIndietro, var_giravolte, var_ritirata, velocita, stanco\n get_prog_eng().check_end()\n get_bot().backward(speed=min(max(100 * velocita, 1), velocita_max), elapse=1)\n get_bot().sleep(attesa)\n get_bot().right(speed=min(max(100 * velocita, 1), velocita_max), elapse=1)\n get_bot().sleep(attesa)\n\n\nattesa = 0.2\nvelocita_max = 80\nvar_marciaIndietro = 0\nvar_giravolte = 0\nvar_ritirata = 0\nstanco = 0\nvelocita = 1\nwhile True:\n get_prog_eng().check_end()\n stanco = (stanco if isinstance(stanco, Number) else 0) + 1\n var_marciaIndietro = (var_marciaIndietro if isinstance(var_marciaIndietro, Number) else 0) + 1\n var_giravolte = (var_giravolte if isinstance(var_giravolte, Number) else 0) + 1\n var_ritirata = (var_ritirata if isinstance(var_ritirata, Number) else 0) + 1\n if stanco == 5:\n stanco = 0\n velocita = 1\n elif stanco > 3:\n velocita = 0.5\n Scodinzola()\n Evita_Ostacoli()\n Patugliamento()\n if var_ritirata == 4:\n var_ritirata = 0\n Ritirata_e_fuga()\n if var_giravolte == 8:\n var_giravolte = 0\n Giravolte()\n get_bot().sleep(5)\n if var_marciaIndietro == 6:\n var_marciaIndietro = 0\n MarciaIndietro_e_Scarto()\n","default":""} \ No newline at end of file +{"id": "a3a01609-abb9-471b-b1fb-9cbb126d67c8", "name":"demo_roboetologist","dom_code":"attesavelocita_maxvar_marciaIndietrovar_giravoltevar_ritiratavelocitastancoScodinzolaDescrivi questa funzione...3RIGHT50MULTIPLY100velocita1100velocita_max0.30.2attesaLEFT50MULTIPLY100velocita1100velocita_max0.50.2attesaRIGHT50MULTIPLY100velocita1100velocita_max0.30.2attesaEvita_OstacoliDescrivi questa funzione...15LT020BACKWARD50MULTIPLY100velocita1100velocita_max0.50.2attesaRIGHT50MULTIPLY100velocita1100velocita_max0.50.2attesaLT120LEFT50MULTIPLY100velocita1100velocita_max0.50.2attesaLT220RIGHT50MULTIPLY100velocita1100velocita_max0.50.2attesaFORWARD50MULTIPLY100velocita1100velocita_max0.50.2attesaattesa0.2velocita_max80var_marciaIndietro0var_giravolte0var_ritirata0stanco0velocita1WHILETRUEstanco1var_marciaIndietro1var_giravolte1var_ritirata1EQstanco5stanco0velocita1GTstanco3velocita0.5EQvar_ritirata4var_ritirata0EQvar_giravolte8var_giravolte05EQvar_marciaIndietro6var_marciaIndietro0GiravolteDescrivi questa funzione...RIGHT50MULTIPLY100velocita1100velocita_max20.2attesaPatugliamentoDescrivi questa funzione...3RIGHT50MULTIPLY100velocita1100velocita_max10.5Ritirata_e_fugaDescrivi questa funzione...BACKWARD50MULTIPLY100velocita1100velocita_max10.2attesaLEFT50MULTIPLY100velocita1100velocita_max30.2attesaFORWARD50MULTIPLY100velocita1100velocita_max10.2attesaMarciaIndietro_e_ScartoDescrivi questa funzione...BACKWARD50MULTIPLY100velocita1100velocita_max10.2attesaRIGHT50MULTIPLY100velocita1100velocita_max10.2attesa","code":"from numbers import Number\n\nattesa = None\nvelocita_max = None\nvar_marciaIndietro = None\nvar_giravolte = None\nvar_ritirata = None\nvelocita = None\nstanco = None\n\n# Descrivi questa funzione...\ndef Scodinzola():\n global attesa, velocita_max, var_marciaIndietro, var_giravolte, var_ritirata, velocita, stanco\n get_prog_eng().check_end()\n for count in range(3):\n get_prog_eng().check_end()\n get_bot().right(speed=min(max(100 * velocita, 1), velocita_max), elapse=0.3)\n get_bot().sleep(attesa)\n get_bot().left(speed=min(max(100 * velocita, 1), velocita_max), elapse=0.5)\n get_bot().sleep(attesa)\n get_bot().right(speed=min(max(100 * velocita, 1), velocita_max), elapse=0.3)\n get_bot().sleep(attesa)\n\n# Descrivi questa funzione...\ndef Evita_Ostacoli():\n global attesa, velocita_max, var_marciaIndietro, var_giravolte, var_ritirata, velocita, stanco\n get_prog_eng().check_end()\n for count2 in range(15):\n get_prog_eng().check_end()\n if get_bot().get_sonar_distance(0) < 20:\n get_bot().backward(speed=min(max(100 * velocita, 1), velocita_max), elapse=0.5)\n get_bot().sleep(attesa)\n get_bot().right(speed=min(max(100 * velocita, 1), velocita_max), elapse=0.5)\n get_bot().sleep(attesa)\n elif get_bot().get_sonar_distance(1) < 20:\n get_bot().left(speed=min(max(100 * velocita, 1), velocita_max), elapse=0.5)\n get_bot().sleep(attesa)\n elif get_bot().get_sonar_distance(2) < 20:\n get_bot().right(speed=min(max(100 * velocita, 1), velocita_max), elapse=0.5)\n get_bot().sleep(attesa)\n else:\n get_bot().forward(speed=min(max(100 * velocita, 1), velocita_max), elapse=0.5)\n get_bot().sleep(attesa)\n\n# Descrivi questa funzione...\ndef Giravolte():\n global attesa, velocita_max, var_marciaIndietro, var_giravolte, var_ritirata, velocita, stanco\n get_prog_eng().check_end()\n get_bot().right(speed=min(max(100 * velocita, 1), velocita_max), elapse=2)\n get_bot().sleep(attesa)\n\n# Descrivi questa funzione...\ndef Patugliamento():\n global attesa, velocita_max, var_marciaIndietro, var_giravolte, var_ritirata, velocita, stanco\n get_prog_eng().check_end()\n for count3 in range(3):\n get_prog_eng().check_end()\n get_bot().right(speed=min(max(100 * velocita, 1), velocita_max), elapse=1)\n get_bot().sleep(0.5)\n\n# Descrivi questa funzione...\ndef Ritirata_e_fuga():\n global attesa, velocita_max, var_marciaIndietro, var_giravolte, var_ritirata, velocita, stanco\n get_prog_eng().check_end()\n get_bot().backward(speed=min(max(100 * velocita, 1), velocita_max), elapse=1)\n get_bot().sleep(attesa)\n get_bot().left(speed=min(max(100 * velocita, 1), velocita_max), elapse=3)\n get_bot().sleep(attesa)\n get_bot().forward(speed=min(max(100 * velocita, 1), velocita_max), elapse=1)\n get_bot().sleep(attesa)\n\n# Descrivi questa funzione...\ndef MarciaIndietro_e_Scarto():\n global attesa, velocita_max, var_marciaIndietro, var_giravolte, var_ritirata, velocita, stanco\n get_prog_eng().check_end()\n get_bot().backward(speed=min(max(100 * velocita, 1), velocita_max), elapse=1)\n get_bot().sleep(attesa)\n get_bot().right(speed=min(max(100 * velocita, 1), velocita_max), elapse=1)\n get_bot().sleep(attesa)\n\n\nattesa = 0.2\nvelocita_max = 80\nvar_marciaIndietro = 0\nvar_giravolte = 0\nvar_ritirata = 0\nstanco = 0\nvelocita = 1\nwhile True:\n get_prog_eng().check_end()\n stanco = (stanco if isinstance(stanco, Number) else 0) + 1\n var_marciaIndietro = (var_marciaIndietro if isinstance(var_marciaIndietro, Number) else 0) + 1\n var_giravolte = (var_giravolte if isinstance(var_giravolte, Number) else 0) + 1\n var_ritirata = (var_ritirata if isinstance(var_ritirata, Number) else 0) + 1\n if stanco == 5:\n stanco = 0\n velocita = 1\n elif stanco > 3:\n velocita = 0.5\n Scodinzola()\n Evita_Ostacoli()\n Patugliamento()\n if var_ritirata == 4:\n var_ritirata = 0\n Ritirata_e_fuga()\n if var_giravolte == 8:\n var_giravolte = 0\n Giravolte()\n get_bot().sleep(5)\n if var_marciaIndietro == 6:\n var_marciaIndietro = 0\n MarciaIndietro_e_Scarto()\n","default":""} \ No newline at end of file diff --git a/defaults/programs/program_demo_sound_clap_control.json b/defaults/programs/program_demo_sound_clap_control.json index 29f377f3..c42d2efd 100644 --- a/defaults/programs/program_demo_sound_clap_control.json +++ b/defaults/programs/program_demo_sound_clap_control.json @@ -1 +1 @@ -{"dom_code": "WHILETRUEnoise10000.2noiseEQnoiseFALSEFORWARD100-1RIGHT1000.5", "code": "noise = None\n\n\nwhile True:\n get_prog_eng().check_end()\n noise = get_audio().hear(level=1000, elapse=0.2)\n get_cam().set_text(noise)\n if noise == False:\n get_bot().forward(speed=100, elapse=-1)\n else:\n get_bot().right(speed=100, elapse=0.5)\nget_bot().stop()\n", "name": "clap_control"} \ No newline at end of file +{"id": "e4dada78-915e-4916-931a-e96f754812c4", "dom_code": "WHILETRUEnoise10000.2noiseEQnoiseFALSEFORWARD100-1RIGHT1000.5", "code": "noise = None\n\n\nwhile True:\n get_prog_eng().check_end()\n noise = get_audio().hear(level=1000, elapse=0.2)\n get_cam().set_text(noise)\n if noise == False:\n get_bot().forward(speed=100, elapse=-1)\n else:\n get_bot().right(speed=100, elapse=0.5)\nget_bot().stop()\n", "name": "demo_sound_clap_control"} \ No newline at end of file diff --git a/defaults/programs/program_test_cnn_classifier.json b/defaults/programs/program_test_cnn_classifier.json index e79a8329..ac8cbdc2 100644 --- a/defaults/programs/program_test_cnn_classifier.json +++ b/defaults/programs/program_test_cnn_classifier.json @@ -1 +1 @@ -{"name": "test_cnn_classifier", "dom_code": "WHILETRUEgeneric_fast_low", "code": "while True:\n get_prog_eng().check_end()\n get_cam().set_text(get_cam().cnn_classify(\"generic_fast_low\"))\n"} \ No newline at end of file +{"id": "4b19dbc7-55a8-4888-9e2e-e05a0e65d29e", "name": "test_cnn_classifier", "dom_code": "WHILETRUEgeneric_fast_low", "code": "while True:\n get_prog_eng().check_end()\n get_cam().set_text(get_cam().cnn_classify(\"generic_fast_low\"))\n"} \ No newline at end of file diff --git a/defaults/programs/program_test_cnn_object_detect.json b/defaults/programs/program_test_cnn_object_detect.json index 9723235d..f7e994bd 100644 --- a/defaults/programs/program_test_cnn_object_detect.json +++ b/defaults/programs/program_test_cnn_object_detect.json @@ -1 +1 @@ -{"name": "test_cnn_object_detect", "dom_code": "WHILETRUEgeneric_object_detect", "code": "while True:\n get_prog_eng().check_end()\n get_cam().set_text(get_cam().cnn_detect_objects(\"generic_object_detect\"))\n"} \ No newline at end of file +{"id": "9622b72c-c7d4-4643-9c76-3148f16572a7", "name": "test_cnn_object_detect", "dom_code": "WHILETRUEgeneric_object_detect", "code": "while True:\n get_prog_eng().check_end()\n get_cam().set_text(get_cam().cnn_detect_objects(\"generic_object_detect\"))\n"} \ No newline at end of file diff --git a/defaults/programs/program_test_find_code.json b/defaults/programs/program_test_find_code.json index 0f16e779..346a4b04 100644 --- a/defaults/programs/program_test_find_code.json +++ b/defaults/programs/program_test_find_code.json @@ -1 +1 @@ -{"dom_code": "WHILETRUE", "code": "while True:\n get_prog_eng().check_end()\n get_cam().set_text(get_cam().find_qr_code())\n", "name": "find_code_test"} \ No newline at end of file +{"id": "6cc16350-c47c-4e02-b442-be3fa7ce5942", "dom_code": "WHILETRUE", "code": "while True:\n get_prog_eng().check_end()\n get_cam().set_text(get_cam().find_qr_code())\n", "name": "test_find_code"} \ No newline at end of file diff --git a/defaults/programs/program_test_find_color.json b/defaults/programs/program_test_find_color.json index 68bc4586..9cd9e0ee 100644 --- a/defaults/programs/program_test_find_color.json +++ b/defaults/programs/program_test_find_color.json @@ -1 +1 @@ -{"dom_code": "WHILETRUEdistDIST#96b73cangleANGLE#96b73cDistance: dist angle: angle", "code": "dist = None\nangle = None\n\n\nwhile True:\n get_prog_eng().check_end()\n dist = get_cam().find_color('#96b73c')[0]\n angle = get_cam().find_color('#96b73c')[1]\n get_cam().set_text(''.join([str(temp_value) for temp_value in ['Distance: ', dist, ' angle: ', angle]]))\n", "name": "find_color"} \ No newline at end of file +{"id": "9fb15bbd-97a6-490f-89c9-67a1ae0f1598", "dom_code": "WHILETRUEdistDIST#96b73cangleANGLE#96b73cDistance: dist angle: angle", "code": "dist = None\nangle = None\n\n\nwhile True:\n get_prog_eng().check_end()\n dist = get_cam().find_color('#96b73c')[0]\n angle = get_cam().find_color('#96b73c')[1]\n get_cam().set_text(''.join([str(temp_value) for temp_value in ['Distance: ', dist, ' angle: ', angle]]))\n", "name": "test_find_color"} \ No newline at end of file diff --git a/defaults/programs/program_test_find_face.json b/defaults/programs/program_test_find_face.json index ace65032..44008f5f 100644 --- a/defaults/programs/program_test_find_face.json +++ b/defaults/programs/program_test_find_face.json @@ -1 +1 @@ -{"dom_code": "faceface_xface_sizeWHILETRUEfaceALLface_xGETFROM_STARTface1face_sizeGETFROM_STARTface2face: face_xface_xLTface_x-10LEFT800.1GTface_x10RIGHT800.1", "code": "face = None\nface_x = None\nface_size = None\n\n\nwhile True:\n get_prog_eng().check_end()\n face = get_cam().find_face()\n face_x = face[0]\n face_size = face[1]\n get_cam().set_text(str('face: ') + str(face_x))\n if face_x:\n if face_x < -10:\n get_bot().left(speed=80, elapse=0.1)\n elif face_x > 10:\n get_bot().right(speed=80, elapse=0.1)\n else:\n get_bot().stop()\n", "name": "face_find"} \ No newline at end of file +{"id": "504eb5f8-650f-4385-8cb4-d30ea09c9ef7", "dom_code": "faceface_xface_sizeWHILETRUEfaceALLface_xGETFROM_STARTface1face_sizeGETFROM_STARTface2face: face_xface_xLTface_x-10LEFT800.1GTface_x10RIGHT800.1", "code": "face = None\nface_x = None\nface_size = None\n\n\nwhile True:\n get_prog_eng().check_end()\n face = get_cam().find_face()\n face_x = face[0]\n face_size = face[1]\n get_cam().set_text(str('face: ') + str(face_x))\n if face_x:\n if face_x < -10:\n get_bot().left(speed=80, elapse=0.1)\n elif face_x > 10:\n get_bot().right(speed=80, elapse=0.1)\n else:\n get_bot().stop()\n", "name": "test_find_face"} \ No newline at end of file diff --git a/defaults/programs/program_test_find_path_ahead.json b/defaults/programs/program_test_find_path_ahead.json index c494a2d2..8875ed39 100644 --- a/defaults/programs/program_test_find_path_ahead.json +++ b/defaults/programs/program_test_find_path_ahead.json @@ -1 +1 @@ -{"dom_code": "spazio_liberoWHILETRUEspazio_liberospazio_liberoANDGTspazio_libero30NEQspazio_libero60FORWARD1000.2RIGHT1000.2", "code": "spazio_libero = None\n\n\nwhile True:\n get_prog_eng().check_end()\n spazio_libero = get_cam().path_ahead()\n get_cam().set_text(spazio_libero)\n if spazio_libero > 30 and spazio_libero != 60:\n get_bot().forward(speed=100, elapse=0.2)\n else:\n get_bot().right(speed=100, elapse=0.2)\n", "name": "path_ahead"} \ No newline at end of file +{"id": "ced77927-df6b-475a-b41e-d36f3976a2a0", "dom_code": "spazio_liberoWHILETRUEspazio_liberospazio_liberoANDGTspazio_libero30NEQspazio_libero60FORWARD1000.2RIGHT1000.2", "code": "spazio_libero = None\n\n\nwhile True:\n get_prog_eng().check_end()\n spazio_libero = get_cam().path_ahead()\n get_cam().set_text(spazio_libero)\n if spazio_libero > 30 and spazio_libero != 60:\n get_bot().forward(speed=100, elapse=0.2)\n else:\n get_bot().right(speed=100, elapse=0.2)\n", "name": "test_find_path_ahead"} \ No newline at end of file diff --git a/defaults/programs/program_test_img_average.json b/defaults/programs/program_test_img_average.json index 90bacb94..08e30d5b 100644 --- a/defaults/programs/program_test_img_average.json +++ b/defaults/programs/program_test_img_average.json @@ -1 +1 @@ -{"dom_code": "WHILETRUEV", "code": "while True:\n get_prog_eng().check_end()\n get_cam().set_text(get_cam().get_average()[2])\n", "name": "img_average_test"} \ No newline at end of file +{"id": "4c448688-2d4f-424c-ac95-85417f30a447", "dom_code": "WHILETRUEV", "code": "while True:\n get_prog_eng().check_end()\n get_cam().set_text(get_cam().get_average()[2])\n", "name": "test_img_average"} \ No newline at end of file diff --git a/defaults/programs/program_test_input.json b/defaults/programs/program_test_input.json index 781dc25a..15fa7f9f 100644 --- a/defaults/programs/program_test_input.json +++ b/defaults/programs/program_test_input.json @@ -1 +1 @@ -{"name": "test_input", "dom_code": "WHILETRUEanalog 1: 0 analog 2: 1 digital 1: 2", "code": "while True:\n get_prog_eng().check_end()\n get_cam().set_text(''.join([str(x) for x in ['analog 1: ', get_atmega().get_input(0), ' analog 2: ', get_atmega().get_input(1), ' digital 1: ', get_atmega().get_input(2)]]))\n"} \ No newline at end of file +{"id": "c4cec8dc-f0e9-4aa0-ba2d-e8eb368a0f88", "name": "test_input", "dom_code": "WHILETRUEanalog 1: 0 analog 2: 1 digital 1: 2", "code": "while True:\n get_prog_eng().check_end()\n get_cam().set_text(''.join([str(x) for x in ['analog 1: ', get_atmega().get_input(0), ' analog 2: ', get_atmega().get_input(1), ' digital 1: ', get_atmega().get_input(2)]]))\n"} \ No newline at end of file diff --git a/defaults/programs/program_test_led.json b/defaults/programs/program_test_led.json index 7246e80d..58f1534c 100644 --- a/defaults/programs/program_test_led.json +++ b/defaults/programs/program_test_led.json @@ -1 +1 @@ -{"name": "test_led", "dom_code": "ledsicleds6031leds000i1leds11MINUSi1000ADDi5leds000c11005iADDi5MULTIPLYc0MULTIPLYc0MULTIPLYc31leds000", "code": "leds = None\ni = None\nc = None\n\ndef upRange(start, stop, step):\n while start <= stop:\n yield start\n start += abs(step)\n\ndef downRange(start, stop, step):\n while start >= stop:\n yield start\n start -= abs(step)\n\n\nleds = 60\nfor count in range(3):\n get_prog_eng().check_end()\n get_atmega().set_led(1, leds, 0, 0, 0)\n for i in (1 <= float(leds)) and upRange(1, float(leds), 1) or downRange(1, float(leds), 1):\n get_prog_eng().check_end()\n get_atmega().set_led(1, i - 1, 0, 0, 0)\n get_atmega().set_led(i + 5, leds, 0, 0, 0)\n for c in range(1, 101, 5):\n get_prog_eng().check_end()\n get_atmega().set_led(i, i + 5, c * 0, c * 0, c * 3)\n get_atmega().set_led(1, leds, 0, 0, 0)\n"} \ No newline at end of file +{"id": "4bf5a8a8-eb06-44aa-8450-e73b62e11f8c", "name": "test_led", "dom_code": "ledsicleds6031leds000i1leds11MINUSi1000ADDi5leds000c11005iADDi5MULTIPLYc0MULTIPLYc0MULTIPLYc31leds000", "code": "leds = None\ni = None\nc = None\n\ndef upRange(start, stop, step):\n while start <= stop:\n yield start\n start += abs(step)\n\ndef downRange(start, stop, step):\n while start >= stop:\n yield start\n start -= abs(step)\n\n\nleds = 60\nfor count in range(3):\n get_prog_eng().check_end()\n get_atmega().set_led(1, leds, 0, 0, 0)\n for i in (1 <= float(leds)) and upRange(1, float(leds), 1) or downRange(1, float(leds), 1):\n get_prog_eng().check_end()\n get_atmega().set_led(1, i - 1, 0, 0, 0)\n get_atmega().set_led(i + 5, leds, 0, 0, 0)\n for c in range(1, 101, 5):\n get_prog_eng().check_end()\n get_atmega().set_led(i, i + 5, c * 0, c * 0, c * 3)\n get_atmega().set_led(1, leds, 0, 0, 0)\n"} \ No newline at end of file diff --git a/defaults/programs/program_test_music.json b/defaults/programs/program_test_music.json index a3d0f3fd..053a21a2 100644 --- a/defaults/programs/program_test_music.json +++ b/defaults/programs/program_test_music.json @@ -1 +1 @@ -{"name": "test_music", "dom_code": "C2nonedog1D2nonecat1E2nonepig1F2noneelephant1G2nonesnake1A2noneduck1B2nonecat1", "code": "get_music().play_note(note=\"C2\", alteration=\"none\" ,instrument=\"dog\" ,duration=1)\nget_music().play_note(note=\"D2\", alteration=\"none\" ,instrument=\"cat\" ,duration=1)\nget_music().play_note(note=\"E2\", alteration=\"none\" ,instrument=\"pig\" ,duration=1)\nget_music().play_note(note=\"F2\", alteration=\"none\" ,instrument=\"elephant\" ,duration=1)\nget_music().play_note(note=\"G2\", alteration=\"none\" ,instrument=\"snake\" ,duration=1)\nget_music().play_note(note=\"A2\", alteration=\"none\" ,instrument=\"duck\" ,duration=1)\nget_music().play_note(note=\"B2\", alteration=\"none\" ,instrument=\"cat\" ,duration=1)\n"} \ No newline at end of file +{"id": "8281a5f5-f9a4-4128-bd3d-bef41fb2c30b", "name": "test_music", "dom_code": "C2nonedog1D2nonecat1E2nonepig1F2noneelephant1G2nonesnake1A2noneduck1B2nonecat1", "code": "get_music().play_note(note=\"C2\", alteration=\"none\" ,instrument=\"dog\" ,duration=1)\nget_music().play_note(note=\"D2\", alteration=\"none\" ,instrument=\"cat\" ,duration=1)\nget_music().play_note(note=\"E2\", alteration=\"none\" ,instrument=\"pig\" ,duration=1)\nget_music().play_note(note=\"F2\", alteration=\"none\" ,instrument=\"elephant\" ,duration=1)\nget_music().play_note(note=\"G2\", alteration=\"none\" ,instrument=\"snake\" ,duration=1)\nget_music().play_note(note=\"A2\", alteration=\"none\" ,instrument=\"duck\" ,duration=1)\nget_music().play_note(note=\"B2\", alteration=\"none\" ,instrument=\"cat\" ,duration=1)\n"} \ No newline at end of file diff --git a/defaults/programs/program_test_output.json b/defaults/programs/program_test_output.json index dbd31e04..1fcb8a4a 100644 --- a/defaults/programs/program_test_output.json +++ b/defaults/programs/program_test_output.json @@ -1 +1 @@ -{"name": "test_output", "dom_code": "WHILETRUE0TRUE0.11TRUE0.12TRUE0.10FALSE0.11FALSE0.12FALSE0.1", "code": "while True:\n get_prog_eng().check_end()\n get_atmega().set_output(0, True)\n get_bot().sleep(0.1)\n get_atmega().set_output(1, True)\n get_bot().sleep(0.1)\n get_atmega().set_output(2, True)\n get_bot().sleep(0.1)\n get_atmega().set_output(0, False)\n get_bot().sleep(0.1)\n get_atmega().set_output(1, False)\n get_bot().sleep(0.1)\n get_atmega().set_output(2, False)\n get_bot().sleep(0.1)\n"} \ No newline at end of file +{"id": "604ca7a1-f2e2-45f0-92b2-afd3c621921f", "name": "test_output", "dom_code": "WHILETRUE0TRUE0.11TRUE0.12TRUE0.10FALSE0.11FALSE0.12FALSE0.1", "code": "while True:\n get_prog_eng().check_end()\n get_atmega().set_output(0, True)\n get_bot().sleep(0.1)\n get_atmega().set_output(1, True)\n get_bot().sleep(0.1)\n get_atmega().set_output(2, True)\n get_bot().sleep(0.1)\n get_atmega().set_output(0, False)\n get_bot().sleep(0.1)\n get_atmega().set_output(1, False)\n get_bot().sleep(0.1)\n get_atmega().set_output(2, False)\n get_bot().sleep(0.1)\n"} \ No newline at end of file diff --git a/defaults/programs/program_test_sonars.json b/defaults/programs/program_test_sonars.json index 6664424a..7c58bec3 100644 --- a/defaults/programs/program_test_sonars.json +++ b/defaults/programs/program_test_sonars.json @@ -1 +1 @@ -{"name": "test_sonars", "dom_code": "WHILETRUEFront: 0 Right: 1 Left: 2", "code": "while True:\n get_prog_eng().check_end()\n get_cam().set_text(''.join([str(x) for x in ['Front: ', get_bot().get_sonar_distance(0), ' Right: ', get_bot().get_sonar_distance(1), ' Left: ', get_bot().get_sonar_distance(2)]]))\n"} \ No newline at end of file +{"id": "ddab8e33-eba7-4f02-93af-823c523d63de", "name": "test_sonars", "dom_code": "WHILETRUEFront: 0 Right: 1 Left: 2", "code": "while True:\n get_prog_eng().check_end()\n get_cam().set_text(''.join([str(x) for x in ['Front: ', get_bot().get_sonar_distance(0), ' Right: ', get_bot().get_sonar_distance(1), ' Left: ', get_bot().get_sonar_distance(2)]]))\n"} \ No newline at end of file diff --git a/defaults/programs/program_test_sound_hear.json b/defaults/programs/program_test_sound_hear.json index 31ff1247..e08fe560 100644 --- a/defaults/programs/program_test_sound_hear.json +++ b/defaults/programs/program_test_sound_hear.json @@ -1 +1 @@ -{"dom_code": "WHILETRUE1001.0", "code": "while True:\n get_prog_eng().check_end()\n get_cam().set_text(get_audio().hear(level=100, elapse=1))\n", "name": "hear_test"} \ No newline at end of file +{"id": "458186a6-1285-4da2-b447-5db266b89c45", "dom_code": "WHILETRUE1001.0", "code": "while True:\n get_prog_eng().check_end()\n get_cam().set_text(get_audio().hear(level=100, elapse=1))\n", "name": "test_sound_hear"} \ No newline at end of file diff --git a/defaults/programs/program_test_sound_rec.json b/defaults/programs/program_test_sound_rec.json index de267400..9efd1c1f 100644 --- a/defaults/programs/program_test_sound_rec.json +++ b/defaults/programs/program_test_sound_rec.json @@ -1 +1 @@ -{"dom_code": "test01.wav5", "code": "get_audio().record_to_file(filename='test01.wav', elapse=5)\n", "name": "sound_rec_test"} \ No newline at end of file +{"id": "522b2754-65ba-4fa7-8264-e15e819c587b", "dom_code": "test01.wav5", "code": "get_audio().record_to_file(filename='test01.wav', elapse=5)\n", "name": "test_sound_rec"} \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index 6b88fce8..7f305ebe 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM coderbot/rpi-debian:bullseye-20220923 +FROM coderbot/rpi-debian:bullseye-20230530 ENV QEMU_CPU=max ENV DEBIAN_FRONTEND=noninteractive @@ -19,7 +19,8 @@ RUN install_packages \ libatlas-base-dev \ libhdf5-dev \ alsa-utils \ - espeak + espeak \ + git RUN install_packages \ libharfbuzz-bin \ libwebp6 \ diff --git a/docker/stub/Dockerfile b/docker/stub/Dockerfile index aaf42d81..2cd2fd2d 100644 --- a/docker/stub/Dockerfile +++ b/docker/stub/Dockerfile @@ -18,6 +18,7 @@ RUN apt-get update -y && apt-get install -y \ libhdf5-dev \ alsa-utils \ espeak \ + git \ libharfbuzz-bin \ libwebp6 \ libilmbase25 \ @@ -61,5 +62,6 @@ ADD docker/stub/start.sh /coderbot/. ARG CODERBOT_VERSION ENV CODERBOT_VERSION=${CODERBOT_VERSION} +ENV CODERBOT_CLOUD_API_ENDPOINT=http://localhost:5000 ENTRYPOINT /coderbot/start.sh diff --git a/docker/stub/requirements.txt b/docker/stub/requirements.txt index 81c1bc21..662a4dc3 100644 --- a/docker/stub/requirements.txt +++ b/docker/stub/requirements.txt @@ -1,26 +1,24 @@ # API framework -connexion==2.14.2 -Flask==2.2.3 -Flask-Cors==3.0.10 -tinydb==4.7.1 -Werkzeug==2.2.3 +connexion[uvicorn,flask,swagger-ui]==3.0.5 +tinydb==4.8.0 +cloud_api_robot_client @ git+https://github.com/CoderBotOrg/cloud_api_robot_client.git # Misc utils -setuptools==67.4.0 +setuptools==69.0.3 event-channel==0.4.3 # IO extensions -spidev==3.5 +spidev==3.6 # Audio sox==1.4.1 # Computer Vision -grpcio==1.48.1 -numpy==1.24.2 -Pillow==9.4.0 -protobuf==4.22.0 +grpcio==1.60.0 +numpy==1.24.3 +Pillow==10.1.0 +protobuf==4.25.1 opencv-contrib-python==4.5.5.62 -tflite-runtime==2.11.0 +tflite-runtime==2.12.0 pytesseract==0.3.10 pyzbar==0.1.9 diff --git a/requirements.txt b/requirements.txt index 35aad39d..fe28f3bf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,31 +1,29 @@ # API framework -connexion==2.14.2 -Flask==2.2.3 -Flask-Cors==3.0.10 -tinydb==4.7.1 -Werkzeug==2.2.3 +connexion[uvicorn,flask,swagger-ui]==3.0.5 +tinydb==4.8.0 +cloud_api_robot_client @ git+https://github.com/CoderBotOrg/cloud_api_robot_client.git # Misc utils -setuptools==67.4.0 +setuptools==69.0.3 event-channel==0.4.3 # IO extensions pigpio==1.78 -smbus2==0.4.2 -spidev==3.5 +smbus2==0.4.3 +spidev==3.6 # Audio sox==1.4.1 -PyAudio==0.2.12 -pyalsaaudio==0.9.2 +PyAudio==0.2.14 +pyalsaaudio==0.10.0 # Computer Vision -grpcio==1.48.1 -numpy==1.24.2 -Pillow==9.4.0 -protobuf==4.22.0 +grpcio==1.60.0 +numpy==1.24.3 +Pillow==10.1.0 +protobuf==4.25.1 opencv-contrib-python==4.5.5.62 -tflite-runtime==2.11.0 +tflite-runtime==2.13.0 pytesseract==0.3.10 picamera==1.13 pyzbar==0.1.9 diff --git a/test/camera_test.py b/test/camera_test.py index 523f2c4b..b074aff4 100755 --- a/test/camera_test.py +++ b/test/camera_test.py @@ -8,9 +8,9 @@ class CameraTest(unittest.TestCase): def setUp(self): - config.Config.read() + settings = config.Config.read().get('settings') picamera.PiCamera = picamera_mock.PiCameraMock - self.cam = camera.Camera.get_instance() + self.cam = camera.Camera.get_instance(settings) def tearDown(self): self.cam.exit() diff --git a/test/coderbot_test.py b/test/coderbot_test.py index dbbd6ee0..d8a651f2 100755 --- a/test/coderbot_test.py +++ b/test/coderbot_test.py @@ -1,6 +1,7 @@ import unittest import test.pigpio_mock import coderbot +import config import logging logger = logging.getLogger() @@ -17,7 +18,8 @@ class CoderBotDCMotorTestCase(unittest.TestCase): def setUp(self): coderbot.pigpio.pi = test.pigpio_mock.PIGPIOMock coderbot.CoderBot._instance = None - self.bot = coderbot.CoderBot.get_instance() + settings = config.Config.read().get('settings') + self.bot = coderbot.CoderBot.get_instance(settings) def test_motor_forward(self): self.bot.forward(speed=100, elapse=0.1) @@ -43,34 +45,34 @@ class CoderBotServoMotorTestCase(unittest.TestCase): def setUp(self): coderbot.pigpio.pi = test.pigpio_mock.PIGPIOMock coderbot.CoderBot._instance = None - self.bot = coderbot.CoderBot.get_instance(servo=True) + settings = config.Config.read().get('settings') + self.bot = coderbot.CoderBot.get_instance(settings) - def test_motor_forward(self): - self.bot.forward(speed=100, elapse=0.1) + def servo_0_0(self): + self.bot.forward(servo=0, angle=0) - def test_motor_backward(self): - self.bot.backward(speed=100, elapse=0.1) + def servo_0_90(self): + self.bot.forward(servo=0, angle=90) - def test_motor_left(self): - self.bot.left(speed=100, elapse=0.1) + def servo_0_180(self): + self.bot.forward(servo=0, angle=180) - def test_motor_right(self): - self.bot.left(speed=100, elapse=0.1) + def servo_1_0(self): + self.bot.forward(servo=1, angle=0) - def test_motor_move(self): - self.bot.move(speed=100, elapse=0.1) - self.bot.move(speed=-100, elapse=0.1) + def servo_1_90(self): + self.bot.forward(servo=1, angle=90) - def test_motor_turn(self): - self.bot.turn(speed=100, elapse=0.1) - self.bot.turn(speed=-100, elapse=0.1) + def servo_1_180(self): + self.bot.forward(servo=1, angle=180) class CoderBotSonarTestCase(unittest.TestCase): def setUp(self): coderbot.pigpio.pi = test.pigpio_mock.PIGPIOMock coderbot.CoderBot._instance = None - self.bot = coderbot.CoderBot.get_instance() + settings = config.Config.read().get('settings') + self.bot = coderbot.CoderBot.get_instance(settings) def test_sonar(self): for i in range(0, 3):