diff --git a/stock_vertical_lift_kardex/__manifest__.py b/stock_vertical_lift_kardex/__manifest__.py index 35670055d..8ccd05222 100644 --- a/stock_vertical_lift_kardex/__manifest__.py +++ b/stock_vertical_lift_kardex/__manifest__.py @@ -11,7 +11,8 @@ 'stock_vertical_lift', ], 'website': 'https://www.camptocamp.com', - 'data': [], + 'data': [ + ], 'installable': True, 'development_status': 'Alpha', } diff --git a/stock_vertical_lift_kardex/models/__init__.py b/stock_vertical_lift_kardex/models/__init__.py index 51a3830f6..5a191bac8 100644 --- a/stock_vertical_lift_kardex/models/__init__.py +++ b/stock_vertical_lift_kardex/models/__init__.py @@ -1,2 +1,3 @@ from . import stock_location from . import vertical_lift_shuttle + diff --git a/stock_vertical_lift_kardex/models/stock_location.py b/stock_vertical_lift_kardex/models/stock_location.py index e307aa21c..39d66fc28 100644 --- a/stock_vertical_lift_kardex/models/stock_location.py +++ b/stock_vertical_lift_kardex/models/stock_location.py @@ -28,10 +28,10 @@ class StockLocation(models.Model): x, y = '', '' subst = { 'code': code, - 'hostId': 'odoo', + 'hostId': self.env['ir.sequence'].next_by_code('vertical.lift.command'), 'addr': shuttle.name, - 'carrier': self.name, - 'carrierNext': '', + 'carrier': self.level, + 'carrierNext': '0', 'x': x, 'y': y, 'boxType': '', @@ -40,7 +40,7 @@ class StockLocation(models.Model): 'part': '', 'desc': '', } - payload = message_template.format(subst) + payload = message_template.format(**subst) return payload.encode('iso-8859-1', 'replace') def _hardware_vertical_lift_tray_payload(self, cell_location=None): @@ -83,4 +83,6 @@ 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_payload(cell_location=cell_location) + else: + payload = super()._hardware_vertical_lift_tray_payload(cell_location=cell_location) + return payload diff --git a/stock_vertical_lift_kardex/models/vertical_lift_shuttle.py b/stock_vertical_lift_kardex/models/vertical_lift_shuttle.py index 67dc1131d..256439d35 100644 --- a/stock_vertical_lift_kardex/models/vertical_lift_shuttle.py +++ b/stock_vertical_lift_kardex/models/vertical_lift_shuttle.py @@ -4,6 +4,24 @@ from odoo import api, models +JMIF_STATUS = { + 0: 'success', + 101: 'common error', + 102: 'sequence number invalid', + 103: 'machine busy', + 104: 'timeout', + 105: 'max retry reached', + 106: 'carrier in use or undefined', + 107: 'cancelled', + 108: 'invalid user input data', + 201: 'request accepted and queued', + 202: 'request processing started / request active', + 203: 'carrier arrived, maybe overwritten by code 0', + 301: 'AO occupied with other try on move back (store / put)', + 302: 'AO occupied with other try on fetch (pick)', +} + + class VerticalLiftShuttle(models.Model): _inherit = 'vertical.lift.shuttle' @@ -13,19 +31,33 @@ class VerticalLiftShuttle(models.Model): 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') + def _check_server_response(self, command): + response = command.answer code, sep, remaining = response.partition('|') - return code == "0" + code = int(code) + if code == 0: + return True + elif 1 <= code <= 99: + command.error = 'interface error %d' % code + return False + elif code in JMIF_STATUS and code < 200: + command.error = '%d: %s' % (code, JMIF_STATUS[code]) + return False + elif code in JMIF_STATUS and code < 300: + command.error = '%d: %s' % (code, JMIF_STATUS[code]) + return True + elif code in JMIF_STATUS: + command.error = '%d: %s' % (code, JMIF_STATUS[code]) + elif 501 <= code <= 999: + command.error = '%d: %s' % (code, 'MM260 Error') + elif 1000 <= code <= 32767: + command.error = '%d: %s' % ( + code, 'C2000TCP/C3000CGI machine error' + ) + elif 0xFF0 <= code == 0xFFF: + command.error = '%x: %s' % ( + code, 'C3000CGI machine error (global short)' + ) + elif 0xFFF < code: + command.error = '%x: %s' % (code, 'C3000CGI machine error (long)') + return False diff --git a/stock_vertical_lift_kardex/proxy/kardex-proxy.py b/stock_vertical_lift_kardex/proxy/kardex-proxy.py new file mode 100644 index 000000000..f568c5a91 --- /dev/null +++ b/stock_vertical_lift_kardex/proxy/kardex-proxy.py @@ -0,0 +1,175 @@ +#!/usr/bin/python3 +import argparse +import asyncio +import logging +import os +import ssl +import time + +import aiohttp + +_logger = logging.getLogger(__name__) + + +class KardexProxyProtocol(asyncio.Protocol): + def __init__(self, loop, queue, args): + _logger.info("Proxy created") + self.transport = None + self.buffer = b"" + self.queue = queue + self.loop = loop + self.args = args + + def connection_made(self, transport): + _logger.info("Proxy incoming cnx") + self.transport = transport + self.buffer = b"" + + def data_received(self, data): + self.buffer += data + _logger.info("Proxy: received %s", data) + if len(self.buffer) > 65535: + # prevent buffer overflow + self.transport.close() + + def eof_received(self): + _logger.info("Proxy: received EOF") + if self.buffer[-1] != b"\n": + # bad format -> close + self.transport.close() + data = ( + self.buffer.replace(b"\r\n", b"\n") + .replace(b"\n", b"\r\n") + .decode("iso-8859-1", "replace") + ) + self.loop.create_task(self.queue.put(data)) + self.buffer = b"" + + def connection_lost(self, exc): + self.transport = None + self.buffer = b"" + + +class KardexClientProtocol(asyncio.Protocol): + def __init__(self, loop, queue, args): + _logger.info("started kardex client") + self.loop = loop + self.queue = queue + self.args = args + self.transport = None + self.buffer = b"" + + def connection_made(self, transport): + self.transport = transport + _logger.info("connected to kardex server %r", transport) + + async def keepalive(self): + while True: + t = int(time.time()) + msg = "61|ping%d|SH1-1|0|0||||||||\r\n" % t + await self.send_message(msg) + await asyncio.sleep(20) + + async def send_message(self, message): + _logger.info("SEND %r", message) + message = message.encode("iso-8859-1") + self.transport.write(message) + + async def process_queue(self): + while True: + message = await self.queue.get() + await self.send_message(message) + + def data_received(self, data): + data = data.replace(b"\0", b"") + _logger.info("RECV %s", data) + self.buffer += data + if b"\r\n" in self.buffer: + msg, sep, rem = self.buffer.partition(b"\r\n") + self.buffer = rem + msg = msg.decode('iso-8859-1', 'replace').strip() + if msg.startswith('0|ping'): + _logger.info('ping ok') + else: + _logger.info('notify odoo: %s', msg) + self.loop.create_task(self.notify_odoo(msg)) + + def connection_lost(self, exc): + self.loop.stop() + + async def notify_odoo(self, msg): + url = self.args.odoo_url + "/vertical-lift" + async with aiohttp.ClientSession() as session: + params = {'answer': msg, 'secret': self.args.secret} + async with session.post(url, data=params) as resp: + resp_text = await resp.text() + _logger.info( + 'Reponse from Odoo: %s %s', resp.status, resp_text + ) + + +def main(args, ssl_context=None): + logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" + ) + loop = asyncio.get_event_loop() + queue = asyncio.Queue(loop=loop) + # create the main server + coro = loop.create_server( + lambda: KardexProxyProtocol(loop, queue, args), + host=args.host, + port=args.port + ) + loop.run_until_complete(coro) + + # create the connection to the JMIF client + if args.kardex_use_tls: + if ssl_context is None: + ssl_context = ssl.create_default_context() + else: + ssl_context = None + coro = loop.create_connection( + lambda: KardexClientProtocol(loop, queue, args), + host=args.kardex_host, + port=args.kardex_port, + ssl=ssl_context, + ) + transport, client = loop.run_until_complete(coro) + loop.create_task(client.keepalive()) + loop.create_task(client.process_queue()) + loop.run_forever() + loop.close() + + +def make_parser(): + listen_address = os.environ.get("INTERFACE", "0.0.0.0") + listen_port = int(os.environ.get("PORT", "7654")) + secret = os.environ.get("ODOO_CALLBACK_SECRET", "") + odoo_url = os.environ.get("ODOO_URL", "http://localhost:8069") + odoo_db = os.environ.get("ODOO_DB", "odoodb") + kardex_host = os.environ.get("KARDEX_HOST", "kardex") + kardex_port = int(os.environ.get("KARDEX_PORT", "9600")) + kardex_use_tls = ( + False + if os.environ.get("KARDEX_TLS", "") in ("", "0", "False", "FALSE") + else True + ) + parser = argparse.ArgumentParser() + arguments = [ + ("--host", listen_address, str), + ("--port", listen_port, int), + ("--odoo-url", odoo_url, str), + ("--odoo-db", odoo_db, str), + ("--secret", secret, str), + ("--kardex-host", kardex_host, str), + ("--kardex-port", kardex_port, str), + ("--kardex-use-tls", kardex_use_tls, bool), + ] + for name, default, type_ in arguments: + parser.add_argument(name, default=default, action="store", type=type_) + return parser + +if __name__ == "__main__": + parser = make_parser() + args = parser.parse_args() + main(args) diff --git a/stock_vertical_lift_kardex/proxy/requirements.txt b/stock_vertical_lift_kardex/proxy/requirements.txt new file mode 100644 index 000000000..ee4ba4f3d --- /dev/null +++ b/stock_vertical_lift_kardex/proxy/requirements.txt @@ -0,0 +1 @@ +aiohttp diff --git a/stock_vertical_lift_kardex/proxy/test.py b/stock_vertical_lift_kardex/proxy/test.py new file mode 100644 index 000000000..5b6ad5188 --- /dev/null +++ b/stock_vertical_lift_kardex/proxy/test.py @@ -0,0 +1,102 @@ +import socket +import asyncio +import logging +import time + +_logger = logging.getLogger('kardex.proxy') +logging.basicConfig(level=logging.DEBUG) + + +class KardexProxyProtocol(asyncio.Protocol): + def __init__(self, loop, queue): + _logger.info('Proxy created') + self.transport = None + self.buffer = b'' + self.queue = queue + self.loop = loop + + def connection_made(self, transport): + _logger.info('Proxy incoming cnx') + self.transport = transport + self.buffer = b'' + + def data_received(self, data): + self.buffer += data + _logger.info('Proxy: received %s', data) + if len(self.buffer) > 65535: + # prevent buffer overflow + self.transport.close() + + def eof_received(self): + _logger.info('Proxy: received EOF') + if self.buffer[-1] != b'\n': + # bad format -> close + self.transport.close() + data = self.buffer.replace(b'\r\n', b'\n').replace(b'\n', b'\r\n').decode('iso-8859-1', 'replace') + task = self.loop.create_task(self.queue.put(data)) + self.buffer = b'' + print('toto', task) + + def connection_lost(self, exc): + self.transport = None + self.buffer = b'' + + +class KardexClientProtocol(asyncio.Protocol): + def __init__(self, loop, queue): + _logger.info('started kardex client') + self.loop = loop + self.queue = queue + self.transport = None + self.buffer = b'' + + def connection_made(self, transport): + self.transport = transport + _logger.info('connected to kardex server %r', transport) + + async def keepalive(self): + while True: + t = int(time.time()) + msg = '61|ping%d|SH1-1|0|0||||||||\r\n' % t + await self.send_message(msg) + await asyncio.sleep(5) + + async def send_message(self, message): + _logger.info('SEND %s', message) + message = message.encode('iso-8859-1').ljust(1024, b'\0') + self.transport.write(message) + + async def process_queue(self): + while True: + message = await self.queue.get() + await self.send_message(message) + + def data_received(self, data): + data = data.replace(b'\0', b'') + _logger.info('RECV %s', data) + self.buffer += data + + def connection_lost(self, exc): + self.loop.stop() + + +if __name__ == '__main__': + _logger.info('starting') + loop = asyncio.get_event_loop() + loop.set_debug(1) + queue = asyncio.Queue(loop=loop) + coro = loop.create_server( + lambda: KardexProxyProtocol(loop, queue), + port=3000, + family=socket.AF_INET + ) + server = loop.run_until_complete(coro) + coro = loop.create_connection(lambda: KardexClientProtocol(loop, queue), + 'localhost', 9600) + transport, client = loop.run_until_complete(coro) + print('%r' % transport) + loop.create_task(client.keepalive()) + loop.create_task(client.process_queue()) + _logger.info('run loop') + loop.run_forever() + loop.close() diff --git a/stock_vertical_lift_kardex/requirements.txt b/stock_vertical_lift_kardex/requirements.txt new file mode 100644 index 000000000..ee4ba4f3d --- /dev/null +++ b/stock_vertical_lift_kardex/requirements.txt @@ -0,0 +1 @@ +aiohttp