def settings_from_params( type_def: type[Generic], settings: list[Parameter], overrides: Optional[list[Parameter]] = None) -> Settings: """Constructs instance of Settings from two arrays of parameters (scene settings and project overrides). :param type_def: :param settings: :param overrides: :return: """ if overrides is None: overrides = [] final: dict[str, Parameter] = {s.name: s for s in settings} for over in overrides: if over.name not in final: raise Arcor2Exception("Invalid override.") if over.type != final[over.name].type: raise Arcor2Exception("Type mismatch.") final[over.name] = over settings_def = get_settings_def(type_def) settings_data: dict[str, Any] = {} settings_def_type_hints = get_type_hints(settings_def.__init__) for s in final.values(): try: setting_def = settings_def_type_hints[s.name] except KeyError as e: raise Arcor2Exception(f"Unknown property {s.name}.") from e try: if issubclass(setting_def, JsonSchemaMixin): settings_data[s.name] = json.loads(s.value) else: settings_data[s.name] = setting_def(json.loads(s.value)) except (json.JsonException, ValidationError) as e: raise Arcor2Exception( f"Parameter {s.name} has invalid value.") from e try: settings_cls = settings_def.from_dict(settings_data) except (ValueError, ValidationError) as e: raise Arcor2Exception("Validation of settings failed.") from e return settings_cls
def _call_rpc(self, req: rpc.common.RPC.Request, resp_type: Type[RR]) -> RR: self._ws.send(req.to_json()) # wait for RPC response, put any incoming event into the queue while True: try: recv_dict = json.loads(self._ws.recv()) except websocket.WebSocketTimeoutException: raise ARServerClientException("RPC timeouted.") if not isinstance(recv_dict, dict): self._logger.debug(f"Invalid data received: {recv_dict}") continue if "response" in recv_dict: break elif "event" in recv_dict: self._event_queue.put(self.event_mapping[ recv_dict["event"]].from_dict(recv_dict)) try: resp = resp_type.from_dict(recv_dict) except ValidationError as e: self._logger.error( f"Request: {req.to_dict()}, response: {recv_dict}.") raise ARServerClientException( "RPC response validation failed.") from e assert req.id == resp.id assert req.request == resp.response return resp
def get_event( self, drop_everything_until: Optional[Type[events.Event]] = None ) -> events.Event: """Returns queued events (if any) or wait until some event arrives. :param drop_everything_until: Drop any event until there is one of required type. :return: """ try: evt = self._event_queue.get_nowait() except Empty: try: recv_dict = json.loads(self._ws.recv()) except websocket.WebSocketTimeoutException: raise ARServerClientException("Timeouted.") if not isinstance(recv_dict, dict): raise ARServerClientException( f"Invalid data received: {recv_dict}") if "event" not in recv_dict: raise ARServerClientException( f"Expected event, got: {recv_dict}") evt = self.event_mapping[recv_dict["event"]].from_dict(recv_dict) if drop_everything_until and not isinstance(evt, drop_everything_until): return self.get_event(drop_everything_until) return evt
def _handle_response(resp: requests.Response) -> None: """Raises exception if there is something wrong with the response. :param resp: :return: """ if resp.status_code < 400: return decoded_content = resp.content.decode() # here we try to handle different cases try: cont = json.loads(decoded_content) except json.JsonException: # response contains invalid JSON raise RestHttpException(decoded_content, error_code=resp.status_code) if isinstance(cont, str): # just plain text raise RestHttpException(cont, error_code=resp.status_code) elif isinstance(cont, dict): # this could be WebApiError try: err = WebApiError.from_dict(cont) except ValidationError: raise RestHttpException(str(cont), error_code=resp.status_code) raise WebApiException(err.message, error_code=resp.status_code, web_api_error=err)
def str_from_value(self) -> str: val = json.loads(self.value) if not isinstance(val, str): raise Arcor2Exception("Value should be string.") return val
def check_parameter(parameter: Parameter) -> None: # TODO check using (some) plugin from arcor2 import json val = json.loads(parameter.value) # however, analysis in get_dataclass_params() can handle also (nested) dataclasses, etc. if not isinstance(val, (int, float, str, bool)): raise Arcor2Exception("Only basic types are supported so far.")
def _param_value_list(cls, param: ActionParameter) -> List[str]: lst = json.loads(param.value) if not isinstance(lst, list): raise ParameterPluginException( "Parameter value should be list of references to action points." ) return lst
def program_src(type_defs: TypesDict, project: CProject, scene: CScene, add_logic: bool = True) -> str: tree = empty_script_tree(project.id, add_main_loop=add_logic) # get object instances from resources object main = find_function("main", tree) last_assign = find_last_assign(main) for obj in scene.objects: add_import(tree, "object_types." + humps.depascalize(obj.type), obj.type, try_to_import=False) last_assign += 1 main.body.insert(last_assign, object_instance_from_res(obj.name, obj.id, obj.type)) # TODO temporary solution - should be (probably) handled by plugin(s) from arcor2 import json # TODO should we put there even unused parameters? for param in project.parameters: val = json.loads(param.value) aval: Optional[expr] = None if isinstance(val, bool): # subclass of int aval = NameConstant(value=val, kind=None) elif isinstance(val, (int, float)): aval = Num(n=val, kind=None) elif isinstance(val, str): aval = Str(s=val, kind="") if not aval: raise Arcor2Exception( f"Unsupported project parameter type ({param.type}) or value ({val})." ) last_assign += 1 main.body.insert( last_assign, Assign( # TODO use rather AnnAssign? targets=[Name(id=param.name, ctx=Store())], value=aval, type_comment=None), ) if add_logic: add_logic_to_loop(type_defs, tree, scene, project) return SCRIPT_HEADER + tree_to_str(tree)
def ws_thread() -> None: # TODO use (refactored) arserver client global package_info global package_state global action_state_before global action_state_after assert ws event_mapping: dict[str, type[events.Event]] = { evt.__name__: evt for evt in EVENTS } rpc_mapping: dict[str, type[arcor2_rpc.common.RPC]] = { rpc.__name__: rpc for rpc in EXPOSED_RPCS } while True: data = json.loads( ws.recv()) # TODO handle WebSocketConnectionClosedException if not isinstance(data, dict): continue if "event" in data: evt = event_mapping[data["event"]].from_dict(data) if isinstance(evt, PackageInfo): package_info = evt.data elif isinstance(evt, PackageState): package_state = evt.data if package_state.state == PackageState.Data.StateEnum.RUNNING: exception_messages.clear() elif isinstance(evt, ProjectException): exception_messages.append(evt.data.message) elif isinstance(evt, ActionStateBefore): # assume ActionStateBefore event to be fired always before ActionStateAfter # thus ActionStateBefore here belong to previous action - unset it action_state_after = None action_state_before = evt.data elif isinstance(evt, ActionStateAfter): action_state_after = evt.data elif "response" in data: resp = rpc_mapping[data["response"]].Response.from_dict(data) rpc_responses[resp.id].put(resp)
def _handle_response(resp: requests.Response) -> None: """Raises exception if there is something wrong with the response. :param resp: :return: """ if resp.status_code >= 400: decoded_content = resp.content.decode() # here we try to handle different cases try: raise RestHttpException(str(json.loads(decoded_content)), error_code=resp.status_code) except json.JsonException: # response contains invalid JSON raise RestHttpException(decoded_content, error_code=resp.status_code)
def parameter_value(cls, type_defs: TypesDict, scene: CScene, project: CProject, action_id: str, parameter_id: str) -> Enum: action = project.action(action_id) param = action.parameter(parameter_id) obj_id, action_type = action.parse_type() obj_type_name = scene.object(obj_id).type try: obj_type = type_defs[obj_type_name] except KeyError: raise ParameterPluginException( f"Unknown object type {obj_type_name}.") try: method = getattr(obj_type, action_type) except AttributeError: raise ParameterPluginException( f"Object type {obj_type_name} does not have method {action_type}." ) try: ttype = get_type_hints(method)[param.name] except KeyError: raise ParameterPluginException( f"Method {obj_type}/{method.__name__} does not have parameter {param.name}." ) if not issubclass(ttype, cls.type()): raise ParameterPluginException( f"Type {ttype.__name__} is not subclass of {cls.type().__name__}." ) try: return ttype(json.loads(param.value)) except (ValueError, json.JsonException): raise ParameterPluginException( f"Parameter {parameter_id} of action {action.name} has invalid value." )
def read_dc_from_zip(zip_file: zipfile.ZipFile, file_name: str, cls: Type[T]) -> T: return cls.from_dict( humps.decamelize(json.loads(read_str_from_zip(zip_file, file_name))))
def test_project_const(start_processes: None, ars: ARServer) -> None: event(ars, events.c.ShowMainScreen) assert ars.call_rpc( rpc.s.NewScene.Request(uid(), rpc.s.NewScene.Request.Args("Test scene")), rpc.s.NewScene.Response).result scene_data = event(ars, events.s.OpenScene).data assert scene_data scene = scene_data.scene event(ars, events.s.SceneState) assert ars.call_rpc( rpc.s.AddObjectToScene.Request( uid(), rpc.s.AddObjectToScene.Request.Args("random_actions", RandomActions.__name__)), rpc.s.AddObjectToScene.Response, ).result obj = event(ars, events.s.SceneObjectChanged).data assert obj # ------------------------------------------------------------------------------------------------------------------ assert ars.call_rpc( rpc.p.NewProject.Request( uid(), rpc.p.NewProject.Request.Args(scene.id, "Project name")), rpc.p.NewProject.Response, ).result open_project = event(ars, events.p.OpenProject).data assert open_project proj = open_project.project # test project parameters added by the arserver d: Dict[str, common.ProjectParameter] = { par.name: par for par in proj.parameters } assert len(d) == 2 assert d["scene_id"].type == "string" assert json.loads(d["scene_id"].value) == scene.id assert d["project_id"].type == "string" assert json.loads(d["project_id"].value) == proj.id event(ars, events.s.SceneState) assert ars.call_rpc( rpc.p.AddProjectParameter.Request( uid(), rpc.p.AddProjectParameter.Request.Args("min_time", "double", json.dumps(0.45))), rpc.p.AddProjectParameter.Response, ).result c1 = event(ars, events.p.ProjectParameterChanged).data assert c1 assert not ars.call_rpc( rpc.p.AddProjectParameter.Request( uid(), rpc.p.AddProjectParameter.Request.Args("min_time", "double", json.dumps(0.62))), rpc.p.AddProjectParameter.Response, ).result assert not ars.call_rpc( # attempt to update without lock rpc.p.UpdateProjectParameter.Request( uid(), rpc.p.UpdateProjectParameter.Request.Args( c1.id, name="min_time_updated")), rpc.p.UpdateProjectParameter.Response, ).result # ------------------------------------------------------------------------------------------------------------------ # the user opens a menu and then closes it without actually changing anything lock_object(ars, c1.id) assert ars.call_rpc( rpc.p.UpdateProjectParameter.Request( uid(), rpc.p.UpdateProjectParameter.Request.Args(c1.id, name="min_time_1"), dry_run=True), rpc.p.UpdateProjectParameter.Response, ).result assert ars.call_rpc( rpc.p.UpdateProjectParameter.Request( uid(), rpc.p.UpdateProjectParameter.Request.Args(c1.id, name="min_time_2"), dry_run=True), rpc.p.UpdateProjectParameter.Response, ).result unlock_object(ars, c1.id) # ------------------------------------------------------------------------------------------------------------------ lock_object(ars, c1.id) assert ars.call_rpc( rpc.p.UpdateProjectParameter.Request( uid(), rpc.p.UpdateProjectParameter.Request.Args( c1.id, name="min_time_updated")), rpc.p.UpdateProjectParameter.Response, ).result c1u = event(ars, events.p.ProjectParameterChanged).data assert c1u event(ars, events.lk.ObjectsUnlocked) assert c1u.id == c1.id assert c1.name != c1u.name assert c1.type == c1u.type # ------------------------------------------------------------------------------------------------------------------ # try to add and remove assert ars.call_rpc( rpc.p.AddProjectParameter.Request( uid(), rpc.p.AddProjectParameter.Request.Args("min_time_2", "double", json.dumps(0.62))), rpc.p.AddProjectParameter.Response, ).result c2 = event(ars, events.p.ProjectParameterChanged).data assert c2 assert ars.call_rpc( rpc.p.RemoveProjectParameter.Request( uid(), rpc.p.RemoveProjectParameter.Request.Args(c2.id)), rpc.p.RemoveProjectParameter.Response, ).result c2e = event(ars, events.p.ProjectParameterChanged) assert c2e.data assert c2e.data.id == c2.id assert c2e.change_type == c2e.Type.REMOVE # ------------------------------------------------------------------------------------------------------------------ # attempt to add a constant with duplicate name assert not ars.call_rpc( rpc.p.AddProjectParameter.Request( uid(), rpc.p.AddProjectParameter.Request.Args(c1u.name, "double", json.dumps(0.62))), rpc.p.AddProjectParameter.Response, ).result # ------------------------------------------------------------------------------------------------------------------ assert ars.call_rpc( rpc.p.AddActionPoint.Request( uid(), rpc.p.AddActionPoint.Request.Args("ap1", common.Position())), rpc.p.AddActionPoint.Response, ).result ap = event(ars, events.p.ActionPointChanged).data assert ap is not None assert ars.call_rpc( rpc.p.AddAction.Request( uid(), rpc.p.AddAction.Request.Args( ap.id, "test_action", f"{obj.id}/{RandomActions.random_double.__name__}", [ common.ActionParameter( "range_min", common.ActionParameter.TypeEnum.PROJECT_PARAMETER, json.dumps(c1.id)), common.ActionParameter("range_max", "double", "0.55"), ], [common.Flow(outputs=["random_value"])], ), ), rpc.p.AddAction.Response, ).result action = event(ars, events.p.ActionChanged).data assert action assert not ars.call_rpc( rpc.p.RemoveProjectParameter.Request( uid(), rpc.p.RemoveProjectParameter.Request.Args(c1.id)), rpc.p.RemoveProjectParameter.Response, ).result # ------------------------------------------------------------------------------------------------------------------ # try to execute action using constant parameter assert ars.call_rpc((rpc.s.StartScene.Request(uid())), rpc.s.StartScene.Response).result assert event(ars, events.s.SceneState ).data.state == events.s.SceneState.Data.StateEnum.Starting assert event(ars, events.s.SceneState ).data.state == events.s.SceneState.Data.StateEnum.Started assert ars.call_rpc( rpc.p.ExecuteAction.Request( uid(), rpc.p.ExecuteAction.Request.Args(action.id)), rpc.p.ExecuteAction.Response) event(ars, events.a.ActionExecution) res = event(ars, events.a.ActionResult) assert res.data assert res.data.action_id == action.id assert not res.data.error assert ars.call_rpc((rpc.s.StopScene.Request(uid())), rpc.s.StopScene.Response).result assert event(ars, events.s.SceneState ).data.state == events.s.SceneState.Data.StateEnum.Stopping assert event(ars, events.s.SceneState ).data.state == events.s.SceneState.Data.StateEnum.Stopped assert ars.call_rpc( rpc.p.RemoveAction.Request(uid(), rpc.p.IdArgs(action.id)), rpc.p.RemoveAction.Response).result assert event(ars, events.p.ActionChanged).data assert ars.call_rpc( rpc.p.RemoveProjectParameter.Request( uid(), rpc.p.RemoveProjectParameter.Request.Args(c1.id)), rpc.p.RemoveProjectParameter.Response, ).result event(ars, events.p.ProjectParameterChanged)
def check_logic_item( obj_types: ObjectTypeDict, scene: CachedScene, parent: LogicContainer, logic_item: LogicItem ) -> None: """Checks if newly added/updated ProjectLogicItem is ok. :param parent: :param logic_item: :return: """ action_ids = parent.action_ids() if logic_item.start == LogicItem.START and logic_item.end == LogicItem.END: raise Arcor2Exception("This does not make sense.") if logic_item.start != LogicItem.START: start_action_id, start_flow = logic_item.parse_start() if start_action_id == logic_item.end: raise Arcor2Exception("Start and end can't be the same.") if start_action_id not in action_ids: raise Arcor2Exception("Logic item has unknown start.") if start_flow != "default": # TODO enum raise Arcor2Exception("Only flow 'default' is supported so far.'") if logic_item.end != LogicItem.END: if logic_item.end not in action_ids: raise Arcor2Exception("Logic item has unknown end.") if logic_item.condition is not None: what = logic_item.condition.parse_what() action = parent.action(what.action_id) # action that produced the result which we depend on here flow = action.flow(what.flow_name) try: flow.outputs[what.output_index] except IndexError: raise Arcor2Exception(f"Flow {flow.type} does not have output with index {what.output_index}.") action_meta = find_object_action(obj_types, scene, action) try: return_type = action_meta.returns[what.output_index] except IndexError: raise Arcor2Exception(f"Invalid output index {what.output_index} for action {action_meta.name}.") return_type_plugin = plugin_from_type_name(return_type) if not return_type_plugin.COUNTABLE: raise Arcor2Exception(f"Output of type {return_type} can't be branched.") # TODO for now there is only support for bool if return_type_plugin.type() != bool: raise Arcor2Exception("Unsupported condition type.") # check that condition value is ok, actual value is not interesting # TODO perform this check using plugin from arcor2 import json if not isinstance(json.loads(logic_item.condition.value), bool): raise Arcor2Exception("Invalid condition value.") for existing_item in parent.logic: if existing_item.id == logic_item.id: # item is updated continue if logic_item.start == logic_item.START and existing_item.start == logic_item.START: raise Arcor2Exception("START already defined.") if logic_item.start == existing_item.start: if None in (logic_item.condition, existing_item.condition): raise Arcor2Exception("Two junctions has the same start action without condition.") # when there are more logical connections from A to B, their condition values must be different if logic_item.condition == existing_item.condition: raise Arcor2Exception("Two junctions with the same start should have different conditions.") if logic_item.end == existing_item.end: if logic_item.start == existing_item.start: raise Arcor2Exception("Junctions can't have the same start and end.")
def _value_from_json(cls, value: str) -> Any: try: return json.loads(value) except json.JsonException as e: raise ParameterPluginException(f"Invalid value '{value}'.") from e
async def read_proc_stdout() -> None: global PACKAGE_STATE_EVENT global ACTION_EVENT global ACTION_ARGS_EVENT global PACKAGE_INFO_EVENT global RUNNING_PACKAGE_ID logger.info("Reading script stdout...") assert PROCESS is not None assert PROCESS.stdout is not None assert RUNNING_PACKAGE_ID is not None await package_state(PackageState(PackageState.Data(PackageState.Data.StateEnum.RUNNING, RUNNING_PACKAGE_ID))) printed_out: List[str] = [] while process_running(): try: stdout = await PROCESS.stdout.readuntil() except asyncio.exceptions.IncompleteReadError: break decoded = stdout.decode("utf-8") stripped = decoded.strip() try: data = json.loads(stripped) except json.JsonException: printed_out.append(decoded) logger.error(decoded.strip()) continue if not isinstance(data, dict) or "event" not in data: logger.error("Strange data from script: {}".format(data)) continue try: evt = EVENT_MAPPING[data["event"]].from_dict(data) except ValidationError as e: logger.error("Invalid event: {}, error: {}".format(data, e)) continue if isinstance(evt, PackageState): evt.data.package_id = RUNNING_PACKAGE_ID await package_state(evt) continue elif isinstance(evt, PackageInfo): PACKAGE_INFO_EVENT = evt await send_to_clients(evt) PACKAGE_INFO_EVENT = None if PROCESS.returncode: if printed_out: # TODO remember this (until another package is started) and send it to new clients? last_line = printed_out[-1].strip() try: exception_type, message = last_line.split(":", 1) except ValueError: exception_type, message = "Unknown", last_line await send_to_clients(ProjectException(ProjectException.Data(message, exception_type))) with open("traceback-{}.txt".format(time.strftime("%Y%m%d-%H%M%S")), "w") as tb_file: tb_file.write("".join(printed_out)) else: logger.warn( f"Process ended with non-zero return code ({PROCESS.returncode}), but didn't printed out anything." ) await package_state(PackageState(PackageState.Data(PackageState.Data.StateEnum.STOPPED, RUNNING_PACKAGE_ID))) logger.info(f"Process finished with returncode {PROCESS.returncode}.") RUNNING_PACKAGE_ID = None
def _add_logic(container: Container, current_action: Action, super_container: Optional[Container] = None) -> None: # more paths could lead to the same action, so it might be already added # ...this is easier than searching the tree if current_action.id in added_actions: logger.debug( f"Action {current_action.name} already added, skipping.") return inputs, outputs = project.action_io(current_action.id) logger.debug( f"Adding action {current_action.name}, with {len(inputs)} input(s) and {len(outputs)} output(s)." ) act = current_action.parse_type() ac_obj = scene.object(act.obj_id).name args: List[AST] = [] # TODO make sure that the order of parameters is correct / re-order for param in current_action.parameters: if param.type == ActionParameter.TypeEnum.LINK: parsed_link = param.parse_link() parent_action = project.action(parsed_link.action_id) # TODO add support for tuples assert len(parent_action.flow(FlowTypes.DEFAULT).outputs ) == 1, "Only one result is supported atm." assert parsed_link.output_index == 0 res_name = parent_action.flow(FlowTypes.DEFAULT).outputs[0] # make sure that the result already exists if parent_action.id not in added_actions: raise SourceException( f"Action {current_action.name} attempts to use result {res_name} " f"of subsequent action {parent_action.name}.") args.append(Name(id=res_name, ctx=Load())) elif param.type == ActionParameter.TypeEnum.PROJECT_PARAMETER: args.append( Name(id=project.parameter(param.str_from_value()).name, ctx=Load())) else: plugin = plugin_from_type_name(param.type) args.append( plugin.parameter_ast(type_defs, scene, project, current_action.id, param.name)) list_of_imp_tup = plugin.need_to_be_imported( type_defs, scene, project, current_action.id, param.name) if list_of_imp_tup: # TODO what if there are two same names? for imp_tup in list_of_imp_tup: add_import(tree, imp_tup.module_name, imp_tup.class_name, try_to_import=False) add_method_call( container.body, ac_obj, act.action_type, args, [keyword(arg="an", value=Str(s=current_action.name, kind=""))], current_action.flow(FlowTypes.DEFAULT).outputs, ) added_actions.add(current_action.id) if not outputs: raise SourceException( f"Action {current_action.name} has no outputs.") elif len(outputs) == 1: output = outputs[0] if output.end == output.END: # TODO this is just temporary (while there is while loop), should be rather Return() container.body.append(Continue()) return seq_act = project.action(output.end) seq_act_inputs, _ = project.action_io(seq_act.id) if len(seq_act_inputs ) > 1: # the action belongs to a different block if seq_act.id in added_actions: return logger.debug( f"Action {seq_act.name} going to be added to super_container." ) # test if this is the correct super_container -> count distance (number of blocks) to the START blocks_to_start: Dict[str, int] = {} for inp in seq_act_inputs: parsed_start = inp.parse_start() pact = project.action(parsed_start.start_action_id) blocks_to_start[pact.id] = _blocks_to_start(pact) winner = min(blocks_to_start, key=blocks_to_start.get ) # type: ignore # TODO what is wrong with it? # TODO if blocks_to_start is cached somewhere, the second part of the condition is useless # it might happen that there are two different ways with the same distance if winner == current_action.id or all( value == list(blocks_to_start.values())[0] for value in blocks_to_start.values()): assert super_container is not None _add_logic(super_container, seq_act) return logger.debug(f"Sequential action: {seq_act.name}") _add_logic(container, seq_act, super_container) else: root_if: Optional[If] = None # action has more outputs - each output should have condition for idx, output in enumerate(outputs): if not output.condition: raise SourceException("Missing condition.") # TODO use parameter plugin (action metadata will be needed - to get the return types) # TODO support for other countable types # ...this will only work for booleans from arcor2 import json condition_value = json.loads(output.condition.value) comp = NameConstant(value=condition_value, kind=None) what = output.condition.parse_what() output_name = project.action(what.action_id).flow( what.flow_name).outputs[what.output_index] cond = If( test=Compare(left=Name(id=output_name, ctx=Load()), ops=[Eq()], comparators=[comp]), body=[], orelse=[], ) if idx == 0: root_if = cond container.body.append(root_if) logger.debug(f"Adding branch for: {condition_value}") else: assert isinstance(root_if, If) root_if.orelse.append(cond) if output.end == output.END: cond.body.append( Continue()) # TODO should be rather return continue _add_logic(cond, project.action(output.end), container)
async def handle_message(msg: str) -> None: try: data = json.loads(msg) except json.JsonException as e: logger.error(f"Invalid data: '{msg}'.") logger.debug(e) return if not isinstance(data, dict): logger.error(f"Invalid data: '{data}'.") return if "request" in data: # ...then it is RPC req_type = data["request"] try: rpc_cls, rpc_cb = rpc_dict[req_type] except KeyError: logger.error(f"Unknown RPC request: {data}.") return assert req_type == rpc_cls.__name__ try: req = rpc_cls.Request.from_dict(data) except ValidationError as e: logger.error(f"Invalid RPC: {data}, error: {e}") return except Arcor2Exception as e: # this might happen if e.g. some dataclass does additional validation of values in its __post_init__ try: await client.send( rpc_cls.Response(data["id"], False, messages=[str(e)]).to_json()) logger.debug(e, exc_info=True) except (KeyError, websockets.exceptions.ConnectionClosed): pass return else: try: rpc_start = time.monotonic() resp = await rpc_cb(req, client) rpc_dur = time.monotonic() - rpc_start if rpc_dur > MAX_RPC_DURATION: logger.warn( f"{req.request} callback took {rpc_dur:.3f}s.") except Arcor2Exception as e: logger.debug(e, exc_info=True) resp = rpc_cls.Response(req.id, False, [str(e)]) else: if resp is None: # default response resp = rpc_cls.Response(req.id, True) else: assert isinstance(resp, rpc_cls.Response) resp.id = req.id try: await client.send(resp.to_json()) except websockets.exceptions.ConnectionClosed: return if logger.level == LogLevel.DEBUG: # Silencing of repetitive log messages # ...maybe this could be done better and in a more general way using logging.Filter? now = time.monotonic() if req.request not in req_last_ts: req_last_ts[req.request] = deque() while req_last_ts[req.request]: if req_last_ts[req.request][0] < now - 5.0: req_last_ts[req.request].popleft() else: break req_last_ts[req.request].append(now) req_per_sec = len(req_last_ts[req.request]) / 5.0 if req_per_sec > 2: if req.request not in ignored_reqs: ignored_reqs.add(req.request) logger.debug( f"Request of type {req.request} will be silenced.") elif req_per_sec < 1: if req.request in ignored_reqs: ignored_reqs.remove(req.request) if req.request not in ignored_reqs: logger.debug(f"RPC request: {req}, result: {resp}") elif "event" in data: # ...event from UI assert event_dict try: event_cls, event_cb = event_dict[data["event"]] except KeyError as e: logger.error(f"Unknown event type: {e}.") return try: event = event_cls.from_dict(data) except ValidationError as e: logger.error(f"Invalid event: {data}, error: {e}") return await event_cb(event, client) else: logger.error(f"unsupported format of message: {data}")
def test_project_ap_rpcs(start_processes: None, ars: ARServer) -> None: upload_def(Box, BoxModel(Box.__name__, 1, 2, 3)) event(ars, events.c.ShowMainScreen) assert ars.call_rpc( rpc.s.NewScene.Request(get_id(), rpc.s.NewScene.Request.Args("Test scene")), rpc.s.NewScene.Response ).result assert len(event(ars, events.o.ChangedObjectTypes).data) == 1 scene_data = event(ars, events.s.OpenScene).data assert scene_data scene = scene_data.scene event(ars, events.s.SceneState) assert ars.call_rpc( rpc.s.AddObjectToScene.Request(get_id(), rpc.s.AddObjectToScene.Request.Args("box", Box.__name__, Pose())), rpc.s.AddObjectToScene.Response, ).result obj = event(ars, events.s.SceneObjectChanged).data assert obj # ------------------------------------------------------------------------------------------------------------------ assert ars.call_rpc( rpc.p.NewProject.Request(get_id(), rpc.p.NewProject.Request.Args(scene.id, "Project name")), rpc.p.NewProject.Response, ).result event(ars, events.s.SceneSaved) event(ars, events.p.OpenProject) event(ars, events.s.SceneState) assert ars.call_rpc( rpc.p.AddActionPoint.Request(get_id(), rpc.p.AddActionPoint.Request.Args("parent_ap", Position())), rpc.p.AddActionPoint.Response, ).result parent_ap_evt = event(ars, events.p.ActionPointChanged) assert ars.call_rpc( rpc.p.AddActionPoint.Request( get_id(), rpc.p.AddActionPoint.Request.Args("child_ap", Position(-1), parent_ap_evt.data.id) ), rpc.p.AddActionPoint.Response, ).result child_ap_evt = event(ars, events.p.ActionPointChanged) assert child_ap_evt.data.parent == parent_ap_evt.data.id lock_object(ars, child_ap_evt.data.id) assert ars.call_rpc( rpc.p.AddActionPointOrientation.Request( get_id(), rpc.p.AddActionPointOrientation.Request.Args(child_ap_evt.data.id, Orientation()) ), rpc.p.AddActionPointOrientation.Response, ).result ori = event(ars, events.p.OrientationChanged) assert ars.call_rpc( rpc.p.AddAction.Request( get_id(), rpc.p.AddAction.Request.Args( child_ap_evt.data.id, "act_name", f"{obj.id}/{Box.update_pose.__name__}", [ActionParameter("new_pose", PosePlugin.type_name(), json.dumps(ori.data.id))], [Flow()], ), ), rpc.p.AddAction.Response, ).result event(ars, events.p.ActionChanged) unlock_object(ars, child_ap_evt.data.id) ars.event_mapping[ActionChanged.__name__] = ActionChanged assert ars.call_rpc( rpc.p.CopyActionPoint.Request(get_id(), rpc.p.CopyActionPoint.Request.Args(parent_ap_evt.data.id)), rpc.p.CopyActionPoint.Response, ).result new_parent_ap = event(ars, events.p.ActionPointChanged) assert not new_parent_ap.data.parent new_child_ap = event(ars, events.p.ActionPointChanged) assert new_child_ap.data.parent == new_parent_ap.data.id new_ori = event(ars, events.p.OrientationChanged) assert new_ori.parent_id == new_child_ap.data.id # with events.p.ActionChanged it would return only BareAction (without parameters) new_action = event(ars, ActionChanged) ars.event_mapping[ActionChanged.__name__] = events.p.ActionChanged assert new_action.parent_id == new_child_ap.data.id # Pose parameter (orientation id) should be updated now assert len(new_action.data.parameters) == 1 assert json.loads(new_action.data.parameters[0].value) == new_ori.data.id