├── .gitignore ├── dcp.py ├── main.py ├── protocol.py ├── rpc.py ├── server.py ├── templates ├── device.html ├── discover.html ├── inm0.html └── inm1.html └── util.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /dcp.py: -------------------------------------------------------------------------------- 1 | import argparse, time 2 | 3 | 4 | from util import * 5 | from protocol import * 6 | 7 | 8 | params = { 9 | "name": PNDCPBlock.NAME_OF_STATION, 10 | "ip": PNDCPBlock.IP_ADDRESS 11 | } 12 | 13 | 14 | class DCPDeviceDescription: 15 | def __init__(self, mac, blocks): 16 | self.mac = mac2s(mac) 17 | self.name = blocks[PNDCPBlock.NAME_OF_STATION].decode() 18 | self.ip = s2ip(blocks[PNDCPBlock.IP_ADDRESS][0:4]) 19 | self.netmask = s2ip(blocks[PNDCPBlock.IP_ADDRESS][4:8]) 20 | self.gateway = s2ip(blocks[PNDCPBlock.IP_ADDRESS][8:12]) 21 | self.vendorHigh, self.vendorLow, self.devHigh, self.devLow = unpack(">BBBB", blocks[PNDCPBlock.DEVICE_ID][0:4]) 22 | 23 | 24 | def get_param(s, src, target, param): 25 | dst = s2mac(target) 26 | 27 | if param not in params.keys(): 28 | return 29 | 30 | param = params[param] 31 | 32 | block = PNDCPBlockRequest(param[0], param[1], 0, bytes()) 33 | dcp = PNDCPHeader(0xfefd, PNDCPHeader.GET, PNDCPHeader.REQUEST, 0x012345, 0, 2, block) 34 | eth = EthernetVLANHeader(dst, src, 0x8100, 0, PNDCPHeader.ETHER_TYPE, dcp) 35 | 36 | s.send(bytes(eth)) 37 | 38 | return list(read_response(s, src, once=True).values())[0][param] 39 | 40 | def set_param(s, src, target, param, value): 41 | dst = s2mac(target) 42 | 43 | if param not in params.keys(): 44 | return 45 | 46 | param = params[param] 47 | 48 | block = PNDCPBlockRequest(param[0], param[1], len(value) + 2, bytes([0x00, 0x00]) + bytes(value, encoding='ascii')) 49 | dcp = PNDCPHeader(0xfefd, PNDCPHeader.SET, PNDCPHeader.REQUEST, 0x012345, 0, len(value) + 6 + (1 if len(value) % 2 == 1 else 0), block) 50 | eth = EthernetVLANHeader(dst, src, 0x8100, 0, PNDCPHeader.ETHER_TYPE, dcp) 51 | 52 | s.send(bytes(eth)) 53 | 54 | # ignore response 55 | s.recv(1522) 56 | 57 | time.sleep(2) 58 | 59 | 60 | def send_discover(s, src): 61 | 62 | block = PNDCPBlockRequest(0xFF, 0xFF, 0, bytes()) 63 | dcp = PNDCPHeader(0xfefe, PNDCPHeader.IDENTIFY, PNDCPHeader.REQUEST, 0x012345, 0, len(block), payload=block) 64 | eth = EthernetVLANHeader(s2mac("01:0e:cf:00:00:00"), src, 0x8100, 0, PNDCPHeader.ETHER_TYPE, payload=dcp) 65 | 66 | s.send(bytes(eth)) 67 | 68 | 69 | def send_request(s, src, t, value): 70 | 71 | block = PNDCPBlockRequest(t[0], t[1], len(value), bytes(value)) 72 | dcp = PNDCPHeader(0xfefe, PNDCPHeader.IDENTIFY, PNDCPHeader.REQUEST, 0x012345, 0, len(block), block) 73 | eth = EthernetVLANHeader(s2mac("01:0e:cf:00:00:00"), src, 0x8100, 0, PNDCPHeader.ETHER_TYPE, dcp) 74 | 75 | s.send(bytes(eth)) 76 | 77 | 78 | def read_response(s, my_mac, to=20, once=False, debug=False): 79 | ret = {} 80 | found = [] 81 | s.settimeout(2) 82 | try: 83 | with max_timeout(to) as t: 84 | while True: 85 | if t.timed_out: 86 | break 87 | try: 88 | data = s.recv(1522) 89 | except timeout: 90 | continue 91 | 92 | # nur Ethernet Pakete an uns und vom Ethertype Profinet 93 | eth = EthernetHeader(data) 94 | if eth.dst != my_mac or eth.type != PNDCPHeader.ETHER_TYPE: 95 | continue 96 | debug and print("MAC address:", mac2s(eth.src)) 97 | 98 | # nur DCP Identify Responses 99 | pro = PNDCPHeader(eth.payload) 100 | if not (pro.service_type == PNDCPHeader.RESPONSE): 101 | continue 102 | 103 | # Blöcke der Response parsen 104 | blocks = pro.payload 105 | length = pro.length 106 | parsed = {} 107 | 108 | while length > 6: 109 | block = PNDCPBlock(blocks) 110 | blockoption = (block.option, block.suboption) 111 | parsed[blockoption] = block.payload 112 | 113 | block_len = block.length 114 | if blockoption == PNDCPBlock.NAME_OF_STATION: 115 | debug and print("Name of Station: %s" % block.payload) 116 | parsed["name"] = block.payload 117 | elif blockoption == PNDCPBlock.IP_ADDRESS: 118 | debug and print(str(block.parse_ip())) 119 | parsed["ip"] = s2ip(block.payload[0:4]) 120 | elif blockoption == PNDCPBlock.DEVICE_ID: 121 | parsed["devId"] = block.payload 122 | 123 | # Padding: 124 | if block_len % 2 == 1: 125 | block_len += 1 126 | 127 | # geparsten Block entfernen 128 | blocks = blocks[block_len+4:] 129 | length -= 4 + block_len 130 | 131 | ret[eth.src] = parsed 132 | 133 | if once: 134 | break 135 | 136 | except TimeoutError: 137 | pass 138 | 139 | return ret 140 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | 3 | 4 | import dcp 5 | import rpc 6 | 7 | from util import * 8 | from protocol import * 9 | 10 | 11 | parser = argparse.ArgumentParser() 12 | parser.add_argument("-i", required=True, 13 | help="use INTERFACE", metavar="INTERFACE") 14 | parser.add_argument("action", choices=("discover", "get-param", "set-param", "read", "read-inm0-filter", "read-inm0", "read-inm1", "write-inm1")) 15 | parser.add_argument("target", nargs='?', help="MAC address of the device") 16 | parser.add_argument("param", nargs='?', help="parameter to read/write") 17 | parser.add_argument("value", nargs='?', help="value to write") 18 | parser.add_argument("additional1", nargs='?', help="additional parameters") 19 | parser.add_argument("additional2", nargs='?', help="additional parameters") 20 | 21 | args = parser.parse_args() 22 | 23 | 24 | s = ethernet_socket(args.i, 3) 25 | src = get_mac(args.i) 26 | 27 | if args.action == "discover": 28 | dcp.send_discover(s, src) 29 | dcp.read_response(s, src, debug=True) 30 | elif args.action == "get-param": 31 | dcp.get_param(s, src, args.target, args.param) 32 | elif args.action == "set-param": 33 | dcp.set_param(s, src, args.target, args.param, args.value) 34 | elif args.action.startswith("read") or args.action.startswith("write"): 35 | print("Getting station info ...") 36 | info = rpc.get_station_info(s, src, args.target) 37 | con = rpc.RPCCon(info) 38 | 39 | print("Connecting to device ...") 40 | con.connect(src) 41 | 42 | if args.action == "read": 43 | print(con.read(api=int(args.param), slot=int(args.value), subslot=int(args.additional1), idx=int(args.additional2, 16)).payload) 44 | 45 | if args.action[5:] == "inm0-filter": 46 | 47 | data = con.read_inm0filter() 48 | for api in data.keys(): 49 | for slot_number, (module_ident_number, subslots) in data[api].items(): 50 | print("Slot %d has module 0x%04X" % (slot_number, module_ident_number)) 51 | for subslot_number, submodule_ident_number in subslots.items(): 52 | print(" Subslot %d has submodule 0x%04X" % (subslot_number, submodule_ident_number)) 53 | 54 | elif args.action[5:] == "inm0": 55 | inm0 = PNInM0(con.read(api=int(args.param), slot=int(args.value), subslot=int(args.additional1), idx=PNInM0.IDX).payload) 56 | print(inm0) 57 | 58 | elif args.action[5:] == "inm1": 59 | inm1 = PNInM1(con.read(api=int(args.param), slot=int(args.value), subslot=int(args.additional1), idx=PNInM1.IDX).payload) 60 | print(inm1) 61 | 62 | elif args.action[6:] == "inm1": 63 | api = int(args.param) 64 | slot = int(args.value) 65 | subslot = int(args.additional1) 66 | inm1 = PNInM1(con.read(api, slot, subslot, PNInM1.IDX).payload) 67 | inm1 = PNInM1(inm1.block_header, bytes(args.additional2, "utf-8"), inm1.im_tag_location) 68 | con.write(api, slot, subslot, PNInM1.IDX, inm1) 69 | 70 | 71 | -------------------------------------------------------------------------------- /protocol.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | 4 | from util import * 5 | 6 | 7 | # ---------------------------------------------------------------------------------------------- 8 | # 9 | # DCP 10 | # 11 | 12 | EthernetHeader = make_packet("EthernetHeader", ( 13 | ("dst", ("6s", mac2s)), 14 | ("src", ("6s", mac2s)), 15 | ("type", ("H", "0x%04X")) 16 | )) 17 | 18 | EthernetVLANHeader = make_packet("EthernetHeader", ( 19 | ("dst", ("6s", mac2s)), 20 | ("src", ("6s", mac2s)), 21 | ("tpid", ("H", "0x%04X")), 22 | ("tci", ("H", "0x%04X")), 23 | ("type", ("H", "0x%04X")) 24 | )) 25 | 26 | PNDCPHeader = make_packet("PNDCPHeader", ( 27 | ("frame_id", ("H", "0x%04X")), 28 | ("service_id", "B"), 29 | ("service_type", "B"), 30 | ("xid", ("I", "0x%08X")), 31 | ("resp", "H"), 32 | ("length", "H") 33 | ), statics={ 34 | "ETHER_TYPE": 0x8892, 35 | "GET": 3, 36 | "SET": 4, 37 | "IDENTIFY": 5, 38 | "REQUEST": 0, 39 | "RESPONSE": 1 40 | }) 41 | 42 | class IPConfiguration(namedtuple("IPConfiguration", ["address", "netmask", "gateway"])): 43 | def __str__(self): 44 | return ("IP configuration\n" 45 | "Address: %s\n" 46 | "Netmask: %s\n" 47 | "Gateway: %s\n") % (self.address, self.netmask, self.gateway) 48 | 49 | class PNDCPBlockRequest(make_packet("PNDCPBlockRequest", ( 50 | ("option", "B"), 51 | ("suboption", "B"), 52 | ("length", "H") 53 | ), payload_size_field="length")): 54 | def parse_ip(self): 55 | return IPConfiguration(s2ip(self.payload[0:4]), s2ip(self.payload[4:8]), s2ip(self.payload[8:12])) 56 | 57 | class PNDCPBlock(make_packet("PNDCPBlockRequest", ( 58 | ("option", "B"), 59 | ("suboption", "B"), 60 | ("length", "H"), 61 | ("status", "H"), 62 | ), payload_size_field="length", payload_offset=-2)): 63 | 64 | IP_ADDRESS = (1, 2) 65 | NAME_OF_STATION = (2, 2) 66 | DEVICE_ID = (2, 3) 67 | ALL = (0xFF, 0xFF) 68 | 69 | def parse_ip(self): 70 | return IPConfiguration(s2ip(self.payload[0:4]), s2ip(self.payload[4:8]), s2ip(self.payload[8:12])) 71 | 72 | 73 | 74 | 75 | # ---------------------------------------------------------------------------------------------- 76 | # 77 | # RPC 78 | # 79 | 80 | _UUID = [0x6c, 0x97, 0x11, 0xd1, 0x82, 0x71, 0x00, 0xA0, 0x24, 0x42, 0xDF, 0x7D] 81 | 82 | PNRPCHeader = make_packet("PNRPCHeader", ( 83 | ("version", "B"), 84 | ("packet_type", "B"), 85 | ("flags1", "B"), 86 | ("flags2", "B"), 87 | ("drep", "3s"), 88 | ("serial_high", "B"), 89 | ("object_uuid", "16s"), 90 | ("interface_uuid", "16s"), 91 | ("activity_uuid", "16s"), 92 | ("server_boot_time", "I"), 93 | ("interface_version", "I"), 94 | ("sequence_number", "I"), 95 | ("operation_number", "H"), 96 | ("interface_hint", "H"), 97 | ("activity_hint", "H"), 98 | ("length_of_body", "H"), 99 | ("fragment_number", "H"), 100 | ("authentication_protocol", "B"), 101 | ("serial_low", "B") 102 | ), statics={ 103 | "REQUEST": 0x00, 104 | "PING": 0x01, 105 | "RESPONSE": 0x02, 106 | "FAULT": 0x03, 107 | "WORKING": 0x04, 108 | "PONG": 0x05, 109 | "REJECT": 0x06, 110 | "ACK": 0x07, 111 | "CANCEL": 0x08, 112 | "FRAG_ACK": 0x09, 113 | "CANCEL_ACK": 0xA, 114 | 115 | "CONNECT": 0x00, 116 | "RELEASE": 0x01, 117 | "READ": 0x02, 118 | "WRITE": 0x03, 119 | "CONTROL": 0x04, 120 | "IMPLICIT_READ": 0x05, 121 | 122 | "IFACE_UUID_DEVICE": bytes([0xDE, 0xA0, 0x00, 0x01] + _UUID), 123 | "IFACE_UUID_CONTROLLER": bytes([0xDE, 0xA0, 0x00, 0x02] + _UUID), 124 | "IFACE_UUID_SUPERVISOR": bytes([0xDE, 0xA0, 0x00, 0x03] + _UUID), 125 | "IFACE_UUID_PARAMSERVER": bytes([0xDE, 0xA0, 0x00, 0x04] + _UUID), 126 | 127 | "OBJECT_UUID_PREFIX": bytes([0xDE, 0xA0, 0x00, 0x00, 0x6C, 0x97, 0x11, 0xD1, 0x82, 0x71]) 128 | }) 129 | 130 | 131 | PNNRDData = make_packet("PNNRDData", ( 132 | ("args_maximum_status", "I"), 133 | ("args_length", "I"), 134 | ("maximum_count", "I"), 135 | ("offset", "I"), 136 | ("actual_count", "I") 137 | )) 138 | 139 | 140 | PNIODHeader = make_packet("PNIODHeader", ( 141 | ("block_header", "6s"), 142 | ("sequence_number", "H"), 143 | ("ar_uuid", "16s"), 144 | ("api", "I"), 145 | ("slot", "H"), 146 | ("subslot", "H"), 147 | ("padding1", "H"), 148 | ("index", "H"), 149 | ("length", "I"), 150 | ("target_ar_uuid", "16s"), 151 | ("padding2", "8s") 152 | )) 153 | 154 | 155 | PNBlockHeader = make_packet("PNBlockHeader", ( 156 | ("block_type", "H"), 157 | ("block_length", "H"), 158 | ("block_version_high", "B"), 159 | ("block_version_low", "B") 160 | ), payload=False, statics={ 161 | "IDOReadRequestHeader": 0x0009, 162 | "IODReadResponseHeader": 0x8009, 163 | "InM0": 0x0020, 164 | "InM0FilterDataSubModul": 0x0030, 165 | "InM0FilterDataModul": 0x0031, 166 | "InM0FilterDataDevice": 0x0032 167 | }) 168 | 169 | PNARBlockRequest = make_packet("PNARBlockRequest", ( 170 | ("block_header", "6s"), 171 | ("ar_type", "H"), 172 | ("ar_uuid", "16s"), 173 | ("session_key", "H"), 174 | ("cm_initiator_mac_address", "6s"), 175 | ("cm_initiator_object_uuid", "16s"), 176 | ("ar_properties", "I"), 177 | ("cm_initiator_activity_timeout_factor", "H"), 178 | ("initiator_udp_rtport", "H"), 179 | ("station_name_length", "H") 180 | ), vlf="cm_initiator_station_name", vlf_size_field="station_name_length") 181 | 182 | 183 | PNIODReleaseBlock = make_packet("IODReleaseBlock", ( 184 | ("block_header", "6s"), 185 | ("padding1", "H"), 186 | ("ar_uuid", "16s"), 187 | ("session_key", "H"), 188 | ("padding2", "H"), 189 | ("control_command", "H"), 190 | ("control_block_properties", "H") 191 | )) 192 | 193 | 194 | PNInM0 = make_packet("InM0", ( 195 | ("block_header", "6s"), 196 | ("vendor_id_high", "B"), 197 | ("vendor_id_low", "B"), 198 | ("order_id", ("20s", decode_bytes)), 199 | ("im_serial_number", ("16s", decode_bytes)), 200 | ("im_hardware_revision", "H"), 201 | ("sw_revision_prefix", "B"), 202 | ("im_sw_revision_functional_enhancement", "B"), 203 | ("im_sw_revision_bug_fix", "B"), 204 | ("im_sw_revision_internal_change", "B"), 205 | ("im_revision_counter", "H"), 206 | ("im_profile_id", "H"), 207 | ("im_profile_specific_type", "H"), 208 | ("im_version", "H"), 209 | ("im_supported", "H") 210 | ), payload=False, statics={ "IDX": 0xAFF0 }) 211 | 212 | PNInM1 = make_packet("InM1", ( 213 | ("block_header", "6s"), 214 | ("im_tag_function", ("32s", decode_bytes)), 215 | ("im_tag_location", ("22s", decode_bytes)), 216 | ), payload=False, statics={ "IDX": 0xAFF1 }) 217 | 218 | 219 | -------------------------------------------------------------------------------- /rpc.py: -------------------------------------------------------------------------------- 1 | from struct import unpack 2 | from socket import MSG_WAITALL 3 | from datetime import datetime 4 | 5 | from util import * 6 | from protocol import * 7 | 8 | import dcp 9 | 10 | 11 | def get_station_info(s, src, name): 12 | dcp.send_request(s, src, PNDCPBlock.NAME_OF_STATION, bytes(name, 'utf-8')) 13 | resp = list(dcp.read_response(s, src, once=True).items())[0] 14 | return dcp.DCPDeviceDescription(*resp) 15 | 16 | 17 | class RPCCon: 18 | def __init__(self, info): 19 | self.info = info 20 | self.peer = (info.ip, 0x8894) 21 | 22 | self.ar_uuid = bytes([0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]) 23 | self.activity_uuid = self.ar_uuid 24 | 25 | self.local_object_uuid = PNRPCHeader.OBJECT_UUID_PREFIX + bytes([0x00, 0x01, 0x76, 0x54, 0x32, 0x10]) 26 | self.remote_object_uuid = PNRPCHeader.OBJECT_UUID_PREFIX + bytes([0x00, 0x01, self.info.devHigh, self.info.devLow, self.info.vendorHigh, self.info.vendorLow]) 27 | 28 | self.live = None 29 | 30 | self.u = socket(AF_INET, SOCK_DGRAM) 31 | 32 | 33 | def _create_rpc(self, operation, nrd): 34 | return PNRPCHeader(0x04, PNRPCHeader.REQUEST, 35 | 0x20, # Flags1 36 | 0x00, # Flags2 37 | bytes([0x00, 0x00, 0x00]), # DRep 38 | 0x00, # Serial High 39 | self.remote_object_uuid, 40 | PNRPCHeader.IFACE_UUID_DEVICE, 41 | self.activity_uuid, 42 | 0, # ServerBootTime 43 | 1, # InterfaceVersion 44 | 0, # SequenceNumber 45 | operation, 46 | 0xFFFF, # InterfaceHint 47 | 0xFFFF, # ActivityHint 48 | len(nrd), 49 | 0, # FragmentNumber 50 | 0, # AuthenticationProtocol 51 | 0, # SerialLow 52 | payload=nrd 53 | ) 54 | 55 | def _create_nrd(self, payload): 56 | return PNNRDData(1500, len(payload), 1500, 0, len(payload), payload=payload) 57 | 58 | def _check_timeout(self): 59 | if self.live is not None and (datetime.now() - self.live).seconds >= 10: 60 | self.connect() 61 | 62 | def connect(self, src_mac=None): 63 | if self.live is None: 64 | self.src_mac = src_mac 65 | 66 | block = PNBlockHeader(0x0101, PNARBlockRequest.fmt_size - 2, 0x01, 0x00) 67 | ar = PNARBlockRequest(bytes(block), 68 | 0x0006, # AR Type 69 | self.ar_uuid, # AR UUID 70 | 0x1234, # Session key 71 | self.src_mac, 72 | self.local_object_uuid, 73 | 0x131, # AR Properties 74 | 100, # Timeout factor 75 | 0x8892, # udp port? 76 | 2, 77 | bytes("tp", encoding="utf-8"), payload=bytes() 78 | ) 79 | nrd = self._create_nrd(ar) 80 | rpc = self._create_rpc(PNRPCHeader.CONNECT, nrd) 81 | self.u.sendto(bytes(rpc), self.peer) 82 | 83 | data = self.u.recvfrom(4096)[0] 84 | # ignore response 85 | #rpc = PNRPCHeader(data) 86 | #nrd = PNNRDData(rpc.payload) 87 | #ar = PNARBlockRequest(nrd.payload) 88 | #block = PNBlockHeader(iod.block_header) 89 | 90 | self.live = datetime.now() 91 | 92 | def read(self, api, slot, subslot, idx): 93 | self._check_timeout() 94 | 95 | block = PNBlockHeader(PNBlockHeader.IDOReadRequestHeader, 60, 0x01, 0x00) 96 | iod = PNIODHeader(bytes(block), 0, self.ar_uuid, api, slot, subslot, 0, idx, 4096, bytes(16), bytes(8), payload=bytes()) 97 | nrd = self._create_nrd(iod) 98 | rpc = self._create_rpc(PNRPCHeader.READ, nrd) 99 | self.u.sendto(bytes(rpc), self.peer) 100 | 101 | data = self.u.recvfrom(4096)[0] 102 | rpc = PNRPCHeader(data) 103 | nrd = PNNRDData(rpc.payload) 104 | iod = PNIODHeader(nrd.payload) 105 | block = PNBlockHeader(iod.block_header) 106 | 107 | self.live = datetime.now() 108 | 109 | return iod 110 | 111 | def read_implicit(self, api, slot, subslot, idx): 112 | block = PNBlockHeader(PNBlockHeader.IDOReadRequestHeader, 60, 0x01, 0x00) 113 | iod = PNIODHeader(bytes(block), 0, bytes(16), api, slot, subslot, 0, idx, 4096, bytes(16), bytes(8), payload=bytes()) 114 | nrd = self._create_nrd(iod) 115 | rpc = self._create_rpc(PNRPCHeader.IMPLICIT_READ, nrd) 116 | self.u.sendto(bytes(rpc), self.peer) 117 | 118 | data = self.u.recvfrom(4096)[0] 119 | rpc = PNRPCHeader(data) 120 | nrd = PNNRDData(rpc.payload) 121 | iod = PNIODHeader(nrd.payload) 122 | block = PNBlockHeader(iod.block_header) 123 | 124 | return iod 125 | 126 | def write(self, api, slot, subslot, idx, data): 127 | self._check_timeout() 128 | block = PNBlockHeader(0x8, 60, 0x01, 0x00) 129 | iod = PNIODHeader(bytes(block), 0, self.ar_uuid, api, slot, subslot, 0, idx, len(data), bytes(16), bytes(8), payload=bytes(data)) 130 | nrd = self._create_nrd(iod) 131 | rpc = self._create_rpc(PNRPCHeader.WRITE, nrd) 132 | self.u.sendto(bytes(rpc), self.peer) 133 | 134 | data = self.u.recvfrom(4096)[0] 135 | # ignore response 136 | 137 | self.live = datetime.now() 138 | 139 | 140 | def read_inm0filter(self): 141 | data = self.read(api=0, slot=0, subslot=0, idx=0xF840).payload 142 | block = PNBlockHeader(data) 143 | data = data[6:] 144 | 145 | ret = {} 146 | 147 | num_api, = unpack(">H", data[:2]) 148 | data = data[2:] 149 | for i_api in range(0, num_api): 150 | api, num_modules = unpack(">IH", data[:6]) 151 | data = data[6:] 152 | ret[api] = {} 153 | for i_module in range(0, num_modules): 154 | slot_number, module_ident_num, num_subslots = unpack(">HIH", data[:8]) 155 | data = data[8:] 156 | ret[api][slot_number] = (module_ident_num, {}) 157 | for i_subslot in range(0, num_subslots): 158 | subslot_number, submodule_ident_number = unpack(">HI", data[:6]) 159 | data = data[6:] 160 | ret[api][slot_number][1][subslot_number] = submodule_ident_number 161 | 162 | return ret 163 | 164 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | from struct import unpack 2 | import argparse 3 | import binascii 4 | 5 | from flask import Flask, request, render_template, redirect, url_for 6 | 7 | 8 | from protocol import * 9 | import dcp, rpc 10 | 11 | 12 | app = Flask(__name__) 13 | conns = {} 14 | 15 | parser = argparse.ArgumentParser() 16 | parser.add_argument("-i", required=True, 17 | help="use INTERFACE", metavar="INTERFACE") 18 | args = parser.parse_args() 19 | 20 | s = ethernet_socket(args.i, 3) 21 | src = get_mac(args.i) 22 | 23 | def get_connection(name): 24 | if name not in conns.keys(): 25 | info = rpc.get_station_info(s, src, name) 26 | con = rpc.RPCCon(info) 27 | con.connect(src) 28 | conns[name] = (info, con) 29 | return conns[name] 30 | 31 | 32 | @app.route("/") 33 | def index(): 34 | dcp.send_discover(s, src) 35 | resp = [dcp.DCPDeviceDescription(mac, blocks) for mac, blocks in dcp.read_response(s, src).items()] 36 | return render_template("discover.html", resp=resp) 37 | 38 | @app.route("/device") 39 | def device(): 40 | name = request.values.get('name') 41 | info, con = get_connection(name) 42 | data = con.read_inm0filter() 43 | return render_template("device.html", info=info, data=data) 44 | 45 | @app.route("/inm0", methods=["GET", "POST"]) 46 | def inm0(): 47 | name = request.values.get('name') 48 | 49 | api = int(request.values.get('api')) 50 | slot = int(request.values.get('slot')) 51 | subslot = int(request.values.get('subslot')) 52 | 53 | info, con = get_connection(name) 54 | payload = con.read(api, slot, subslot, idx=PNInM0.IDX).payload 55 | if len(payload) != 0: 56 | data = PNInM0(payload) 57 | else: 58 | data = None 59 | 60 | idx = request.values.get('idx') 61 | paramdata = None 62 | if idx is not None: 63 | _idx = int(idx, 16) 64 | if request.values.get('action') == "write": 65 | paramdata = request.values.get('data') 66 | con.write(api, slot, subslot, _idx, binascii.unhexlify(paramdata.replace(":", ""))) 67 | else: 68 | paramdata = to_hex(con.read(api, slot, subslot, idx=_idx).payload) 69 | else: 70 | idx = "" 71 | 72 | return render_template("inm0.html", name=name, api=api, slot=slot, subslot=subslot, idx=idx, data=data, paramdata=paramdata, inm1_supported=(data.im_supported&1<<1 if data is not None else False)) 73 | 74 | @app.route("/inm1", methods=["GET", "POST"]) 75 | def inm1(): 76 | name = request.values.get('name') 77 | 78 | api = int(request.values.get('api')) 79 | slot = int(request.values.get('slot')) 80 | subslot = int(request.values.get('subslot')) 81 | 82 | info, con = get_connection(name) 83 | 84 | data = PNInM1(con.read(api, slot, subslot, idx=PNInM1.IDX).payload) 85 | 86 | if request.method == 'POST': 87 | function = request.values.get('function') 88 | location = request.values.get('location') 89 | inm1 = PNInM1(data.block_header, bytes(function, "utf-8"), bytes(location, "utf-8")) 90 | con.write(api, slot, subslot, PNInM1.IDX, inm1) 91 | return redirect(url_for("inm1", name=name, api=api, slot=slot, subslot=subslot)) 92 | else: 93 | return render_template("inm1.html", name=name, api=api, slot=slot, subslot=subslot, data=data) 94 | 95 | @app.route("/rename") 96 | def rename(): 97 | mac = request.values.get('mac') 98 | name = request.values.get('name') 99 | 100 | old_name = dcp.get_param(s, src, mac, "name").decode() 101 | 102 | # delete connection 103 | if old_name in conns.keys(): 104 | del conns[old_name] 105 | 106 | dcp.set_param(s, src, mac, "name", name) 107 | 108 | return redirect(url_for("device", name=name)) 109 | 110 | app.run(debug=True) 111 | -------------------------------------------------------------------------------- /templates/device.html: -------------------------------------------------------------------------------- 1 |

