From e7e31828334695820eb17d83e05963821d57487a Mon Sep 17 00:00:00 2001 From: Giuseppe Carboni Date: Tue, 28 May 2024 10:19:51 +0200 Subject: [PATCH] Fix #350, fix #351, updated minor_servos (#352) * Fix #35, fix #351, updated minor_servos This branch will not be merged until the component is updated as well * Updated timer for tests * Upgraded minor_servos with setup table * Fixed CSV file * Updated air blade logic * Fix #353, added minor_servos minimal REST Api --- setup.py | 4 ++ simulators/minor_servos/__init__.py | 92 ++++++++++++++++++++--------- simulators/minor_servos/helpers.py | 72 ++++++++++++++++++++++ simulators/minor_servos/setup.csv | 14 +++++ tests/__init__.py | 0 tests/test_minor_servos.py | 75 +++++++++++++++++++---- 6 files changed, 215 insertions(+), 42 deletions(-) create mode 100644 simulators/minor_servos/helpers.py create mode 100644 simulators/minor_servos/setup.csv create mode 100644 tests/__init__.py diff --git a/setup.py b/setup.py index 543ebd7..0363412 100644 --- a/setup.py +++ b/setup.py @@ -13,4 +13,8 @@ 'Operating System :: OS Independent', 'Programming Language :: Python :: 3.10.7', ], + include_package_data=True, + package_data={ + 'simulators': ['minor_servos/setup.csv'] + } ) diff --git a/simulators/minor_servos/__init__.py b/simulators/minor_servos/__init__.py index 6c9ff51..46e0d32 100644 --- a/simulators/minor_servos/__init__.py +++ b/simulators/minor_servos/__init__.py @@ -1,7 +1,6 @@ import time import re import random -import os try: from numpy import sign except ImportError as ex: # skip coverage @@ -17,7 +16,9 @@ from multiprocessing import Value from bisect import bisect_left from socketserver import ThreadingTCPServer +from http.server import HTTPServer from simulators.common import ListeningSystem +from simulators.minor_servos.helpers import setup_import, VBrainRequestHandler # Each system module (like active_surface.py, acu.py, etc.) has to @@ -28,7 +29,8 @@ # subscribe and unsibscribe methods, while kwargs is a dict of optional # extra arguments. servers = [(('0.0.0.0', 12800), (), ThreadingTCPServer, {})] -TIMER_VALUE = 1 if os.environ.get('CI') == 'true' else 5 +httpserver_address = ('0.0.0.0', 12799) +DEFAULT_TIMER_VALUE = 5 def _change_atomic_value(variable, value): @@ -76,7 +78,7 @@ def plc_time(now=None): 'BWG4': {'ID': 24}, } - def __init__(self): + def __init__(self, timer_value=DEFAULT_TIMER_VALUE, rest_api=True): self.msg = '' self.configuration = 0 self.simulation = 1 @@ -86,16 +88,21 @@ def __init__(self): self.emergency = 2 self.gregorian_cap = Value(c_int, 1) self.last_executed_command = 0 + self.timer_value = timer_value self.servos = { 'PFP': PFP(), 'SRP': SRP(), 'M3R': M3R(), 'GFR': GFR(), - 'DerotatoreGFR1': Derotator('GFR1'), - 'DerotatoreGFR2': Derotator('GFR2'), - 'DerotatoreGFR3': Derotator('GFR3'), - 'DerotatorePFP': Derotator('PFP'), + 'DR_GFR1': Derotator('GFR1'), + 'DR_GFR2': Derotator('GFR2'), + 'DR_GFR3': Derotator('GFR3'), + 'DR_PFP': Derotator('PFP'), } + setup_import( + list(self.servos.keys()) + ['GREGORIAN_CAP'], + self.configurations + ) self.stop = Value(c_bool, False) self.update_thread = Thread( target=self._update, @@ -103,6 +110,16 @@ def __init__(self): ) self.update_thread.daemon = True self.update_thread.start() + self.rest_api = rest_api + if self.rest_api: + self.httpserver = HTTPServer( + httpserver_address, + VBrainRequestHandler + ) + self.server_thread = Thread( + target=self.httpserver.serve_forever + ) + self.server_thread.start() self.cover_timer = Timer(1, lambda: None) def __del__(self): @@ -118,6 +135,9 @@ def system_stop(self): self.cover_timer.join() except RuntimeError: pass + if self.rest_api: + self.httpserver.shutdown() + self.server_thread.join() return super().system_stop() @staticmethod @@ -184,22 +204,27 @@ def _setup(self, args): configuration = args[0] if configuration not in self.configurations: return self.bad - self.configuration = self.configurations.get(configuration)['ID'] - for _, servo in self.servos.items(): + configuration = self.configurations.get(configuration) + self.configuration = configuration['ID'] + for servo_name, servo in self.servos.items(): + coordinates = configuration[servo_name] servo.operative_mode_timer.cancel() _change_atomic_value(servo.operative_mode, 0) servo.operative_mode_timer = Timer( - TIMER_VALUE, + self.timer_value, _change_atomic_value, args=(servo.operative_mode, 10) # SETUP ) servo.operative_mode_timer.daemon = True servo.operative_mode_timer.start() - gregorian_cap_position = 1 if self.configuration == 1 else 2 - if self.gregorian_cap.value != gregorian_cap_position: + servo.set_coords(coordinates) + gregorian_cap_position = configuration['GREGORIAN_CAP'][0] + if (gregorian_cap_position + and self.gregorian_cap.value != gregorian_cap_position): + self.cover_timer.cancel() _change_atomic_value(self.gregorian_cap, 0) self.cover_timer = Timer( - TIMER_VALUE, + self.timer_value, _change_atomic_value, args=(self.gregorian_cap, gregorian_cap_position) ) @@ -212,30 +237,37 @@ def _stow(self, args): if len(args) != 2: return self.bad servo_id = args[0] - if servo_id not in list(self.servos) + ['Gregoriano']: + if servo_id not in list(self.servos) + ['GREGORIAN_CAP']: return self.bad try: stow_pos = int(args[1]) # STOW POSITION except ValueError: return self.bad - if servo_id == 'Gregoriano': - if stow_pos not in [1, 2]: + if servo_id == 'GREGORIAN_CAP': + if stow_pos not in range(5): return self.bad if self.gregorian_cap.value != stow_pos: - _change_atomic_value(self.gregorian_cap, 0) - self.cover_timer = Timer( - TIMER_VALUE, - _change_atomic_value, - args=(self.gregorian_cap, stow_pos) - ) - self.cover_timer.daemon = True - self.cover_timer.start() + self.cover_timer.cancel() + if self.gregorian_cap.value <= 1 or stow_pos == 1: + _change_atomic_value(self.gregorian_cap, 0) + self.cover_timer = Timer( + self.timer_value, + _change_atomic_value, + args=(self.gregorian_cap, stow_pos) + ) + self.cover_timer.daemon = True + self.cover_timer.start() + else: + _change_atomic_value( + self.gregorian_cap, + stow_pos + ) else: servo = self.servos.get(servo_id) servo.operative_mode_timer.cancel() _change_atomic_value(servo.operative_mode, 0) servo.operative_mode_timer = Timer( - TIMER_VALUE, + self.timer_value, _change_atomic_value, args=(servo.operative_mode, 20) # STOW ) @@ -254,7 +286,7 @@ def _stop(self, args): servo.operative_mode_timer.cancel() _change_atomic_value(servo.operative_mode, 0) servo.operative_mode_timer = Timer( - TIMER_VALUE, + self.timer_value, _change_atomic_value, args=(servo.operative_mode, 30) # STOP ) @@ -281,7 +313,7 @@ def _preset(self, args): servo.operative_mode_timer.cancel() _change_atomic_value(servo.operative_mode, 0) servo.operative_mode_timer = Timer( - TIMER_VALUE, + self.timer_value, _change_atomic_value, args=(servo.operative_mode, 40) # PRESET ) @@ -488,7 +520,9 @@ def get_status(self, now): def set_coords(self, coords): for index, value in enumerate(coords): - self.coords[index] = value + if value is None: + continue + self.coords[index] = value + self.offsets[index] def set_offsets(self, coords): for index, value in enumerate(coords): @@ -618,7 +652,7 @@ def __init__(self, name): self.max_coord = [220.0] self.min_coord = [-220.0] self.max_delta = [3.3] - super().__init__(name) + super().__init__(f'DR_{name}') def get_status(self, now): answer = super().get_status(now) diff --git a/simulators/minor_servos/helpers.py b/simulators/minor_servos/helpers.py new file mode 100644 index 0000000..0b43636 --- /dev/null +++ b/simulators/minor_servos/helpers.py @@ -0,0 +1,72 @@ +import os +import csv +import json +from http.server import BaseHTTPRequestHandler +import pkg_resources + + +def setup_import(servos, configurations): + filename = os.environ.get('ACS_CDB', '/') + filename = os.path.join( + filename, + 'CDB', + 'alma', + 'DataBlock', + 'MinorServo', + 'Tabella Setup.csv' + ) + if not os.path.exists(filename): + filename = pkg_resources.resource_filename( + 'simulators', + 'minor_servos/setup.csv' + ) + with open(filename, 'r', encoding='utf-8') as csvfile: + reader = csv.reader(csvfile, delimiter=';') + indexes = {} + for line in reader: + if not indexes: + for servo in servos: + temp = [e for e in line if e.startswith(servo)] + indexes[servo] = [line.index(e) for e in temp] + continue + for servo, servo_indexes in indexes.items(): + coordinates = [] + for index in servo_indexes: + coord = line[index] + try: + if servo == 'GREGORIAN_CAP': + coord = int(coord) + else: + coord = float(coord) + except ValueError: + coord = None + coordinates.append(coord) + configurations[line[0]][servo] = coordinates + + +class VBrainRequestHandler(BaseHTTPRequestHandler): + + emergency = 'INAF_SRT_OR7_EMG_RESET_CMD' + alarm = 'INAF_SRT_OR7_RESET_CMD' + baseurl = '/Exporting/json/ExecuteCommand?name' + urls = [ + f'{baseurl}={emergency}', + f'{baseurl}={alarm}' + ] + answer = {'Message': 'OUTPUT:GOOD', 'Status': 'Good'} + + def do_GET(self): + try: + if self.path in self.urls: + self.send_response(200) + self.send_header('Content-type', 'application/json') + self.end_headers() + self.wfile.write(json.dumps(self.answer).encode()) + else: + self.send_response(404) + self.end_headers() + except BrokenPipeError: # skip coverage + pass + + def log_message(self, *_): + pass diff --git a/simulators/minor_servos/setup.csv b/simulators/minor_servos/setup.csv new file mode 100644 index 0000000..4c21c91 --- /dev/null +++ b/simulators/minor_servos/setup.csv @@ -0,0 +1,14 @@ +CONFIGURATION;PFP_TX;PFP_TZ;PFP_RTHETA;SRP_TX;SRP_TY;SRP_TZ;SRP_RX;SRP_RY;SRP_RZ;M3R_RZ;GFR_RZ;DR_GFR1;DR_GFR2;DR_GFR3;DR_PFP;GREGORIAN_CAP; +Primario;0;0;0;-5;5;-120;0;0;0;*;*;*;*;*;*;1; +Gregoriano1;0;0;0;-1.5;11.1393650793988;1.08830677049999;0.049894179898239;-0.036111111111111;0;*;-88.70659;*;*;*;*;2; +Gregoriano2;0;0;0;-1.5;11.1393650793988;1.08830677049999;0.049894179898239;-0.036111111111111;0;*;-159.8899;*;*;*;*;2; +Gregoriano3;0;0;0;-1.5;11.1393650793988;1.08830677049999;0.049894179898239;-0.036111111111111;0;*;90.97161;*;*;*;*;2; +Gregoriano4;0;0;0;-1.5;11.1393650793988;1.08830677049999;0.049894179898239;-0.036111111111111;0;*;162.771;*;*;*;*;2; +Gregoriano5;0;0;0;-1.5;11.1393650793988;1.08830677049999;0.049894179898239;-0.036111111111111;0;*;55.373967;*;*;*;*;2; +Gregoriano6;0;0;0;-1.5;11.1393650793988;1.08830677049999;0.049894179898239;-0.036111111111111;0;*;-51.82117;*;*;*;*;2; +Gregoriano7;0;0;0;*;*;*;*;*;*;*;*;*;*;*;*;*; +Gregoriano8;0;0;0;*;*;*;*;*;*;*;*;*;*;*;*;*; +BWG1;0;0;0;-1.5;11.1393650793988;0.358165166130078;0.049894179898239;-0.036111111111111;0;10;0;*;*;*;*;2; +BWG2;0;0;0;*;*;*;*;*;*;*;*;*;*;*;*;*; +BWG3;0;0;0;-1.5;11.1393650793988;-3.77159716192807;0.049894179898239;-0.036111111111111;0;20;0;*;*;*;*;2; +BWG4;0;0;0;*;*;*;*;*;*;*;*;*;*;*;*;*; diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_minor_servos.py b/tests/test_minor_servos.py index 5f2ef12..301202f 100644 --- a/tests/test_minor_servos.py +++ b/tests/test_minor_servos.py @@ -1,9 +1,11 @@ import unittest import random import time -from simulators.minor_servos import System, TIMER_VALUE - +import requests +from simulators.minor_servos import System, httpserver_address +from simulators.minor_servos.helpers import VBrainRequestHandler +TIMER_VALUE = 0.01 tail = '\r\n' good = r'^OUTPUT:GOOD,[0-9]+\.[0-9]{6}' bad = f'^OUTPUT:BAD{tail}$' @@ -12,7 +14,10 @@ class TestMinorServos(unittest.TestCase): def setUp(self): - self.system = System() + self.system = System( + timer_value=TIMER_VALUE, + rest_api=False + ) def tearDown(self): del self.system @@ -149,15 +154,15 @@ def test_status_GFR(self): def test_status_derotators(self): derotators = ['GFR1', 'GFR2', 'GFR3', 'PFP'] for derotator in derotators: - cmd = fr'STATUS=Derotatore{derotator}{tail}' + cmd = fr'STATUS=DR_{derotator}{tail}' regex = good - regex += fr',{derotator}_ENABLED=1\|' - regex += fr'{derotator}_STATUS=1\|' - regex += fr'{derotator}_BLOCK=2\|' - regex += fr'{derotator}_OPERATIVE_MODE=0\|' - regex += fr'{derotator}_ROTARY_AXIS_ENABLED=1\|' - regex += fr'{derotator}_ROTATION=[\-]?[0-9]+\.[0-9]+\|' - regex += fr'{derotator}_OFFSET=[0-9]+\.[0-9]+' + regex += fr',DR_{derotator}_ENABLED=1\|' + regex += fr'DR_{derotator}_STATUS=1\|' + regex += fr'DR_{derotator}_BLOCK=2\|' + regex += fr'DR_{derotator}_OPERATIVE_MODE=0\|' + regex += fr'DR_{derotator}_ROTARY_AXIS_ENABLED=1\|' + regex += fr'DR_{derotator}_ROTATION=[\-]?[0-9]+\.[0-9]+\|' + regex += fr'DR_{derotator}_OFFSET=[0-9]+\.[0-9]+' regex += fr'{tail}$' for byte in cmd[:-1]: self.assertTrue(self.system.parse(byte)) @@ -210,15 +215,28 @@ def test_stow(self): def test_stow_gregorian_cap(self): for stow_pos in [1, 2]: - cmd = f'STOW=Gregoriano,{stow_pos}{tail}' + cmd = f'STOW=GREGORIAN_CAP,{stow_pos}{tail}' for byte in cmd[:-1]: self.assertTrue(self.system.parse(byte)) self.assertRegex(self.system.parse(cmd[-1]), f'{good}{tail}$') time.sleep(TIMER_VALUE + 0.1) self.assertEqual(self.system.gregorian_cap.value, stow_pos) + def test_stow_gregorian_air_blade(self): + cmd = f'STOW=GREGORIAN_CAP,2{tail}' + for byte in cmd[:-1]: + self.assertTrue(self.system.parse(byte)) + self.assertRegex(self.system.parse(cmd[-1]), f'{good}{tail}$') + time.sleep(TIMER_VALUE + 0.1) + self.assertEqual(self.system.gregorian_cap.value, 2) + cmd = f'STOW=GREGORIAN_CAP,3{tail}' + for byte in cmd[:-1]: + self.assertTrue(self.system.parse(byte)) + self.assertRegex(self.system.parse(cmd[-1]), f'{good}{tail}$') + self.assertEqual(self.system.gregorian_cap.value, 3) + def test_stow_gregorian_cap_wrong_pos(self): - cmd = f'STOW=Gregoriano,3{tail}' + cmd = f'STOW=GREGORIAN_CAP,5{tail}' for byte in cmd[:-1]: self.assertTrue(self.system.parse(byte)) self.assertRegex(self.system.parse(cmd[-1]), bad) @@ -541,5 +559,36 @@ def test_offset_non_numeric_coordinates(self): self.assertRegex(self.system.parse(cmd[-1]), bad) +class TestRESTApi(unittest.TestCase): + + def setUp(self): + self.system = System( + rest_api=True + ) + + def tearDown(self): + del self.system + + def test_rest_GET(self): + for url in VBrainRequestHandler.urls: + baseurl = f'http://{httpserver_address[0]}:{httpserver_address[1]}' + url = baseurl + url + try: + response = requests.get(url, timeout=TIMER_VALUE) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), VBrainRequestHandler.answer) + except requests.exceptions.ReadTimeout: + self.fail('Request is taking too long to be answered') + + def test_rest_GET_wrong_address(self): + baseurl = f'http://{httpserver_address[0]}:{httpserver_address[1]}' + url = baseurl + '/wrong' + try: + response = requests.get(url, timeout=TIMER_VALUE) + self.assertEqual(response.status_code, 404) + except requests.exceptions.ReadTimeout: + self.fail('Request is taking too long to be answered') + + if __name__ == '__main__': unittest.main()