From 82e5c5b087a48cc6645df62935faa5649b690a1c Mon Sep 17 00:00:00 2001 From: Alexandre Fayolle Date: Mon, 4 Nov 2019 15:03:03 +0100 Subject: [PATCH] [IMP] abstract communication with shuttle --- stock_vertical_lift/models/stock_location.py | 13 ++- .../models/vertical_lift_shuttle.py | 91 +++++++++++++++++++ .../views/vertical_lift_shuttle_views.xml | 19 ++-- .../models/stock_location.py | 38 +++++++- .../models/vertical_lift_shuttle.py | 17 ++++ 5 files changed, 164 insertions(+), 14 deletions(-) diff --git a/stock_vertical_lift/models/stock_location.py b/stock_vertical_lift/models/stock_location.py index dd4573228..14c1977ce 100644 --- a/stock_vertical_lift/models/stock_location.py +++ b/stock_vertical_lift/models/stock_location.py @@ -71,7 +71,12 @@ class StockLocation(models.Model): location.vertical_lift_shuttle_id = shuttle def _hardware_vertical_lift_tray(self, cell_location=None): - """Send instructions to the vertical lift hardware + payload = self._hardware_vertical_lift_tray_payload(cell_location) + res = self.vertical_lift_shuttle_id._hardware_send_message(payload) + return res + + def _hardware_vertical_lift_tray_payload(self, cell_location=None): + """Prepare the message to be sent to the vertical lift hardware Private method, this is where the implementation actually happens. Addons can add their instructions based on the hardware used for @@ -120,9 +125,9 @@ class StockLocation(models.Model): from_left, from_bottom, ) - self.env.user.notify_info( - message=message, title=_("Lift Simulation") - ) + return message + else: + return super()._hardware_vertical_lift_tray_payload(cell_location) def fetch_vertical_lift_tray(self, cell_location=None): """Send instructions to the vertical lift hardware diff --git a/stock_vertical_lift/models/vertical_lift_shuttle.py b/stock_vertical_lift/models/vertical_lift_shuttle.py index d3271acb2..a7e0f35c5 100644 --- a/stock_vertical_lift/models/vertical_lift_shuttle.py +++ b/stock_vertical_lift/models/vertical_lift_shuttle.py @@ -1,8 +1,13 @@ # Copyright 2019 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging +import socket +import ssl from odoo import _, api, fields, models +_logger = logging.getLogger(__name__) + class VerticalLiftShuttle(models.Model): _name = "vertical.lift.shuttle" @@ -26,6 +31,13 @@ class VerticalLiftShuttle(models.Model): hardware = fields.Selection( selection="_selection_hardware", default="simulation", required=True ) + server = fields.Char(help="hostname or IP address of the server") + port = fields.Integer( + help="network port of the server on which to send the message" + ) + use_tls = fields.Boolean( + help="set this if the server expects TLS wrapped communication" + ) _sql_constraints = [ ( @@ -64,6 +76,85 @@ class VerticalLiftShuttle(models.Model): ), } + def _hardware_send_message(self, payload): + """default implementation for message sending + + If in hardware is 'simulation' then display a simple message. + Otherwise defaults to connecting to server:port using a TCP socket + (optionnally wrapped with TLS) and sending the payload, then waiting + for a response and disconnecting. + + :param payload: a bytes object containing the payload + + """ + self.ensure_one() + _logger.info('send %r', payload) + if self.hardware == "simulation": + self.env.user.notify_info(message=payload, + title=_("Lift Simulation")) + return True + else: + conn = self._hardware_get_server_connection() + try: + offset = 0 + while True: + size = conn.send(payload[offset:]) + offset += size + if offset >= len(payload) or not size: + break + response = self._hardware_recv_response(conn) + _logger.info('recv %r', response) + return self._check_server_response(payload, response) + finally: + self._hardware_release_server_connection(conn) + + def _hardware_recv_response(self, conn): + """Default implementation expects the remote server to close() + the socket after sending the reponse. + Override to match the protocol implemented by the hardware. + + :param conn: a socket connected to the server + :return: the response sent by the server, as a bytes object + """ + response = b'' + chunk = True + while chunk: + chunk = conn.recv(1024) + response += chunk + return response + + def _check_server_response(self, payload, response): + """Use this to check if the response is a success or a failure + + :param payload: the payload sent + :param response: the response received + :return: True if the response is a succes, False otherwise + """ + return True + + def _hardware_release_server_connection(self, conn): + conn.close() + + def _hardware_get_server_connection(self): + """This implementation will yield a new connection to the server + and close() it when exiting the context. + Override to match the communication protocol of your hardware""" + conn = socket.create_connection((self.server, self.port)) + if self.use_tls: + ctx = ssl.create_default_context() + self._hardware_update_tls_context(ctx) + conntls = ctx.wrap_socket(conn, server_hostname=self.server) + return conntls + else: + return conn + + def _hardware_update_tls_context(self, context): + """Update the TLS context, e.g. to add a client certificate. + + This method does nothing, override to match your communication + protocol.""" + pass + def _operation_for_mode(self): model = self._model_for_mode[self.mode] record = self.env[model].search([("shuttle_id", "=", self.id)]) diff --git a/stock_vertical_lift/views/vertical_lift_shuttle_views.xml b/stock_vertical_lift/views/vertical_lift_shuttle_views.xml index c8d7cb639..0a33ad98d 100644 --- a/stock_vertical_lift/views/vertical_lift_shuttle_views.xml +++ b/stock_vertical_lift/views/vertical_lift_shuttle_views.xml @@ -58,12 +58,19 @@ vertical.lift.shuttle
- - - - - - + + + + + + + + + + + + +
diff --git a/stock_vertical_lift_kardex/models/stock_location.py b/stock_vertical_lift_kardex/models/stock_location.py index d1098d59a..e307aa21c 100644 --- a/stock_vertical_lift_kardex/models/stock_location.py +++ b/stock_vertical_lift_kardex/models/stock_location.py @@ -11,10 +11,40 @@ class StockLocation(models.Model): _inherit = 'stock.location' def _hardware_kardex_prepare_payload(self, cell_location=None): - return "" + message_template = ("{code}|{hostId}|{addr}|{carrier}|{carrierNext}|" + "{x}|{y}|{boxType}|{Q}|{order}|{part}|{desc}|\r\n") + shuttle = self.vertical_lift_shuttle_id + if shuttle.mode == "pick": + code = "1" + elif shuttle.mode == "put": + code = "2" + elif shuttle.mode == "inventory": + code = "5" + else: + code = "61" # ping + if cell_location: + x, y = cell_location.tray_cell_center_position() + else: + x, y = '', '' + subst = { + 'code': code, + 'hostId': 'odoo', + 'addr': shuttle.name, + 'carrier': self.name, + 'carrierNext': '', + 'x': x, + 'y': y, + 'boxType': '', + 'Q': '', + 'order': '', + 'part': '', + 'desc': '', + } + payload = message_template.format(subst) + return payload.encode('iso-8859-1', 'replace') - def _hardware_vertical_lift_tray(self, cell_location=None): - """Send instructions to the vertical lift hardware + def _hardware_vertical_lift_tray_payload(self, cell_location=None): + """Prepare the message to be sent to the vertical lift hardware Private method, this is where the implementation actually happens. Addons can add their instructions based on the hardware used for @@ -53,4 +83,4 @@ class StockLocation(models.Model): payload = self._hardware_kardex_prepare_payload() _logger.debug("Sending to kardex: {}", payload) # TODO implement the communication with kardex - super()._hardware_vertical_lift_tray(cell_location=cell_location) + super()._hardware_vertical_lift_tray_payload(cell_location=cell_location) diff --git a/stock_vertical_lift_kardex/models/vertical_lift_shuttle.py b/stock_vertical_lift_kardex/models/vertical_lift_shuttle.py index cfd3c74b9..67dc1131d 100644 --- a/stock_vertical_lift_kardex/models/vertical_lift_shuttle.py +++ b/stock_vertical_lift_kardex/models/vertical_lift_shuttle.py @@ -12,3 +12,20 @@ class VerticalLiftShuttle(models.Model): values = super()._selection_hardware() values += [('kardex', 'Kardex')] return values + + def _hardware_recv_response(self, conn): + # the implementation uses messages delimited with \r\n + response = b'' + chunk = True + while chunk: + chunk = conn.recv(1) + response += chunk + if response.endswith(b'\r\n'): + break + return response + + def _check_server_response(self, payload, response): + payload = payload.decode('iso-8859-1') + response = response.decode('iso-8859-1') + code, sep, remaining = response.partition('|') + return code == "0"