commit 28a2b34b281bfb7beedb00f05c0b4265729eeb9c Author: michael_k Date: Mon Aug 31 16:37:53 2026 +0300 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6246a91 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +./__pycache__ \ No newline at end of file diff --git a/CRC16.py b/CRC16.py new file mode 100644 index 0000000..8d3b05c --- /dev/null +++ b/CRC16.py @@ -0,0 +1,10 @@ +def modbus_crc16(data: bytearray) -> bytearray: + crc = 0xFFFF + for byte in data: + crc ^= byte + for _ in range(8): + if crc & 0x0001: + crc = (crc >> 1) ^ 0xA001 + else: + crc >>= 1 + return bytearray([crc >> 8,crc & 0xFF]) \ No newline at end of file diff --git a/Executioner.py b/Executioner.py new file mode 100644 index 0000000..0687b13 --- /dev/null +++ b/Executioner.py @@ -0,0 +1,50 @@ +import sys +from time import sleep, time + +from source.IdiBus4DC import Module4DC +from source.IdiBusSerialLine import IdiBusSerialLine + + +class Executioner(): + def __init__(self, name): + self.name = name + self.port = "" + self.address = 0 + self.speed = 0 + def execute(self, command, args): + pass + # Must return string with all data in csv format. without \n or \r + def get_log_string(self): + pass +class Exec4DC(Executioner): + def __init__(self, ): + super().__init__("Module4DC") + def execute(self, command, args): + module = Module4DC(addr=self.address, line=IdiBusSerialLine(port=self.port, baudrate=self.speed)) + if command == "set-data": + module.set_channel_data(args[0], args[1], args[2]) + elif command == "set-state": + module.set_channel_state(args[0], args[1]) + else: + print("Unknown command: " + command) + def get_log_string(self): + module = Module4DC(addr=self.address, line=IdiBusSerialLine(port=self.port, baudrate=self.speed)) + str = "" + for i in range(4): + data = module.set_channel_data(i) + if data == None: + return None + else: + str += f"{data["voltage"]},{data["current"]}," +class ExecSystem(Executioner): + def __init__(self): + super().__init__("Module4DC") + def execute(self, command, args): + if command == "sleep": + sleep(args[0]) + elif command == "exit": + sys.exit(0) + else: + print("Unknown command: " + command) + def get_log_string(self): + return "System is ok. :P" \ No newline at end of file diff --git a/IdiBusDefs.py b/IdiBusDefs.py new file mode 100644 index 0000000..a7e0f32 --- /dev/null +++ b/IdiBusDefs.py @@ -0,0 +1,103 @@ +from enum import Enum +from typing import List, Dict + +class comFuncList(Enum): + IDIMMES_COM_C_Init = 220 + IDIMMES_COM_C_ShtDown = 221 + IDIMMES_COM_C_Freeze = 222 + IDIMMES_COM_C_Resume = 223 + IDIMMES_COM_C_Dummy = 224 + IDIMMES_COM_C_AssignGroup = 225 + IDIMMES_COM_C_SetAlarmL12 = 226 + IDIMMES_COM_C_SetAlarmL = 227 + IDIMMES_COM_C_Virtual = 228 + IDIMMES_COM_C_SyncReadChnl = 229 + IDIMMES_COM_C_SyncRead = 230 + IDIMMES_COM_C_SyncDoChnl = 231 + IDIMMES_COM_C_SyncDo = 232 + IDIMMES_COM_C_SyncClear = 233 + IDIMMES_COM_C_BurstReadCnt = 234 + IDIMMES_COM_C_BurstReadTime = 235 + IDIMMES_COM_C_SendTimeDate = 236 + IDIMMES_COM_C_MkTimedMaster = 237 + IDIMMES_COM_C_EnterBootloader = 238 + #Skipped for historical reasons + IDIMMES_COM_C_ReadDevFullSN_MS = 241 + IDIMMES_COM_C_WriteSnIPv4IPv6 = 242 + IDIMMES_COM_C_WriteSnVerifyDates = 243 + IDIMMES_COM_C_WriteSnAES256 = 244 + IDIMMES_COM_C_SendLongMessage = 245 + IDIMMES_COM_C_GetLondMessage = 246 + IDIMMES_COM_C_DummyModule = 247 + IDIMMES_COM_C_CheckModuleLongOp = 248 + IDIMMES_COM_C_CheckChannelLongOp = 249 + + IDIMMES_COM_C_FmwBootloaderInfo = 250 # Ask for bootloader version + IDIMMES_COM_C_FmwBootloaderStart = 251 # Start upload and recieve fwinfo + IDIMMES_COM_C_FmwBootloaderWrite = 252 # Write chunk + IDIMMES_COM_C_FmwBootloaderEnd = 253 # upload end. Finish operations and reboot + IDIMMES_COM_C_GotoApp = 254 #Goto APP + @classmethod + def values(cls) -> List[int]: + """Список всех значений""" + return [status.value for status in cls] + + @classmethod + def names(cls) -> List[str]: + """Список всех имен""" + return [status.name for status in cls] + + @classmethod + def dict(cls) -> Dict[str, int]: + """Словарь имя -> значение""" + return {status.name: status.value for status in cls} + + @classmethod + def get_by_value(cls, value: int): + """Получить статус по значению""" + for status in cls: + if status.value == value: + return status + return None + +''' +moduleTypeList = { +"TYPE_TST" : "ATmega328PB", +"TYPE_1W1" : "ATmega328PB", +"TYPE_TTT" : "ATmega2560", +"TYPE_2ST" : "ATmega2560", +"TYPE_4DC" : "ATmega2560", +"TYPE_RM8" : "ATmega2560", +} +''' +class IdiBusSpeedCodes(Enum): + CODE_19200B = 19200, + CODE_500K = 576000, + CODE_2400B = 2400, + CODE_9600B = 9600, + CODE_115200B = 115200, + CODE_250K = 256000, + CODE_1M = 921600, + CODE_10M = 10000000 + @classmethod + def values(cls) -> List[int]: + """Список всех значений""" + return [status.value for status in cls] + + @classmethod + def names(cls) -> List[int]: + """Список всех имен""" + return [status.name for status in cls] + + @classmethod + def dict(cls) -> Dict[str, int]: + """Словарь имя -> значение""" + return {status.name: status.value for status in cls} + + @classmethod + def get_by_value(cls, value: str): + """Получить статус по значению""" + for status in cls: + if status.value == value: + return status + return None \ No newline at end of file diff --git a/IdiBusMaster.py b/IdiBusMaster.py new file mode 100644 index 0000000..70c7db3 --- /dev/null +++ b/IdiBusMaster.py @@ -0,0 +1,118 @@ +from .IdiBusDefs import comFuncList +from .IdiBusParse import IdiBusSMESParser +from .IdiBusSerialLine import IdiBusSerialLine +from .IdiBusMessage import * + + +class IdiBusModule(): + def __init__(self, addr, line:IdiBusSerialLine): + self.addr = addr + self.last_mmes_err = 0 + if not isinstance(line, IdiBusSerialLine): + raise TypeError("Line is not an IdiBusSerialLine") + self.line = line + def startLongOp(self): + self.line.timeout = 3 + def endLongOp(self): + self.line.timeout = self.line.defaultTimeout + def c_Init(self): + return self._sendMMESG(comFuncList.IDIMMES_COM_C_Init.value, await_bytes=5) + def c_Shutdown(self): + return self._sendMMESG(comFuncList.IDIMMES_COM_C_ShtDown.value, await_bytes=5) + def c_Dummy(self): + return self._sendMMESG(comFuncList.IDIMMES_COM_C_Dummy.value, await_bytes=5) + def c_GotoApp(self): + return self._sendMMESG(comFuncList.IDIMMES_COM_C_GotoApp.value, await_bytes=5) + def c_FmwBootloaderInfo(self): + return self._sendMMESG(comFuncList.IDIMMES_COM_C_FmwBootloaderInfo.value, ) + def c_EnterBootloader(self, moduleType, hwRev, SN): + moduleType = bytearray(moduleType, 'ascii') + hwRev = bytearray(hwRev, 'ascii') + #hwRev[0]-=48 + #hwRev[1]-=48 + SN = bytearray(SN, 'ascii') + devdata =bytearray() + devdata.extend(moduleType) + devdata.extend(hwRev) + devdata.extend(SN) + return self._sendMMESG(comFuncList.IDIMMES_COM_C_EnterBootloader.value, data=devdata, await_bytes=5) + def c_ReadDevFullSN_MS(self): + return self._sendMMESG(comFuncList.IDIMMES_COM_C_ReadDevFullSN_MS.value) + def c_BootloaderStart(self, data): + self.startLongOp() + rcv = self._sendMMESG(comFuncList.IDIMMES_COM_C_FmwBootloaderStart.value, data=data, await_bytes=5) + self.endLongOp() + return rcv + def c_BootloaderWrite(self, data): + self.startLongOp() + rcv = self._sendMMESG(comFuncList.IDIMMES_COM_C_FmwBootloaderWrite.value, data=data, await_bytes=5) + self.endLongOp() + return rcv + def c_BootloaderEnd(self): + self.startLongOp() + rcv = self._sendMMESG(comFuncList.IDIMMES_COM_C_FmwBootloaderEnd.value, await_bytes=5) + self.endLongOp() + return rcv + def _sendMMESG(self, func, data=None, await_bytes = 0): + if not func in comFuncList.values(): + self.last_mmes_err = "BadFunc" + return 1 + init_com = IdiBusMMESG( + self.addr, + IdiBusMMPS(IdiBusMMPS.typeMMESG).getValue(), + func, + ) + #Append data if present + if not data is None: + init_com.addData(data) + + self.line.writeData(init_com.getMsg()) + + #Set max packet size if not specified + if await_bytes == 0: + await_bytes = self.line.maxPacketSize + #read all data + SMES = self.line.readData(await_bytes) + #No response from slave + if len(SMES) == 0: + self.last_mmes_err = "NoResponse" + return 1 + if len(SMES) < 5 or len(SMES) > 270: + self.last_mmes_err = "InvalidPacketSize" + return 1 + else: + self.parser = IdiBusSMESParser(SMES) + if self.parser.validate() == -1: + self.last_mmes_err = "Corrupted" + return 1 + else: + #Packet good + return 0 + def _sendMMES(self,msg:IdiBusMMES, await_bytes=0 ): + #send it + #print(f"raw message : {msg.getMsg().hex()}") + self.line.writeData(msg.getMsg()) + # read all data + SMES = self.line.readData(await_bytes) + #print(f"raw answer : {SMES.hex()}") + # No response from slave + if len(SMES) == 0: + self.last_mmes_err = "NoResponse" + return 1 + if len(SMES) < 5 or len(SMES) > 270: + self.last_mmes_err = "InvalidPacketSize" + return 1 + else: + self.parser = IdiBusSMESParser(SMES) + if self.parser.validate() == -1: + self.last_mmes_err = "Corrupted" + return 1 + else: + # Packet good + self.last_mmes_err = self.parser.getErrcode() + return 0 +class IdiBusDevice(): + def __init__(self, num): + self.num = num + self.errors = {} + self.commands = {} \ No newline at end of file diff --git a/IdiBusMessage.py b/IdiBusMessage.py new file mode 100644 index 0000000..e7eec53 --- /dev/null +++ b/IdiBusMessage.py @@ -0,0 +1,86 @@ +from .CRC16 import modbus_crc16 + +#Fast Master message type -I hate this- + +#Usage +#1. Construct message +#2. Append data if needed +#3. get message body (CRC will be added automatically) +class IdiBusMMESG: + def __init__(self, address, mmps, cmd): + self.MMESG = bytearray() + self.MMESG.extend(int(address).to_bytes(1, byteorder="little")) + self.MMESG.extend(int(mmps).to_bytes(1, byteorder="little")) + self.MMESG.extend(int(cmd).to_bytes(1, byteorder="little")) + + def addData(self, data): + self.MMESG.extend(data) + #Appends CRC16 and returns whole packet + def getMsg(self): + crc = modbus_crc16(self.MMESG) + self.MMESG.extend(crc) + return self.MMESG + + def __repr__(self): + return f"MMESG({self.MMESG})" + +#Usage +#1. Construct message +#2. Append data if needed +#3. get message body (CRC will be added automatically) +#Main Master message type +class IdiBusMMES: + def __init__(self, address, mmps, dev, ch,cmd): + self.MMES = bytearray() + if (cmd <=15): + mmps |= (cmd << 2) + self.MMES.extend(address.to_bytes(1, byteorder="little")) + self.MMES.extend(mmps.to_bytes(1, byteorder="little")) + self.MMES.extend(dev.to_bytes(1, byteorder="little")) + self.MMES.extend(ch.to_bytes(1, byteorder="little")) + if (cmd > 15): + self.MMES.extend(cmd.to_bytes(1, byteorder="little")) + def addData(self, data): + self.MMES.extend(bytearray(data)) + + def getMsg(self): + crc = modbus_crc16(self.MMES) + self.MMES.extend(crc) + return self.MMES + + def __repr__(self): + return f"Addr:{self.getAddr()}" + + +#Slave message constructor +class IdiBusSMES: + def __init__(self, address, spdu, errcode): + self.SMES = bytearray() + self.SMES.extend(address.to_bytes(1, byteorder="little")) + self.SMES.extend(spdu.to_bytes(1, byteorder="little")) + self.SMES.extend(errcode.to_bytes(1, byteorder="little")) + def addData(self, data): + self.SMES.extend(bytearray(data)) + + def getMsg(self): + crc = modbus_crc16(self.SMES) + self.SMES.extend(crc) + return self.SMES + + def __repr__(self): + return f"SMES: {self.SMES}" + + +class IdiBusMMPS: + typeMMESG = 1 + typeMMES = 0 + def __init__(self, MesType = typeMMESG,): + self.LongMessage = 0 + self.MesType = MesType + self.CommandType = 0 + self.AlarmFrame = 0 + self.EncriptedAES = 0 + def getValue(self): + val = 0 + val |= (self.LongMessage << 0) | (self.MesType << 1) | (self.CommandType << 2) | (self.AlarmFrame << 6) | (self.EncriptedAES << 7) + return val diff --git a/IdiBusParse.py b/IdiBusParse.py new file mode 100644 index 0000000..d1605ae --- /dev/null +++ b/IdiBusParse.py @@ -0,0 +1,113 @@ +import struct +from .CRC16 import modbus_crc16 + + +def get_bit(byte, position): + return (byte >> position) & 1 + + +class IdiBusMES: + def __init__(self, data: bytearray): + if len(data) == 0: + raise TypeError("Data is an empty bytearray") + self.data = data + def validate(self): + if self.getCRC() != modbus_crc16(self.data[0:-2]): + return -1 + else: + return 0 + def getAddress(self): + return int(self.data[0]) + def getCRC(self): + return self.data[-2::] + def __repr__(self): + return f"Addr:{self.getAddress()}, Body:{self.data[1::]}" +class IdiBusMMESParser(IdiBusMES): + def __init__(self, data: bytearray): + super().__init__(data) + def getMMPS(self): + return self.data[1] + def getLongMes(self): + return get_bit(int(self.data[1]), 0) + def getMesType(self): + return get_bit(int(self.data[1]), 1) + def getAlarm(self): + return get_bit(int(self.data[1]), 6) + def getEncStatus(self): + return get_bit(int(self.data[1]), 7) + def getDEV(self): + return int(self.data[2]) + def getCHAN(self): + return int(self.data[3]) + def getCMD(self): + return int(self.data[4]) + def getData(self): + return self.data[4:-2] + +class IdiBusMMESGParser(IdiBusMES): + def __init__(self, data: bytearray): + super().__init__(data) + def getMMPS(self): + return int(self.data[1]) + def getCMD(self): + return int(self.data[2]) + def getData(self): + return self.data[3:-2] +class IdiBusSMESParser(IdiBusMES): + def __init__(self, data: bytearray): + super().__init__(data) + def getSPDU(self): + return int(self.data[1]) + def getErrorBit(self): + return get_bit(self.data[1], 0) + def getLongMessage(self): + return get_bit(self.data[1], 1) + def getErrcode(self): + return int(self.data[2]) + def getData(self): + return self.data[3:-2] + def getDataStruct(self, format_string): + try: + unpacked = struct.unpack(format_string, self.getData()) + return unpacked + except struct.error as e: + print(f"Parse error: {e}") + return None + except UnicodeDecodeError as e: + print(f"Parse error: {e}") + return None +class IdiBusDeviceInfoParser(): + # Формат: 'B H I f 10s' + # B - unsigned char (1 байт) + # H - unsigned short (2 байта) + # I - unsigned int (4 байта) + # f - float (4 байта) + # 10s - строка из 10 байт + format_string = '