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):