├── src └── plc_data │ ├── __init__.py │ ├── __main__.py │ ├── decoder │ ├── __init__.py │ ├── jtekt_decoder_0x26.py │ ├── jtekt_decoder_0x22.py │ └── jtekt_decoder.py │ ├── tcp_client.py │ ├── core.py │ ├── tcp_packet.py │ └── send_command.py ├── config ├── command_list.json └── trigger_list.json ├── setup.py ├── README.md ├── Dockerfile ├── docker-build.sh └── LICENSE /src/plc_data/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Copyright (c) 2019-2020 Latona. All rights reserved. 3 | 4 | from .core import main 5 | 6 | -------------------------------------------------------------------------------- /src/plc_data/__main__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Copyright (c) 2019-2020 Latona. All rights reserved. 3 | 4 | from . import main 5 | 6 | if __name__ == "__main__": 7 | main() 8 | 9 | -------------------------------------------------------------------------------- /src/plc_data/decoder/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding: utf-8 3 | 4 | # Copyright (c) 2019-2020 Latona. All rights reserved. 5 | 6 | from .jtekt_decoder import JtektPlcData, JtektPlcDataList 7 | 8 | -------------------------------------------------------------------------------- /config/command_list.json: -------------------------------------------------------------------------------- 1 | { 2 | "command": [ 3 | { 4 | "command": "22", 5 | "detail": "register word many", 6 | "arrayNo": [ 7 | "1100", 8 | "1101", 9 | "1110", 10 | "1111" 11 | ], 12 | "interval": 1000, 13 | "expire_time": 60 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | # Copyright (c) 2019-2020 Latona. All rights reserved. 4 | 5 | from setuptools import setup, find_packages 6 | 7 | setup( 8 | name="plc_data", 9 | version="0.0.1", 10 | author="Latona", 11 | packages=find_packages("./src"), 12 | package_dir={"":"src"}, 13 | install_requires=[ 14 | "setuptools" 15 | ], 16 | tests_require=[] 17 | ) 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # control-jtekt-plc-r-kube 2 | ジェイテクト製のPLCのレジスタに登録されたメッセージを読み込むマイクロサービスです。 3 | メッセージの送受信方法およびフォーマットはバイナリ形式です。 4 | 5 | 6 | ## 1.動作環境 7 | * OS: Linux 8 | * CPU:ARM/AMD/Intel 9 | 10 | 11 | ## 2.対応している接続方式 12 | * Ethernet接続 13 | 14 | 15 | ## 3.IO 16 | 17 | ### Input 18 | PLCのレジスタへの読み取りを定期実行し、バイナリで構成された電文を取得します。 19 | 20 | ### Output 21 | 電文の内容を元にkanban(RabbitMQ)へデータの投入を行います。 22 | 23 | 24 | ## 4.関連するマイクロサービス 25 | control-jtekt-plc-w-kube -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM latonaio/l4t:latest 2 | 3 | ENV POSITION=Runtime \ 4 | SERVICE=control-jtekt-plc-r \ 5 | AION_HOME="/var/lib/aion" \ 6 | CONFIG="${AION_HOME}/Data/${SERVICE}_1" 7 | 8 | RUN mkdir ${AION_HOME} 9 | RUN mkdir -p ${CONFIG} 10 | 11 | # Setup Directoties 12 | RUN mkdir -p ${AION_HOME}/$POSITION/$SERVICE 13 | WORKDIR ${AION_HOME}/$POSITION/$SERVICE/ 14 | 15 | ADD . . 16 | #RUN cp ./data/command_list.json ${CONFIG} && cp ./data/trigger_list.json ${CONFIG} 17 | RUN python3 setup.py install 18 | CMD ["python3", "-m", "plc_data"] 19 | # ENTRYPOINT ["/bin/sh", "-c", "while :; do sleep 10; done"] 20 | -------------------------------------------------------------------------------- /docker-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PUSH=$1 4 | DATE="$(date "+%Y%m%d%H%M")" 5 | REPOSITORY_PREFIX="latonaio" 6 | SERVICE_NAME="control-jtekt-plc-r" 7 | 8 | DOCKER_BUILDKIT=1 docker build --progress=plain -t ${SERVICE_NAME}:"${DATE}" . 9 | 10 | # tagging 11 | docker tag ${SERVICE_NAME}:"${DATE}" ${SERVICE_NAME}:latest 12 | docker tag ${SERVICE_NAME}:"${DATE}" ${REPOSITORY_PREFIX}/${SERVICE_NAME}:"${DATE}" 13 | docker tag ${REPOSITORY_PREFIX}/${SERVICE_NAME}:"${DATE}" ${REPOSITORY_PREFIX}/${SERVICE_NAME}:latest 14 | 15 | if [[ $PUSH == "push" ]]; then 16 | docker push ${REPOSITORY_PREFIX}/${SERVICE_NAME}:"${DATE}" 17 | docker push ${REPOSITORY_PREFIX}/${SERVICE_NAME}:latest 18 | fi 19 | -------------------------------------------------------------------------------- /src/plc_data/decoder/jtekt_decoder_0x26.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding: utf-8 3 | 4 | # Copyright (c) 2019-2020 Latona. All rights reserved. 5 | 6 | from .jtekt_decoder import JtektPlcDataMulti 7 | 8 | ADDR_SIZE = 2 9 | DATA_SIZE = 1 10 | 11 | class PlcData(JtektPlcDataMulti): 12 | 13 | def __init__(self, req, res): 14 | super().__init__(req, res) 15 | 16 | def to_array(self): 17 | addr_num = int(len(self.head_binary)/ADDR_SIZE) 18 | array_list = [] 19 | 20 | for i in range(addr_num): 21 | array_no = self.head_binary[i*ADDR_SIZE:(i+1)*ADDR_SIZE] 22 | bit_value = self.binary[i*DATA_SIZE:(i+1)*DATA_SIZE] 23 | array_list.append({ 24 | "ArrayNo": array_no[::-1].hex(), 25 | "Bit": int(bit_value.hex(), 16), 26 | }) 27 | return array_list 28 | 29 | -------------------------------------------------------------------------------- /src/plc_data/decoder/jtekt_decoder_0x22.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding: utf-8 3 | 4 | # Copyright (c) 2019-2020 Latona. All rights reserved. 5 | 6 | from .jtekt_decoder import JtektPlcDataMulti 7 | 8 | ADDR_SIZE = 2 9 | DATA_SIZE = 2 10 | 11 | class PlcData(JtektPlcDataMulti): 12 | 13 | def __init__(self, req, res): 14 | super().__init__(req, res) 15 | 16 | def to_array(self): 17 | addr_num = int(len(self.head_binary)/ADDR_SIZE) 18 | array_list = [] 19 | 20 | for i in range(addr_num): 21 | array_no = self.head_binary[i*ADDR_SIZE:(i+1)*ADDR_SIZE][::-1] 22 | word_value = self.binary[i*DATA_SIZE:(i+1)*DATA_SIZE][::-1] 23 | array_list.append({ 24 | "ArrayNo": array_no.hex(), 25 | "Bit": [int(s) for s in format(int(word_value.hex(), 16), '016b')], 26 | "Word": int(word_value.hex(), 16), 27 | }) 28 | return array_list 29 | 30 | -------------------------------------------------------------------------------- /src/plc_data/tcp_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding: utf-8 3 | 4 | # Copyright (c) 2019-2020 Latona. All rights reserved. 5 | 6 | import asyncio 7 | from aion.logger import lprint 8 | 9 | class TCPClient(asyncio.Protocol): 10 | def __init__(self, send, on_response): 11 | self.send = send 12 | self.command = send.command.hex() 13 | self.array_no = send.array_no.hex() 14 | self.on_response = on_response 15 | 16 | def connection_made(self, transport): 17 | lprint("[client] create connection and send packet") 18 | pkt = self.send.get_packet() 19 | lprint(pkt.hex()) 20 | transport.write(pkt) 21 | 22 | def data_received(self, data): 23 | lprint(data.hex()) 24 | lprint(f"[client] get response: (command:{self.command})") 25 | self.on_response.set_result((self.send, data)) 26 | 27 | def error_received(self, exc): 28 | lprint(f'[client] Error received ({self.command}):') 29 | 30 | def connection_lost(self, exc): 31 | lprint(f"[client] Connection closed ({self.command})") 32 | 33 | -------------------------------------------------------------------------------- /src/plc_data/core.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding: utf-8 3 | 4 | # Copyright (c) 2019-2020 Latona. All rights reserved. 5 | 6 | import asyncio 7 | import os 8 | from time import sleep 9 | from aion.microservice import main_decorator, Options 10 | from . import send_command 11 | 12 | SERVICE_NAME = "control-jtekt-plc-r" 13 | ADDRESS = os.environ.get("PLC_ADDRESS", "192.168.1.2") 14 | PORT = int(os.environ.get("PLC_PORT", 1025)) 15 | AION_HOME = os.environ.get("AION_HOME", "/var/lib/aion/") 16 | JSON_PATH = os.path.join( 17 | AION_HOME, 18 | "Data", 19 | SERVICE_NAME+"_1", 20 | "config/command_list.json") 21 | TRIGGER_PATH = os.path.join( 22 | AION_HOME, 23 | "Data", 24 | SERVICE_NAME+"_1", 25 | "config/trigger_list.json") 26 | 27 | 28 | @main_decorator(SERVICE_NAME) 29 | def main(opt: Options): 30 | conn = opt.get_conn() 31 | num = opt.get_number() 32 | kanban = conn.set_kanban(SERVICE_NAME, num) 33 | loop = asyncio.get_event_loop() 34 | y = send_command.JtektPlcCommunicator( 35 | JSON_PATH, ADDRESS, PORT, loop, __file__, TRIGGER_PATH 36 | ) 37 | y.start_to_send(conn) 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Latona, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /config/trigger_list.json: -------------------------------------------------------------------------------- 1 | { 2 | "command": [ 3 | { 4 | "trigger": { 5 | "command": "26", 6 | "arrayNo": "1000", 7 | "elementName": "Bit", 8 | "conditions": "==1", 9 | "always": 0 10 | }, 11 | "connectionKey": "car-number" 12 | }, 13 | { 14 | "trigger": { 15 | "command": "26", 16 | "arrayNo": "1001", 17 | "elementName": "Bit", 18 | "conditions": "==1", 19 | "always": 0 20 | }, 21 | "connectionKey": "car-number" 22 | }, 23 | { 24 | "trigger": { 25 | "command": "26", 26 | "arrayNo": "1002", 27 | "elementName": "Bit", 28 | "conditions": "==1", 29 | "always": 0 30 | }, 31 | "connectionKey": "car-number" 32 | }, 33 | { 34 | "trigger": { 35 | "command": "26", 36 | "arrayNo": "1003", 37 | "elementName": "Bit", 38 | "conditions": "==1", 39 | "always": 0 40 | }, 41 | "connectionKey": "car-number" 42 | }, 43 | { 44 | "trigger": { 45 | "command": "26", 46 | "arrayNo": "1000", 47 | "elementName": "Bit", 48 | "conditions": "==0", 49 | "always": 0 50 | }, 51 | "connectionKey": "car-number" 52 | }, 53 | { 54 | "trigger": { 55 | "command": "26", 56 | "arrayNo": "1001", 57 | "elementName": "Bit", 58 | "conditions": "==0", 59 | "always": 0 60 | }, 61 | "connectionKey": "car-number" 62 | }, 63 | { 64 | "trigger": { 65 | "command": "26", 66 | "arrayNo": "1002", 67 | "elementName": "Bit", 68 | "conditions": "==0", 69 | "always": 0 70 | }, 71 | "connectionKey": "car-number" 72 | }, 73 | { 74 | "trigger": { 75 | "command": "26", 76 | "arrayNo": "1003", 77 | "elementName": "Bit", 78 | "conditions": "==0", 79 | "always": 0 80 | }, 81 | "connectionKey": "car-number" 82 | }, 83 | { 84 | "trigger": { 85 | "command": "26", 86 | "arrayNo": "1004", 87 | "elementName": "Bit", 88 | "conditions": "==1", 89 | "always": 0 90 | }, 91 | "connectionKey": "inspection-start-a" 92 | }, 93 | { 94 | "trigger": { 95 | "command": "26", 96 | "arrayNo": "1005", 97 | "elementName": "Bit", 98 | "conditions": "==1", 99 | "always": 0 100 | }, 101 | "connectionKey": "inspection-start-b" 102 | } 103 | ] 104 | } 105 | -------------------------------------------------------------------------------- /src/plc_data/tcp_packet.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding: utf-8 3 | 4 | # Copyright (c) 2019-2020 Latona. All rights reserved. 5 | 6 | def set_length(val, length): 7 | if len(val) > length: 8 | raise ValueError( 9 | "value is too large (set length: %d, real length: %d)", len(val), length) 10 | return val.ljust(length, b'\00') 11 | 12 | 13 | class JtektTcpPacket: 14 | def __init__(self): 15 | self.binary = bytearray(5) 16 | self.header_size = 5 17 | self.data_size = 2 18 | 19 | # set main header 20 | index = 0 21 | index = self.define_property("direction", index, 1, 0) 22 | index = self.define_property("status", index, 1, 0) 23 | index = self.define_property("block_num", index, 2, 0) 24 | index = self.define_property("command", index, 1, 0) 25 | 26 | def set_binary(self, binary): 27 | self.binary = binary 28 | self.data_size = len(binary) - self.header_size 29 | return True 30 | 31 | def set_body(self, binary): 32 | self.binary += binary 33 | self.data_size = len(binary) 34 | return True 35 | 36 | def getter(self, start, length): 37 | end = start + length 38 | if len(self.binary) >= end and len(self.binary) >= start > end: 39 | raise IndexError("out of range") 40 | return bytes(self.binary[start:end])[::-1] 41 | 42 | def setter(self, start, length, val): 43 | end = start + length 44 | if isinstance(val, int): 45 | self.binary[start:end] = set_length( 46 | val.to_bytes(length, 'little'), length) 47 | elif isinstance(val, str): 48 | self.binary[start:end] = set_length( 49 | bytes.fromhex(val), length)[::-1] 50 | elif isinstance(val, bytes): 51 | self.binary[start:end] = set_length(val, length) 52 | else: 53 | raise ValueError("cant set value") 54 | 55 | def define_property(self, name, start, length, initial_value=None): 56 | end = start + length 57 | 58 | def getter(in_self): 59 | return in_self.getter(start, length) 60 | 61 | def setter(in_self, value): 62 | return in_self.setter(start, length, value) 63 | 64 | setattr(self.__class__, name, property(getter, setter)) 65 | self.setter(start, length, initial_value) 66 | 67 | return end 68 | 69 | def get_packet(self): 70 | return bytes(self.binary) 71 | 72 | def get_data(self): 73 | if self.header_size is None or self.data_size is None: 74 | raise IndexError("cant get header size or data size") 75 | header_size = self.header_size 76 | data_size = self.data_size 77 | return self.binary[header_size:header_size + data_size] 78 | 79 | 80 | class SendPacket(JtektTcpPacket): 81 | def __init__(self): 82 | super().__init__() 83 | 84 | 85 | class RcvPacket(JtektTcpPacket): 86 | def __init__(self): 87 | super().__init__() 88 | 89 | def get_status(self): 90 | res = { 91 | "ResponseStatus": self.status.hex(), 92 | } 93 | return res 94 | -------------------------------------------------------------------------------- /src/plc_data/decoder/jtekt_decoder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding: utf-8 3 | # Copyright (c) 2019-2020 Latona. All rights reserved. 4 | 5 | from ..tcp_packet import SendPacket, RcvPacket 6 | from aion.logger import lprint 7 | 8 | class JtektPlcData: 9 | byte_order = 'little' 10 | encode = "shift_jis" 11 | data_size = 0 # ファイル上の1データのサイズ 12 | status = {} 13 | command = "" 14 | 15 | def __init__(self, req, res): 16 | if res: 17 | self.status = res.get_status() 18 | self._set_binary(res.get_data()) 19 | else: 20 | self._set_binary(bytes(self.data_size)) 21 | if req: 22 | self.array_no = int.from_bytes(req.array_no, byteorder="big") 23 | self.command = req.command.hex() 24 | 25 | def _set_binary(self, binary): 26 | self.binary = binary 27 | 28 | def _set_bytes(self, pos, value): 29 | data = bytearray(self.binary) 30 | data[pos:pos + len(value)] = value 31 | self.binary = bytes(data) 32 | 33 | def is_success(self): 34 | return True if self.status and self.status.get("ResponseStatus") == "00" else False 35 | 36 | def to_array(self): 37 | return {} 38 | 39 | def get_status(self): 40 | return {"ArrayNo": self.array_no, **self.status} 41 | 42 | def _string_decoder(self, byte): 43 | return byte.decode(self.encode).replace("\x00", "") 44 | 45 | @staticmethod 46 | def create_request(data): 47 | header_list = {} 48 | try: 49 | for array in data.get("arrayNo"): 50 | header = SendPacket() 51 | header.set_body(bytearray(2)) 52 | header.command = bytes.fromhex(data.get("command")) 53 | header.define_property("array_no", 5, 2, int(array,16)) 54 | header.block_num = 3 55 | header_list[array] = header 56 | except Exception as e: 57 | lprint("cant convert to hex: " + str(e)) 58 | lprint(traceback.format_exc()) 59 | return [] 60 | return header_list 61 | 62 | @staticmethod 63 | def create_datalist(resp_list, decoder_class): 64 | robot_data_list = [] 65 | for req, res_raw in resp_list: 66 | res = RcvPacket() 67 | res.set_binary(res_raw) 68 | robot_data_list.append(decoder_class(req, res)) 69 | return robot_data_list 70 | 71 | 72 | class JtektPlcDataMulti: 73 | byte_order = 'little' 74 | encode = "shift_jis" 75 | data_size = 0 # ファイル上の1データのサイズ 76 | status = {} 77 | command = "" 78 | 79 | def __init__(self, req, res): 80 | if res: 81 | self.status = res.get_status() 82 | self._set_binary(res.get_data()) 83 | else: 84 | self._set_binary(bytes(self.data_size)) 85 | if req: 86 | self.array_no = int.from_bytes(req.array_no, byteorder="big") 87 | self.head_binary = req.get_data() 88 | self.command = req.command.hex() 89 | 90 | def _set_binary(self, binary): 91 | self.binary = binary 92 | 93 | def _set_bytes(self, pos, value): 94 | data = bytearray(self.binary) 95 | data[pos:pos + len(value)] = value 96 | self.binary = bytes(data) 97 | 98 | def is_success(self): 99 | return True if self.status and self.status.get("ResponseStatus") == "00" else False 100 | 101 | def to_array(self): 102 | return {} 103 | 104 | def get_status(self): 105 | return {"ArrayNo": self.array_no, **self.status} 106 | 107 | def _string_decoder(self, byte): 108 | return byte.decode(self.encode).replace("\x00", "") 109 | 110 | @staticmethod 111 | def create_request(data): 112 | header_list = {} 113 | try: 114 | array_list = data.get("arrayNo") 115 | array_bytes = b'' 116 | for array in array_list: 117 | array_bytes += bytes.fromhex(array)[::-1] 118 | header = SendPacket() 119 | header.command = bytes.fromhex(data.get("command")) 120 | header.block_num = len(array_bytes)+1 121 | header.array_no = bytes.fromhex(array_list[0])[::-1] 122 | header.set_body(array_bytes) 123 | header_list[array_list[0]] = header 124 | except Exception as e: 125 | lprint("cant convert to hex: " + str(e)) 126 | lprint(traceback.format_exc()) 127 | return [] 128 | return header_list 129 | 130 | @staticmethod 131 | def create_datalist(resp_list, decoder_class): 132 | robot_data_list = [] 133 | req, res_raw = resp_list[0] 134 | res = RcvPacket() 135 | res.set_binary(res_raw) 136 | robot_data_list = decoder_class(req, res) 137 | return robot_data_list 138 | 139 | 140 | class JtektPlcDataList: 141 | def __init__(self, command, expire_time, robot_data_list): 142 | self.expire_time = expire_time 143 | self.data_list = robot_data_list 144 | if isinstance(command, bytes): 145 | command = command.hex() 146 | self.command = command 147 | #self.is_success = all([d.is_success() for d in self.data_list]) 148 | self.is_success = self.data_list.is_success() 149 | 150 | def get_header(self): 151 | return { 152 | "Command": self.command, 153 | "Result": self.is_success, 154 | "ExpireTime": self.expire_time, 155 | "BaseObjectType": "", 156 | "ComponentType": "", 157 | "MotionDeviceSystemType": "", 158 | "MotionDeviceIdentifier": "", 159 | "MotionDeviceType": "", 160 | "ComponentName": "", 161 | "Manufacturer": "", 162 | "Model": "", 163 | "DataForm": "16bit_integer", 164 | } 165 | 166 | def to_json(self): 167 | #robot_data_list = [d.to_array() if d.is_success() else d.get_status() for d in self.data_list] 168 | return {**self.get_header(), **{"RobotData": self.data_list.to_array()}} 169 | 170 | -------------------------------------------------------------------------------- /src/plc_data/send_command.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding: utf-8 3 | 4 | # Copyright (c) 2019-2020 Latona. All rights reserved. 5 | 6 | import asyncio 7 | import json 8 | import os 9 | import traceback 10 | from datetime import datetime 11 | from importlib import import_module 12 | from aion.kanban import kanban 13 | from aion.logger import lprint 14 | from .tcp_packet import SendPacket, RcvPacket 15 | from .tcp_client import TCPClient 16 | from .decoder import JtektPlcDataList 17 | 18 | 19 | BUF_SIZE = 4096 20 | DEFAULT_TIMEOUT = 1 21 | 22 | 23 | def read_config_json(json_path): 24 | try: 25 | with open(json_path, "r") as f: 26 | data = json.load(f) 27 | except FileNotFoundError as e: 28 | lprint(str(e)) 29 | return None 30 | except json.JSONDecodeError as e: 31 | lprint(str(e)) 32 | return None 33 | 34 | command = data.get("command") 35 | if command is None: 36 | lprint(f"there is no command data: {json_path}") 37 | return None 38 | 39 | return command 40 | 41 | 42 | def create_header(data): 43 | header_list = {} 44 | try: 45 | for array in data.get("arrayNo"): 46 | header = SendPacket() 47 | header.command = bytes.fromhex(data.get("command")) 48 | header.define_property("array_no", 5, 2, int(array,16)) 49 | header.block_num = 3 50 | header_list[array] = header 51 | except Exception as e: 52 | lprint("cant convert to hex: " + str(e)) 53 | lprint(traceback.format_exc()) 54 | return [] 55 | return header_list 56 | 57 | 58 | def get_all_header_list_by_json(json_path): 59 | header_list = {} 60 | conf = read_config_json(json_path) 61 | if conf is None: 62 | return {} 63 | for data in conf: 64 | command = data.get("command") 65 | dec_data_library = import_module( 66 | "src.plc_data.decoder.jtekt_decoder_0x" + command) 67 | decoder_class = dec_data_library.PlcData 68 | if data.get("expire_time") is None or data.get("interval") is None: 69 | lprint( 70 | f"there is no expire_time or interval (command:{command})") 71 | continue 72 | headers = decoder_class.create_request(data) 73 | if headers is None: 74 | lprint(f"cant get header data (command:{command})") 75 | continue 76 | header_list[command] = { 77 | "headers": headers, 78 | "interval": data.get("interval"), 79 | "expire_time": data.get("expire_time"), 80 | "decoder": decoder_class, 81 | } 82 | return header_list 83 | 84 | 85 | class JtektPlcCommunicator: 86 | def __init__(self, json_path, address, port, loop, main_path, trigger_path): 87 | self.header_list = get_all_header_list_by_json(json_path) 88 | self.send_queue = asyncio.Queue() 89 | self.rcv_queue = asyncio.Queue() 90 | self.address = address 91 | self.port = port 92 | self.loop = loop 93 | self.main_path = main_path 94 | self.trigger_path = trigger_path 95 | 96 | self.json_path = json_path 97 | self.task_list = {} 98 | self.last_file_updated = os.path.getmtime(json_path) 99 | self.data_list = [] 100 | self.timestamp = None 101 | 102 | def start_to_send(self, conn): 103 | if self.header_list is None: 104 | return False 105 | for command, header_data in self.header_list.items(): 106 | self.task_list[command] = asyncio.ensure_future( 107 | self.set_queue_by_interval(command, header_data)) 108 | #asyncio.ensure_future(self.reload_command_list()) 109 | asyncio.ensure_future(self.send_request()) 110 | self.loop.run_until_complete(self.output_status_json(conn)) 111 | 112 | async def reload_command_list(self): 113 | while True: 114 | # wait 115 | lprint("check command list file reload><><>") 116 | await asyncio.sleep(1) 117 | # check file update 118 | tmp = os.path.getmtime(self.json_path) 119 | if self.last_file_updated != tmp: 120 | lprint("command list file reload><><>") 121 | self.last_file_updated = tmp 122 | # stop old task 123 | for command, header_data in self.header_list.items(): 124 | self.task_list[command].cancel() 125 | # update header list 126 | self.header_list = get_all_header_list_by_json(self.json_path) 127 | # start new task 128 | for command, header_data in self.header_list.items(): 129 | self.task_list[command] = asyncio.ensure_future( 130 | self.set_queue_by_interval(command, header_data)) 131 | 132 | async def set_queue_by_interval(self, command, header_data): 133 | interval = header_data.get("interval") 134 | expire_time = header_data.get("expire_time") 135 | header_list = header_data.get("headers") 136 | decoder_class = header_data.get("decoder") 137 | if interval is None or not header_list: 138 | lprint("invalid input in set_queue_by_interval") 139 | return False 140 | 141 | while True: 142 | await self.send_queue.put((command, header_list, decoder_class, expire_time)) 143 | if interval == 0: 144 | break 145 | await asyncio.sleep(interval / 1000) 146 | 147 | async def send_request(self): 148 | while True: 149 | command, header_list, decoder_class, expire_time = await self.send_queue.get() 150 | if not isinstance(header_list, dict): 151 | raise TypeError("header to list") 152 | lprint(f"[client] send to robot: {command}") 153 | 154 | resp_list = [] 155 | 156 | async def get_response(wait_func): 157 | # return : request packet (SendPacket) , response packet (bytes) 158 | resp_list.append(await wait_func) 159 | 160 | # send all array no 161 | for array_no, header in header_list.items(): 162 | on_response = self.loop.create_future() 163 | try: 164 | transport, protocol = await self.loop.create_connection( 165 | lambda: TCPClient(header, on_response), self.address, self.port) 166 | except OSError as e: 167 | lprint(str(e)) 168 | continue 169 | try: 170 | await asyncio.wait_for(get_response(on_response), DEFAULT_TIMEOUT) 171 | except asyncio.TimeoutError: 172 | lprint(f"timeout to receive: {header.command.hex()}") 173 | pass 174 | finally: 175 | transport.close() 176 | # safety interval 177 | await asyncio.sleep(0.1) 178 | 179 | # set to data decoder class 180 | robot_data_list = decoder_class.create_datalist(resp_list, decoder_class) 181 | 182 | data_list = JtektPlcDataList( 183 | command, expire_time, robot_data_list) 184 | 185 | await self.rcv_queue.put((data_list, datetime.now().isoformat())) 186 | 187 | async def output_status_json(self, conn): 188 | #trigger_list = read_config_json( 189 | # self.trigger_path) if self.trigger_path else [] 190 | previous_executed = {} 191 | 192 | while True: 193 | data_list, timestamp = await self.rcv_queue.get() 194 | robot_data = data_list.to_json() 195 | 196 | metadata_sets = {} 197 | metadata_sets["RobotData"] = robot_data 198 | metadata_sets["timestamp"] = timestamp 199 | conn.output_kanban( 200 | result=True, 201 | metadata=metadata_sets 202 | ) 203 | 204 | """ 205 | # start check trigger 206 | for row in trigger_list: 207 | trigger = row.get('trigger') 208 | connection_key = row.get('connectionKey') 209 | metadata = row.get('metadata', {}) 210 | 211 | # check commnad no 212 | if robot_data.get('Command') and \ 213 | robot_data.get('Command') == trigger.get('command'): 214 | for command in robot_data.get('RobotData') \ 215 | if robot_data.get('RobotData') else []: 216 | # check array no 217 | if command.get('ArrayNo') is not None and \ 218 | command.get('ArrayNo') == int(trigger.get('arrayNo'),16): 219 | element_name = trigger.get("elementName") 220 | element_value = command.get(element_name) 221 | state_tag = "%s:%s:%s:%s" % (trigger.get('command'), 222 | trigger.get( 223 | 'arrayNo'), element_name, 224 | trigger.get("conditions")) 225 | # check conditions 226 | if str(element_value).isdecimal(): 227 | is_condition = eval(str(element_value) + trigger.get("conditions")) 228 | else: 229 | lprint("element value is not number!") 230 | is_condition = eval(str(f"'{element_value}'") + trigger.get("conditions")) 231 | 232 | lprint(f"[condition] is {str(is_condition)}") 233 | if is_condition: 234 | # check previous state 235 | if trigger.get("always", 0) != 1: # 1:always,0:on_change 236 | if previous_executed.get(state_tag): 237 | continue 238 | previous_executed[state_tag] = True 239 | 240 | metadata_sets = metadata 241 | metadata_sets["RobotData"] = robot_data 242 | metadata_sets["timestamp"] = timestamp 243 | 244 | lprint("[RobotData]", metadata_sets.get("RobotData")) 245 | # call next service 246 | if connection_key: 247 | conn.output_kanban( 248 | result=True, 249 | connection_key=connection_key, 250 | metadata=metadata_sets 251 | ) 252 | else: 253 | conn.output_kanban( 254 | result=True, 255 | metadata=metadata_sets 256 | ) 257 | lprint(f'[client] output kanban') 258 | else: 259 | lprint("[client ]condition is not true") 260 | previous_executed[state_tag] = False 261 | """ 262 | 263 | --------------------------------------------------------------------------------