[IMP] abstract communication with shuttle

This commit is contained in:
Alexandre Fayolle
2019-11-04 15:03:03 +01:00
committed by Guewen Baconnier
parent fc995a9410
commit 82e5c5b087
5 changed files with 164 additions and 14 deletions

View File

@@ -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

View File

@@ -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)])

View File

@@ -58,12 +58,19 @@
<field name="model">vertical.lift.shuttle</field>
<field name="arch" type="xml">
<form string="Operations">
<group>
<field name="name"/>
<field name="mode"/>
<field name="location_id"/>
<field name="hardware"/>
</group>
<group name="main">
<group name="left">
<field name="name"/>
<field name="mode"/>
<field name="location_id"/>
<field name="hardware"/>
</group>
<group string="Network" name="network">
<field name="server"/>
<field name="port"/>
<field name="use_tls"/>
</group>
</group>
</form>
</field>
</record>

View File

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

View File

@@ -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"