From 28a2b34b281bfb7beedb00f05c0b4265729eeb9c Mon Sep 17 00:00:00 2001 From: michael_k Date: Mon, 31 Aug 2026 16:37:53 +0300 Subject: [PATCH] first commit --- .gitignore | 1 + CRC16.py | 10 ++++ Executioner.py | 50 +++++++++++++++++++ IdiBusDefs.py | 103 ++++++++++++++++++++++++++++++++++++++ IdiBusMaster.py | 118 ++++++++++++++++++++++++++++++++++++++++++++ IdiBusMessage.py | 86 ++++++++++++++++++++++++++++++++ IdiBusParse.py | 113 ++++++++++++++++++++++++++++++++++++++++++ IdiBusSerialLine.py | 50 +++++++++++++++++++ IdiBusSlave.py | 30 +++++++++++ IdiBusUtils.py | 75 ++++++++++++++++++++++++++++ __init__.py | 0 atmega_mcu.py | 54 ++++++++++++++++++++ 12 files changed, 690 insertions(+) create mode 100644 .gitignore create mode 100644 CRC16.py create mode 100644 Executioner.py create mode 100644 IdiBusDefs.py create mode 100644 IdiBusMaster.py create mode 100644 IdiBusMessage.py create mode 100644 IdiBusParse.py create mode 100644 IdiBusSerialLine.py create mode 100644 IdiBusSlave.py create mode 100644 IdiBusUtils.py create mode 100644 __init__.py create mode 100644 atmega_mcu.py 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 = '