diff --git a/config/machine_settings/defaults.ini b/config/machine_settings/defaults.ini index 3ac4deb..2d03bab 100644 --- a/config/machine_settings/defaults.ini +++ b/config/machine_settings/defaults.ini @@ -9,9 +9,9 @@ exposure_time: 10000 ; vertical_crop_resolution: 2048 ; rotate_90_clockwise times: 0 ; auto_white_balance: off -balance_red: 1.66 +balance_red: 1.85 balance_green: 1.0 -balance_blue: 1.12 +balance_blue: 1.5 [vision_saver] time_format: %Y-%m-%d_%H-%M-%S @@ -44,6 +44,6 @@ baudrate: 9600 [vision] detection_threshold: 0.5 -neural_network: hs3-20000 +neural_network: hs5-20000 ; recipes_path: ./config/vision_test_recipes diff --git a/config/machine_settings/hostnames.ini b/config/machine_settings/hostnames.ini index ec8cff8..50cc150 100644 --- a/config/machine_settings/hostnames.ini +++ b/config/machine_settings/hostnames.ini @@ -2,3 +2,4 @@ this: this mino: mino DESKTOP-VOCOB38: vm +ST-RES-1: stres1 \ No newline at end of file diff --git a/config/machine_settings/stres1.ini b/config/machine_settings/stres1.ini new file mode 100644 index 0000000..b338022 --- /dev/null +++ b/config/machine_settings/stres1.ini @@ -0,0 +1,8 @@ +[tecna_t3] +port: COM5 + +[neo_pixels] +port: COM4 +baudrate: 9600 + + diff --git a/config/vision/labels/labels.pbtxt b/config/vision/labels/labels.pbtxt index 5cc99f0..4ab5798 100644 --- a/config/vision/labels/labels.pbtxt +++ b/config/vision/labels/labels.pbtxt @@ -1,7 +1,7 @@ item { id: 1 name: 'hs-ok' - color: '0x000000' + color: '0x55AA55' } item { id: 2 diff --git a/config/vision/recipes/termorestringente_923578.ini b/config/vision/recipes/termorestringente_923578.ini index a68160e..9f9a83d 100644 --- a/config/vision/recipes/termorestringente_923578.ini +++ b/config/vision/recipes/termorestringente_923578.ini @@ -22,7 +22,7 @@ instruction: CONTROLLARE PRESENZA TERMORESTRINGENTE [markers] [zones] -p1: 800,850 300,100 hs-ok # HEATSINK PRESENT +p1: 1000,500 800,800 hs-ok # HEATSINK PRESENT [labels] -p1: 500,600 120 0xffffffff 0xff000000 4 TERMORESTRINGENTE +p1: 660,1200 120 0xffffffff 0xff000000 4 TERMORESTRINGENTE diff --git a/designer_win.sh b/designer_win.sh new file mode 100644 index 0000000..11fefa5 --- /dev/null +++ b/designer_win.sh @@ -0,0 +1,2 @@ +#!/bin/bash +venv/Lib/site-packages/qt5_applications/Qt/bin/designer diff --git a/run_chart_example.sh b/run_chart_example.sh deleted file mode 100755 index 6b7f73c..0000000 --- a/run_chart_example.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -e -cd "$(dirname "$0")" -source "./venv/Scripts/activate" || source "./venv/bin/activate" -python -O "./src/lib/charts/MultiAxisExample.py" diff --git a/run_lifecycle.sh b/run_lifecycle.sh deleted file mode 100755 index 49e80f6..0000000 --- a/run_lifecycle.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -e -cd "$(dirname "$0")" -source "./venv/Scripts/activate" || source "./venv/bin/activate" -python -O "./src/lifecycle.py" diff --git a/runme.sh b/runme.sh index bec3ef5..c707543 100755 --- a/runme.sh +++ b/runme.sh @@ -1,4 +1,4 @@ #!/bin/bash -e cd "$(dirname "$0")" source "./venv/bin/activate" || source "./venv/Scripts/activate" || : -python -O "./src/main.py" +python -O "./src/main.py" --no-edgetpu --no-tflite diff --git a/src/components/neo_pixels.py b/src/components/neo_pixels.py index 7481d8a..cf11052 100644 --- a/src/components/neo_pixels.py +++ b/src/components/neo_pixels.py @@ -1,4 +1,5 @@ import sys +import time import traceback if "--sim-serial" in sys.argv: @@ -16,6 +17,15 @@ C_UINT32_MAX = c_uint32(-1).value class NeoPixels(Component): def __init__(self, config=None, name=None): super().__init__(config=config, name=name, threaded=False) + self.conn = serial.Serial( + self.port, + baudrate=self.baudrate, + stopbits=self.stopbits, + parity=self.parity, + bytesize=self.bytesize, + ) + time.sleep(5) + self.set_all_pixel_color("#000000") def config_changed(self): self.port = self.config[self.name]["port"] @@ -30,18 +40,11 @@ class NeoPixels(Component): if not color.startswith("#") or len(color) != 7: raise ValueError("the color parameter must be in the html hex format: '#RRGGBB'") try: - conn = serial.Serial( - self.port, - baudrate=self.baudrate, - stopbits=self.stopbits, - parity=self.parity, - bytesize=self.bytesize, - ) - conn.write(int.to_bytes(pixel, length=4, byteorder="big") + bytes.fromhex(color[1:7]) + b"\n") - response = conn.readline() - if response != "ok\n": + self.conn.write(int.to_bytes(pixel, length=4, byteorder="big") + bytes.fromhex(color[1:7]) + b"\n") + response = self.conn.readline() + if response != b"ok\n": self.log.error(response.strip()) - conn.close() + # conn.close() except serial.serialutil.SerialException: self.log.exception(traceback.format_exc()) diff --git a/src/components/tecna_marposs_provaset_t3p.py b/src/components/tecna_marposs_provaset_t3p.py index 3daee7b..6db4684 100644 --- a/src/components/tecna_marposs_provaset_t3p.py +++ b/src/components/tecna_marposs_provaset_t3p.py @@ -180,7 +180,7 @@ class TecnaMarpossProvasetT3P(ModbusComponent): "Running test: test type", "Running test: sequence index", ]} - if info["Running test: active phase"] == "END TEST, WAITING THE START OF A NEW TEST": + if info["Running test: active phase"] == "FINE TEST": info.update(self.get_test_results()) self.log.debug(str(info)) super()._get([info]) diff --git a/src/components/tecna_marposs_provaset_t3p_registers.py b/src/components/tecna_marposs_provaset_t3p_registers.py index 071c46a..cdfeedf 100644 --- a/src/components/tecna_marposs_provaset_t3p_registers.py +++ b/src/components/tecna_marposs_provaset_t3p_registers.py @@ -7,7 +7,7 @@ registers = { "Life counter: TOTAL TESTS": [8 - 1, {"dt": "32bit_uint", }], "Real time test pressure output": [11 - 1, {"dt": "32bit_int", "f": 21, }], "Real time differential pressure output": [13 - 1, {"dt": "32bit_int", "f": 22, }], - "Real time pressure line regulator": [15 - 1, {"dt": "16bit_int", "f": 23, }], + "Real time pressure line regulator": [15 - 1, {"dt": "16bit_int", "f": 23,"g":0.1 }], "Active alarm flags": [16 - 1, {"dt": "16bit_uint", }], # | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | # | | | | A5 | A4 | A3 | A2 | A1 | | | | | | E3 | E2 | E1 | @@ -28,9 +28,9 @@ registers = { "Running test: flow rate format": [27 - 1, {"dt": "16bit_uint", }], # white "Active test program number": [31 - 1, {"dt": "16bit_uint", }], "Running test: active phase": [32 - 1, {"dt": "16bit_uint", "decoding": { - 0: "WAITING START", - 10: "CHECK BARCODE", - 20: "WAITING BARCODE", + 0: "ATTESA START", + 10: "CONTROLLO BARCODE", + 20: "ATTESA BARCODE", 30: "START TEST", 40: "START CAGE", 50: "START PLUG", @@ -38,10 +38,10 @@ registers = { 70: "WAIT DELAY ACKNOWLEDGE COMMAND", 80: "WAIT DELAY", 90: "WAIT ACKNOWLEDGE", - 100: "T0 PRE-FILL", - 110: "T1 FILLING", - 120: "T2 SETTLING", - 130: "T3 MEASURE", + 100: "T0 PRE-RIEMPIMENTO", + 110: "T1 RIEMPIMENTO", + 120: "T2 ASSESTAMENTO", + 130: "T3 MISURA", 140: "WAITING OPERATOR RESULT ACKNOWLEDGE", 150: "RESULT PRESENT", 160: "MARKING", @@ -49,7 +49,7 @@ registers = { 180: "END SEQUENCE PROGRAM", 190: "END PLUG", 200: "END CAGE", - 210: "END TEST, WAITING THE START OF A NEW TEST", + 210: "FINE TEST", }, }], "Running test: phase backwards time": [33 - 1, {"dt": "16bit_uint", "f": 26, }], "Running test: filling pressure": [34 - 1, {"dt": "32bit_int", "f": 21, }], diff --git a/src/components/vision.py b/src/components/vision.py index 9f28b87..f2e2665 100644 --- a/src/components/vision.py +++ b/src/components/vision.py @@ -318,6 +318,7 @@ class Vision(Component): try: # create tensorflow model model = tf.saved_model.load(str(self.models_dir / model_name)) + model = model.signatures['serving_default'] tf_mode = "normal" except Exception: self.log.exception(traceback.format_exc()) @@ -385,12 +386,15 @@ class Vision(Component): if lock: self.lock.unlock() detections = { - "detection_scores": [scores], - "detection_boxes": [boxes], - "detection_classes": [map(lambda class_id: class_id + 1, class_ids)], + "detection_scores": [scores][0:1], + "detection_boxes": [boxes][0:1], + "detection_classes": [map(lambda class_id: class_id + 1, class_ids)][0:1], } else: - detections = self.model(tensor) + input_tensor = tf.convert_to_tensor(np.asarray(tensor)) + #input_tensor = input_tensor[tf.newaxis, ...] + + detections = self.model(input_tensor) if lock: self.lock.unlock() # WARNING: results other than the ones related to tensor[-1] will be discarded diff --git a/src/test/multicomp.py b/src/test/multicomp.py index c06d821..f23cacd 100644 --- a/src/test/multicomp.py +++ b/src/test/multicomp.py @@ -2,8 +2,11 @@ import time import serial +#tester_port="/dev/ttyUSB0" +tester_port="COM1" + conn = serial.Serial( - "/dev/ttyUSB0", + tester_port, baudrate=115200, stopbits=serial.STOPBITS_ONE, parity=serial.PARITY_NONE, diff --git a/src/test/neo_lights.py b/src/test/neo_lights.py index c9a3394..e64e933 100644 --- a/src/test/neo_lights.py +++ b/src/test/neo_lights.py @@ -1,17 +1,21 @@ +import time from ctypes import c_uint32 import serial C_UINT32_MAX = c_uint32(-1).value conn = serial.Serial( - "/dev/ttyACM1", + "COM4", baudrate=9600, stopbits=serial.STOPBITS_ONE, parity=serial.PARITY_NONE, bytesize=serial.EIGHTBITS, + rtscts=False, + xonxoff=False ) pixel = C_UINT32_MAX -color = "#ff00ff" +time.sleep(5) +color = "#000000" conn.write(int.to_bytes(pixel, length=4, byteorder="big") + bytes.fromhex(color[1:7]) + b"\n") response = conn.readline() print(response) diff --git a/src/ui/test/test.py b/src/ui/test/test.py index c5533ea..0a3f986 100755 --- a/src/ui/test/test.py +++ b/src/ui/test/test.py @@ -44,7 +44,7 @@ class Test(Widget): "done": Test_Assembly(self.select_step_img("success"), u"COLLAUDO COMPLETATO"), "emergency": Test_Assembly(self.select_step_img("reset_emergency"), u"EMERGENZA INTERVENUTA - RIPRISTINARE PULSANTE E SELEZIONARE \"RESET EMERGENZA\" DAL MEN\u00d9 \"STRUMENTI\""), "fail": Test_Assembly(self.select_step_img("fail"), u"CICLO INTERROTTO"), - "leak": Test_Assembly(None, u"PREMERE START PER COMINCIARE LA PROVA TENUTA E ATTENDERE IL SUO COMPLETAMENTO", Test_Leak(components=self.components, recipe=self.recipe, step=self.step)), + "leak": Test_Assembly(None, u"PREMERE START PER INIZIARE LA PROVA TENUTA", Test_Leak(components=self.components, recipe=self.recipe, step=self.step)), "print": Test_Assembly(self.select_step_img("print"), u"STAMPA ETICHETTA IN CORSO"), "select_recipe": Test_Assembly(None, u"SELEZIONARE IL CODICE DA COLLAUDARE", Recipe_Selection()), "vision": Test_Assembly(None, u"VERIFICARE CONTROLLO CON TELECAMERA", Test_Vision(components=self.components, recipe=self.recipe, step=self.step)), @@ -270,6 +270,8 @@ class Test(Widget): def done(self, ok=True): self.log.info("cycle done") + self.data["vision"][0].pop("detections") + self.data["vision"][0].pop("results") archived = Archive.archive(self.recipe, self.data, ok and self.data["ok"], overridden=self.data["overridden"]) self.log.info(f"cycle archived locally: {archived!r}") # COUNT OK @@ -279,17 +281,22 @@ class Test(Widget): def print(self, archived=None): self.log.info("cycle print") # LABEL PRINT + datamatrix= archived.recipe.part_number+ archived.time.strftime("%m%y")+f"{archived.id:0>5}" + dt_label=archived.time.strftime("%d/%m/%Y %H:%M:%S") + pmax = self.data.get("leak", [{}, {}])[0].get("step", {}).get("spec", {}).get("test_pressure", "-") + pmin=self.data.get("leak", [{}, {}])[0].get("results", {}).get("data", {}).get("Running test: pressure at the end of settling", "-") + leak=self.data.get("leak", [{}, {}])[0].get("results", {}).get("data", {}).get("Running test: measured leak", "-") context = { - "DATAMATRIX": str(self.data.get("barcodes", {}).get("serial", "")), - "DATAMATRIX_TEXT": str(self.data.get("barcodes", {}).get("serial", "-")), - "PIECE_NUM": str(self.data.get("barcodes", {}).get("serial", "-")), - "DATETIME": str(archived.time.isoformat()), + "DATAMATRIX": datamatrix, + "DATAMATRIX_TEXT": datamatrix, + "PIECE_NUM": str(archived.id), + "DATETIME": dt_label, "SHIFT": str(get_shift(archived.time)), "STATION": str(self.machine_id), "OPERATOR": str(Users.get_session().user.username), - "PMAX": str(self.data.get("leak", [{}, {}])[0].get("step", {}).get("spec", {}).get("test_pressure", "-")), - "PMIN": str(self.data.get("leak", [{}, {}])[0].get("results", {}).get("data", {}).get("Running test: pressure at the end of settling", "-")), - "LEAK": str(self.data.get("leak", [{}, {}])[0].get("results", {}).get("data", {}).get("Running test: measured leak", "-")), + "PMAX": f"{pmax:.3f}", + "PMIN": f"{pmin:.3f}", + "LEAK": f"{leak:.3f}", "TFILL": str(self.data.get("leak", [{}, {}])[0].get("step", {}).get("spec", {}).get("filling_time", "-")), "TSTAB": str(self.data.get("leak", [{}, {}])[0].get("step", {}).get("spec", {}).get("settling_time", "-")), "TTEST": str(self.data.get("leak", [{}, {}])[0].get("step", {}).get("spec", {}).get("test_time", "-")), diff --git a/src/ui/test_leak/test_leak.ui b/src/ui/test_leak/test_leak.ui index 2ed8e36..af9e28f 100644 --- a/src/ui/test_leak/test_leak.ui +++ b/src/ui/test_leak/test_leak.ui @@ -6,505 +6,14 @@ 0 0 - 305 - 459 + 980 + 704 Test Leak - - - - - 0 - 0 - - - - - 0 - 100 - - - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 112 - 114 - - - - - - - 255 - 127 - 127 - - - - - - - 255 - 63 - 63 - - - - - - - 127 - 0 - 0 - - - - - - - 170 - 0 - 0 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 112 - 114 - - - - - - - 255 - 112 - 114 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 127 - 127 - - - - - - - 255 - 255 - 220 - - - - - - - 0 - 0 - 0 - - - - - - - 0 - 0 - 0 - - - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 112 - 114 - - - - - - - 255 - 127 - 127 - - - - - - - 255 - 63 - 63 - - - - - - - 127 - 0 - 0 - - - - - - - 170 - 0 - 0 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 112 - 114 - - - - - - - 255 - 112 - 114 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 127 - 127 - - - - - - - 255 - 255 - 220 - - - - - - - 0 - 0 - 0 - - - - - - - 0 - 0 - 0 - - - - - - - - - 127 - 0 - 0 - - - - - - - 255 - 112 - 114 - - - - - - - 255 - 127 - 127 - - - - - - - 255 - 63 - 63 - - - - - - - 127 - 0 - 0 - - - - - - - 170 - 0 - 0 - - - - - - - 127 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 127 - 0 - 0 - - - - - - - 255 - 112 - 114 - - - - - - - 255 - 112 - 114 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 0 - 0 - - - - - - - 255 - 255 - 220 - - - - - - - 0 - 0 - 0 - - - - - - - 0 - 0 - 0 - - - - - - - - - 20 - 75 - true - - - - background-color:rgb(255, 112, 114); - - - STOP - - - - - - - - 0 - 0 - - - - - 12 - 75 - true - - - - FORZA ACCETTAZIONE - - - @@ -522,9 +31,9 @@ - 12 - 75 - true + 16 + 50 + false @@ -539,9 +48,9 @@ - 12 - 75 - true + 16 + 50 + false @@ -556,9 +65,9 @@ - 24 - 75 - true + 16 + 50 + false @@ -573,9 +82,9 @@ - 12 - 75 - true + 16 + 50 + false @@ -588,6 +97,24 @@ + + + 130 + 16777215 + + + + + 20 + 50 + false + + + + background-color: rgb(255, 255, 255); +border: 1px solid black; + + - @@ -595,6 +122,24 @@ + + + 130 + 16777215 + + + + + 20 + 50 + false + + + + background-color: rgb(255, 255, 255); +border: 1px solid black; + + - @@ -604,9 +149,9 @@ - 12 - 75 - true + 16 + 50 + false @@ -632,6 +177,24 @@ + + + 130 + 16777215 + + + + + 20 + 50 + false + + + + background-color: rgb(255, 255, 255); +border: 1px solid black; + + - @@ -641,9 +204,9 @@ - 12 - 75 - true + 16 + 50 + false @@ -656,6 +219,13 @@ + + + 16 + 50 + false + + mbar @@ -663,6 +233,13 @@ + + + 16 + 50 + false + + mbar @@ -670,6 +247,13 @@ + + + 16 + 50 + false + + mbar @@ -679,11 +263,16 @@ - 24 - 75 - true + 16 + 50 + false + + background-color: rgb(255, 255, 255); +border: 1px solid black; + + - @@ -691,6 +280,18 @@ + + + 16 + 50 + false + + + + background-color: rgb(255, 255, 255); +border: 1px solid black; + + - @@ -698,6 +299,18 @@ + + + 16 + 50 + false + + + + background-color: rgb(255, 255, 255); +border: 1px solid black; + + - @@ -1231,6 +844,477 @@ + + + + + 0 + 0 + + + + + 0 + 100 + + + + + + + + + 0 + 0 + 0 + + + + + + + 255 + 112 + 114 + + + + + + + 255 + 127 + 127 + + + + + + + 255 + 63 + 63 + + + + + + + 127 + 0 + 0 + + + + + + + 170 + 0 + 0 + + + + + + + 0 + 0 + 0 + + + + + + + 255 + 255 + 255 + + + + + + + 0 + 0 + 0 + + + + + + + 255 + 112 + 114 + + + + + + + 255 + 112 + 114 + + + + + + + 0 + 0 + 0 + + + + + + + 255 + 127 + 127 + + + + + + + 255 + 255 + 220 + + + + + + + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + + + + + + + + + 0 + 0 + 0 + + + + + + + 255 + 112 + 114 + + + + + + + 255 + 127 + 127 + + + + + + + 255 + 63 + 63 + + + + + + + 127 + 0 + 0 + + + + + + + 170 + 0 + 0 + + + + + + + 0 + 0 + 0 + + + + + + + 255 + 255 + 255 + + + + + + + 0 + 0 + 0 + + + + + + + 255 + 112 + 114 + + + + + + + 255 + 112 + 114 + + + + + + + 0 + 0 + 0 + + + + + + + 255 + 127 + 127 + + + + + + + 255 + 255 + 220 + + + + + + + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + + + + + + + + + 127 + 0 + 0 + + + + + + + 255 + 112 + 114 + + + + + + + 255 + 127 + 127 + + + + + + + 255 + 63 + 63 + + + + + + + 127 + 0 + 0 + + + + + + + 170 + 0 + 0 + + + + + + + 127 + 0 + 0 + + + + + + + 255 + 255 + 255 + + + + + + + 127 + 0 + 0 + + + + + + + 255 + 112 + 114 + + + + + + + 255 + 112 + 114 + + + + + + + 0 + 0 + 0 + + + + + + + 255 + 0 + 0 + + + + + + + 255 + 255 + 220 + + + + + + + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + + + + + + + + + 20 + 75 + true + + + + background-color:rgb(255, 112, 114); + + + STOP + + + @@ -1239,6 +1323,13 @@ 0 + + + 12 + 75 + true + + FORZA ACCETTAZIONE diff --git a/src/ui/test_vision/test_vision.py b/src/ui/test_vision/test_vision.py index 3d1ebdf..27093f9 100644 --- a/src/ui/test_vision/test_vision.py +++ b/src/ui/test_vision/test_vision.py @@ -1,9 +1,18 @@ +import os import sys +from datetime import datetime +import cv2 from PyQt5.QtCore import pyqtSignal from PyQt5.QtGui import QImage, QPixmap from ui.test_test import Test_Test +TMP_DIR = "tmp" +ARCHIVE_DIR = "data/vision_saves" +INFERENCE_INPUT_W = 256 +INFERENCE_INPUT_H = 256 + + class Test_Vision(Test_Test): request_frame = pyqtSignal() @@ -27,6 +36,7 @@ class Test_Vision(Test_Test): self.components["vision"].resume() self.components["galaxy_camera"].resume() self.request_frame.emit() # request first frame + self.save_b.clicked.connect(lambda: self.save_frame_all(position="tmp")) def stop(self): # disable camera-vision loop @@ -98,3 +108,26 @@ class Test_Vision(Test_Test): self.last.pop("render", None) super().emit_ko() self.components["neo_pixels"].set_all_pixel_color("#000000") + + def save_frame_detection(self, position=None, date=None): + if date is None: + date = datetime.now() + if position == "archive": + save_dir = ARCHIVE_DIR + elif position == "tmp": + save_dir = TMP_DIR + elif position is None: + save_dir = self.config["image saver"]["location"] + else: + save_dir = position + save_dir += "/img_archive" + save_dir += f"/{date.strftime('%Y')}/{date.strftime('%m')}" + self.remove_older_images_if_needed() + if not os.path.exists(save_dir): + os.makedirs(save_dir, exist_ok=True) + filename = f"{save_dir}/{date.strftime('%Y-%m-%d_%H_%M_%S')}_{self.config['image saver']['suffix']}.png" + print(f"saving {filename}") + image_np_bgr = cv2.cvtColor(self.detection_image, cv2.COLOR_RGB2BGR) + cv2.imwrite(filename, image_np_bgr) + return filename +