{{info.name}}

2 | 3 | Back to discovery 4 | 5 | 28 | 29 |

I&M0 Filter Data

30 | 31 | 48 | -------------------------------------------------------------------------------- /templates/discover.html: -------------------------------------------------------------------------------- 1 |

Devices on the local network

2 | 3 | 11 | -------------------------------------------------------------------------------- /templates/inm0.html: -------------------------------------------------------------------------------- 1 |

{{name}}

2 | 3 | Back to device 4 | 5 | {% if data is not none %} 6 | 7 |

I&M0 data

8 | 9 | 36 | 37 | {% if inm1_supported %} 38 | 39 | I&M1 data 40 | 41 | {% endif %} 42 | 43 | {% endif %} 44 | 45 | 46 |

Read / Write parameters

47 | 48 |
49 | 50 | 51 | 52 | 53 | 54 | 59 | 60 | 61 | 62 | 63 |
64 | -------------------------------------------------------------------------------- /templates/inm1.html: -------------------------------------------------------------------------------- 1 |

{{name}}

2 | 3 | Back to device 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 |
18 | -------------------------------------------------------------------------------- /util.py: -------------------------------------------------------------------------------- 1 | from socket import * 2 | from fcntl import ioctl 3 | from sys import argv, exit, stdout, stderr 4 | from struct import pack, unpack, calcsize 5 | from collections import namedtuple, OrderedDict 6 | 7 | import time 8 | 9 | 10 | def to_hex(s): 11 | return ":".join("{:02x}".format(c) for c in s) 12 | 13 | def s2mac(s): 14 | return bytes([int(num, 16) for num in s.split(':')]) 15 | 16 | def mac2s(m): 17 | return ':'.join(hex(num)[2:].zfill(2) for num in m) 18 | 19 | def get_mac(ifname): 20 | s = socket(AF_INET, SOCK_DGRAM) 21 | info = ioctl(s.fileno(), 0x8927, pack('256s', bytes(ifname[:15], "ascii"))) 22 | return info[18:24] 23 | 24 | def ethernet_socket(interface, ethertype): 25 | s = socket(AF_PACKET, SOCK_RAW) 26 | s.bind((interface, ethertype)) 27 | return s 28 | 29 | def udp_socket(host, port): 30 | s = socket(AF_INET, SOCK_DGRAM) 31 | s.connect((host, port)) 32 | return s 33 | 34 | def s2ip(i): 35 | return '.'.join(str(o) for o in i) 36 | 37 | def decode_bytes(s): 38 | return s.decode() 39 | 40 | class max_timeout(object): 41 | def __init__(self, seconds): 42 | self.seconds = seconds 43 | def __enter__(self): 44 | self.die_after = time.time() + self.seconds 45 | return self 46 | def __exit__(self, type, value, traceback): 47 | pass 48 | @property 49 | def timed_out(self): 50 | return time.time() > self.die_after 51 | 52 | """ 53 | class timeout: 54 | def __init__(self, seconds=1, error_message='Timeout'): 55 | self.seconds = seconds 56 | self.error_message = error_message 57 | def handle_timeout(self, signum, frame): 58 | raise TimeoutError(self.error_message) 59 | def __enter__(self): 60 | signal.signal(signal.SIGALRM, self.handle_timeout) 61 | signal.alarm(self.seconds) 62 | def __exit__(self, type, value, traceback): 63 | signal.alarm(0) 64 | """ 65 | 66 | # vlf = variable length field 67 | def make_packet(name, fields, statics={}, payload=True, payload_size_field=None, payload_offset=0, vlf=None, vlf_size_field=None): 68 | fields = OrderedDict(fields) 69 | fmt = ">" + "".join([(f[0] if isinstance(f, tuple) else f) for f in fields.values()]) 70 | size = calcsize(fmt) 71 | 72 | f = list(fields.keys()) 73 | if vlf is not None: 74 | f.append(vlf) 75 | if payload: 76 | f.append("payload") 77 | 78 | t = namedtuple(name, f) 79 | class _class(t): 80 | 81 | def __new__(cls, *args, **kwargs): 82 | 83 | # unpack (parse packet) 84 | if len(args) == 1: 85 | data = args[0] 86 | 87 | # unpack known-size fields 88 | unpacked = unpack(fmt, data[0:size]) 89 | 90 | kw = {} 91 | 92 | # handle variable length fields 93 | if vlf is not None: 94 | vlf_size = unpacked[list(fields.keys()).index(vlf_size_field)] 95 | kw[vlf] = data[size:size+vlf_size] 96 | else: 97 | vlf_size = 0 98 | 99 | # handle payload 100 | if payload: 101 | if payload_size_field is not None: 102 | pl_size = unpacked[list(fields.keys()).index(payload_size_field)] + payload_offset 103 | kw["payload"] = data[size+vlf_size:size+vlf_size+pl_size] 104 | else: 105 | kw["payload"] = data[size+vlf_size:] 106 | 107 | # finally create instance 108 | self = t.__new__(cls, *unpacked, **kw) 109 | 110 | # pack (create packet) 111 | else: 112 | self = t.__new__(cls, *args, **kwargs) 113 | 114 | return self 115 | 116 | def __str__(self): 117 | ret = "%s packet (%d bytes)\n" % (name, len(self)) 118 | for k, v in fields.items(): 119 | ret += k + ": " 120 | value = getattr(self, k) 121 | if isinstance(v, tuple): 122 | if isinstance(v[1], str): 123 | ret += v[1] % value 124 | else: 125 | ret += v[1](value) 126 | else: 127 | ret += str(value) 128 | ret += "\n" 129 | return ret 130 | 131 | def __bytes__(self): 132 | packed = pack(fmt, *(getattr(self, key) for key in fields.keys())) 133 | if vlf is not None: 134 | packed += bytes(getattr(self, vlf)) 135 | if payload: 136 | packed += bytes(self.payload) 137 | return packed 138 | 139 | def __len__(self): 140 | s = size 141 | if vlf is not None: 142 | s += len(bytes(getattr(self, vlf))) 143 | if payload: 144 | s += len(self.payload) 145 | return s 146 | 147 | _class.fmt = fmt 148 | _class.fmt_size = size 149 | 150 | for k, v in statics.items(): 151 | setattr(_class, k, v) 152 | 153 | return _class 154 | 155 | --------------------------------------------------------------------------------