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): pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy