PythonでiCAD SXのモデル情報を設定してみた

2023/12/02 categories:iCAD SX| tags:iCAD SX|Python|

iCAD SXとのソケット通信

iCAD SXとソケット通信するためにClientというクラスを作って、そのクラスでデータを送信しています。送受信するデータはutf-16leです。受信したデータにsx_errが含まれるとエラーが起きたということみたいなので、例外を出すようにしています。

    class Client:
        def send(self, commands: list[str]):
            self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.client.connect( ('localhost', 3999) )
            self.stream = self.client.makefile('rwb', buffering=0)

            send_data  = ''
            send_data += 'license=ON\n'
            send_data += 'mode=MACRO,NODISP\n'
            send_data += 'ret_ent=ON\n'
            send_data += '\n'.join(commands)
            send_data += 'SxMsg_End'

            self.stream.write( send_data.encode('utf-16le') )

        def read(self):
            xml = self.stream.read().decode('utf-16le')
            if xml[-1] == '\x00':
                xml = xml[:-1]

            root = ElementTree.fromstring(xml)
            
            error = root.find('sx_err')
            if error is not None:
                raise Exception( '{}, {}, {} : {}'.format(error.get('ir0'), error.get('ir1'), error.get('ir2'), error.text) )
            
            return root
        
        def close(self):
            self.client.close()

モデル情報のタブ名を取得

モデル情報のタブ名は環境設定で設定できるようになっているので、環境によって異なるようです。そのタブ名は、icad.iniに記載されているようなので、iniファイルを読み取ります。ここでconfigparserを使って読み込んでみようと思いましたが、icad.ini内に対応していない文字があったのか、エラーが出てしまってうまく読み取れませんでした。仕方がないので、iniファイルを読み取るためのIniFileParserというクラスを作って、そのクラスで文字列の処理することで、モデル情報のタブ名を取得することにしました。

class iCADSX:
    def __init__(self) -> None:
        self.icad_dir = os.getenv('ICADDIR')
        self.ini = self.IniFileParser(self.icad_dir + '\\ETC\\ICAD.ini')
        
    class IniFileParser:
        def __init__(self, filepath) -> None:
            self._data = {}
            with open(filepath, encoding='cp932') as f:
                lines = f.readlines()
            for line in lines:
                if line[-1] == '\n':
                    line = line[:-1]
                if line == '' or line.startswith(';') or line.startswith(';'):
                    continue
                elif line.startswith('[') and line.endswith(']'):
                    section = line[1:-1]
                    self._data[section] = {}
                    continue
                else:
                    splitted = line.split('=') + ['', '']
                    self._data[section][splitted[0]] = splitted[1]

        def __getitem__(self, key) -> dict:
            return self._data.get(key, {})
        
        def get(self, key) -> dict:
            return self.__getitem__(key)

モデル情報設定のコマンドを送信

コマンドはマクロ記録で記録したコマンドを使用します。記録したマクロを見てみるとTABCNTの行まではそのまま使えそうで、それ以降の行がモデル情報に書き込む文字列のデータになっているようです。適当な文字列を書き込んだ時のデータを見てみると、どうやらURLセーフなBase64のデータになっているような気がします。マクロの区切り文字としてスラッシュが使われているようなので、送信するデータにスラッシュが含まれないようにするためにURLセーフなデータにしているのでしょう。INFTITに続く文字列がタブ名で、INFDATに続く文字列が入力するデータとして、base64パッケージでエンコードした文字列を送ることでデータを書き込むことができました。ちなみにたくさんの文字を書き込む操作をマクロ記録してみるとINFDAT内のデータは160文字毎に区切られていたので、それと同じように160文字で区切るような処理にしています。

    class PartsInfo:
        def __init__(self, icad: 'iCADSX') -> None:
            self.icad = icad
            self.tab_names  = ['設計情報', '加工情報', '組付情報'] 
            self.tab_names += [ self.icad.ini.get('@PARTSINFO').get('TAB_NAME{}'.format(i)) for i in range(1, 5) ]

        def set_datas(self, datas: list[str]):
            commands = [
                ';PTINFO\n',
                '@GO\n',
                '..PISWAT /1/\n',
                ';PTINFO\n',
                ';PISET\n',
                ';@GO\n',
                '..PISWAT /0/\n',
                '..CODFLG /1/\n',
                '..TABCNT /7/\n'
            ]
            
            for tab_name, data in zip(self.tab_names, datas):
                commands += self.input_tab_data( tab_name, data )
            
            commands += [';GXDMY;@JVEND\n']

            client = self.icad.Client()
            client.send(commands)

            result = client.read()
            entity = self.icad.Ent( result.find('sx_ent') )
            client.close()

            return entity
            
        def input_tab_data(self, info_title: str, info_data: str):
            base64_string = self.to_base64_string(info_data)
            base64_string = self.to_base64_string(info_data)
            splitted = [ base64_string[i:i+160] for i in range(0, len(base64_string), 160) ]

            data = [
                '..INFTIT /{}/\n'.format( self.to_base64_string(info_title) ),
                '..LINCNT /{}/\n'.format( len(splitted) )
            ]

            for d in splitted:
                info_data
                data.append( '..INFDAT /{}/\n'.format(d) )
            
            return data
        
        def to_base64_string(self, text: str):
            encorded = text.encode('utf-16le')
            base64_data = base64.urlsafe_b64encode(encorded)
            base64_string = base64_data.decode('utf-8')
            return base64_string

受信したデータ

上記のコマンドを送信するとタイプ207の要素が送り返されるようです。そのデータを使うときようにEntクラスを作成して、そのクラスに受信したデータをセットするようにしました。

    class Ent:
        def __init__(self, element: ElementTree.Element) -> None:
            self.type = element.get('type')
            self.id = element.get('id')
            self.prmno = element.get('prmno')
            self.kind = element.get('kind')
            self.part_id = element.get('part_id')
            self.dim = element.get('dim')

        def __str__(self):
            return f'<iCAD.Ent type={self.type} id={self.id} prmno={self.prmno} kind={self.kind} part_id={self.part_id} dim={self.dim}>'

作った処理の実行

試しに下記のように設計情報に注記を書き込んでみるとうまくいきました。

def main():
    design_info = [
        '注1)指示無き角は糸面取りのこと',
        '注2)表面にキズ無きこと'
    ]

    cad = iCADSX()
    entity = cad.parts_info.set_datas([ '\r\n'.join(design_info), '', '', '', '', '', '' ])
    print(entity)

Pythonコード

import base64
import os
import socket
from xml.etree import ElementTree


def main():
    design_info = [
        '注1)指示無き角は糸面取りのこと',
        '注2)表面にキズ無きこと'
    ]

    cad = iCADSX()
    entity = cad.parts_info.set_datas([ '\r\n'.join(design_info), '', '', '', '', '', '' ])
    print(entity)


class iCADSX:

    def __init__(self) -> None:
        self.client = self.Client()
        self.icad_dir = os.getenv('ICADDIR')
        self.ini = self.IniFileParser(self.icad_dir + '\\ETC\\ICAD.ini')
        self.parts_info = self.PartsInfo(self)

    class IniFileParser:
        def __init__(self, filepath) -> None:
            self._data = {}
            with open(filepath, encoding='cp932') as f:
                lines = f.readlines()
            for line in lines:
                if line[-1] == '\n':
                    line = line[:-1]
                if line == '' or line.startswith(';') or line.startswith(';'):
                    continue
                elif line.startswith('[') and line.endswith(']'):
                    section = line[1:-1]
                    self._data[section] = {}
                    continue
                else:
                    splitted = line.split('=') + ['', '']
                    self._data[section][splitted[0]] = splitted[1]

        def __getitem__(self, key) -> dict:
            return self._data.get(key, {})
        
        def get(self, key) -> dict:
            return self.__getitem__(key)
        

    class Client:
        def send(self, commands: list[str]):
            self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.client.connect( ('localhost', 3999) )
            self.stream = self.client.makefile('rwb', buffering=0)

            send_data  = ''
            send_data += 'license=ON\n'
            send_data += 'mode=MACRO,NODISP\n'
            send_data += 'ret_ent=ON\n'
            send_data += '\n'.join(commands)
            send_data += 'SxMsg_End'

            self.stream.write( send_data.encode('utf-16le') )

        def read(self):
            xml = self.stream.read().decode('utf-16le')
            if xml[-1] == '\x00':
                xml = xml[:-1]

            root = ElementTree.fromstring(xml)
            
            error = root.find('sx_err')
            if error is not None:
                raise Exception( '{}, {}, {} : {}'.format(error.get('ir0'), error.get('ir1'), error.get('ir2'), error.text) )
            
            return root
        
        def close(self):
            self.client.close()
    

    class Ent:
        def __init__(self, element: ElementTree.Element) -> None:
            self.type = element.get('type')
            self.id = element.get('id')
            self.prmno = element.get('prmno')
            self.kind = element.get('kind')
            self.part_id = element.get('part_id')
            self.dim = element.get('dim')

        def __str__(self):
            return f'<iCAD.Ent type={self.type} id={self.id} prmno={self.prmno} kind={self.kind} part_id={self.part_id} dim={self.dim}>'


    class PartsInfo:
        def __init__(self, icad: 'iCADSX') -> None:
            self.icad = icad
            self.tab_names  = ['設計情報', '加工情報', '組付情報'] 
            self.tab_names += [ self.icad.ini.get('@PARTSINFO').get('TAB_NAME{}'.format(i)) for i in range(1, 5) ]

        def set_datas(self, datas: list[str]):
            commands = [
                ';PTINFO\n',
                '@GO\n',
                '..PISWAT /1/\n',
                ';PTINFO\n',
                ';PISET\n',
                ';@GO\n',
                '..PISWAT /0/\n',
                '..CODFLG /1/\n',
                '..TABCNT /7/\n'
            ]
            
            for tab_name, data in zip(self.tab_names, datas):
                commands += self.input_tab_data( tab_name, data )
            
            commands += [';GXDMY;@JVEND\n']

            client = self.icad.Client()
            client.send(commands)

            result = client.read()
            entity = self.icad.Ent( result.find('sx_ent') )
            client.close()

            return entity

        def input_tab_data(self, info_title: str, info_data: str):
            base64_string = self.to_base64_string(info_data)
            base64_string = self.to_base64_string(info_data)
            splitted = [ base64_string[i:i+160] for i in range(0, len(base64_string), 160) ]

            data = [
                '..INFTIT /{}/\n'.format( self.to_base64_string(info_title) ),
                '..LINCNT /{}/\n'.format( len(splitted) )
            ]

            for d in splitted:
                info_data
                data.append( '..INFDAT /{}/\n'.format(d) )
            
            return data
        
        def to_base64_string(self, text: str):
            encorded = text.encode('utf-16le')
            base64_data = base64.urlsafe_b64encode(encorded)
            base64_string = base64_data.decode('utf-8')
            return base64_string


if __name__ == '__main__':
    main()

Share post

Related Posts

コメント