fdl.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. # -*- coding: utf-8 -*-
  2. #
  3. # PROFIBUS - Layer 2 - Fieldbus Data Link (FDL)
  4. #
  5. # Copyright (c) 2013-2020 Michael Buesch <m@bues.ch>
  6. #
  7. # Licensed under the terms of the GNU General Public License version 2,
  8. # or (at your option) any later version.
  9. #
  10. from __future__ import division, absolute_import, print_function, unicode_literals
  11. from pyprofibus.compat import *
  12. from pyprofibus.phy import *
  13. from pyprofibus.util import *
  14. __all__ = [
  15. "FdlError",
  16. "FdlFCB",
  17. "FdlTransceiver",
  18. "FdlTelegram",
  19. "FdlTelegram_var",
  20. "FdlTelegram_stat8",
  21. "FdlTelegram_stat0",
  22. "FdlTelegram_token",
  23. "FdlTelegram_ack",
  24. "FdlTelegram_FdlStat_Req",
  25. "FdlTelegram_FdlStat_Con",
  26. "FdlTelegram_Ident_Req",
  27. "FdlTelegram_Lsap_Req",
  28. ]
  29. class FdlError(ProfibusError):
  30. __slots__ = (
  31. )
  32. class FdlFCB():
  33. """FCB context, per slave.
  34. """
  35. __slots__ = (
  36. "__fcb",
  37. "__fcv",
  38. "__fcbWaitingReply",
  39. "__fcbEnabled",
  40. )
  41. def __init__(self, enable = False):
  42. self.resetFCB()
  43. self.enableFCB(enable)
  44. def resetFCB(self):
  45. self.__fcb = 1
  46. self.__fcv = 0
  47. self.__fcbWaitingReply = False
  48. def enableFCB(self, enabled = True):
  49. self.__fcbEnabled = bool(enabled)
  50. def FCBnext(self):
  51. self.__fcb ^= 1
  52. self.__fcv = 1
  53. self.__fcbWaitingReply = False
  54. def enabled(self):
  55. return self.__fcbEnabled
  56. def bitIsOn(self):
  57. return self.__fcb != 0
  58. def bitIsValid(self):
  59. return self.__fcv != 0
  60. def setWaitingReply(self):
  61. self.__fcbWaitingReply = True
  62. def handleReply(self):
  63. if self.__fcbWaitingReply:
  64. self.FCBnext()
  65. def __repr__(self):
  66. return ("FdlFCB(en=%s, fcb=%d, fcv=%d, wait=%s)" % (
  67. str(self.__fcbEnabled),
  68. self.__fcb,
  69. self.__fcv,
  70. str(self.__fcbWaitingReply)))
  71. class FdlTransceiver(object):
  72. __slots__ = (
  73. "phy",
  74. "__rxFilter",
  75. )
  76. def __init__(self, phy):
  77. self.phy = phy
  78. self.setRXFilter(None)
  79. def setRXFilter(self, newFilter):
  80. if newFilter is None:
  81. newFilter = range(0, FdlTelegram.ADDRESS_MASK + 1)
  82. self.__rxFilter = set(newFilter)
  83. def __checkRXFilter(self, telegram):
  84. if telegram.da is None:
  85. # Accept telegrams without DA field.
  86. return True
  87. # Accept the packet, if it's in the RX filter.
  88. return (telegram.da & FdlTelegram.ADDRESS_MASK) in self.__rxFilter
  89. def poll(self, timeout=0.0):
  90. ok, telegram = False, None
  91. reply = self.phy.poll(timeout)
  92. if reply is not None:
  93. telegram = FdlTelegram.fromRawData(reply)
  94. if self.__checkRXFilter(telegram):
  95. ok = True
  96. return (ok, telegram)
  97. # Send an FdlTelegram.
  98. def send(self, fcb, telegram):
  99. srd = False
  100. if telegram.fc & FdlTelegram.FC_REQ:
  101. func = telegram.fc & FdlTelegram.FC_REQFUNC_MASK
  102. srd = func in (FdlTelegram.FC_SRD_LO,
  103. FdlTelegram.FC_SRD_HI,
  104. FdlTelegram.FC_SDA_LO,
  105. FdlTelegram.FC_SDA_HI,
  106. FdlTelegram.FC_DDB,
  107. FdlTelegram.FC_FDL_STAT,
  108. FdlTelegram.FC_IDENT,
  109. FdlTelegram.FC_LSAP)
  110. telegram.fc &= ~(FdlTelegram.FC_FCB | FdlTelegram.FC_FCV)
  111. if fcb.enabled():
  112. if fcb.bitIsOn():
  113. telegram.fc |= FdlTelegram.FC_FCB
  114. if fcb.bitIsValid():
  115. telegram.fc |= FdlTelegram.FC_FCV
  116. if srd:
  117. fcb.setWaitingReply()
  118. else:
  119. fcb.FCBnext()
  120. self.phy.send(telegram, srd)
  121. class FdlTelegram(object):
  122. # Start delimiter
  123. SD1 = 0x10 # No DU
  124. SD2 = 0x68 # Variable DU
  125. SD3 = 0xA2 # 8 octet fixed DU
  126. SD4 = 0xDC # Token telegram
  127. SC = 0xE5 # Short ACK
  128. # End delimiter
  129. ED = 0x16
  130. # Addresses
  131. ADDRESS_MASK = 0x7F # Address value mask
  132. ADDRESS_EXT = 0x80 # DAE/SAE present
  133. ADDRESS_MCAST = 127 # Multicast/broadcast address
  134. # DAE/SAE (Address extension)
  135. AE_EXT = 0x80 # Further extensions present
  136. AE_SEGMENT = 0x40 # Segment address
  137. AE_ADDRESS = 0x3F # Address extension number
  138. # Frame Control
  139. FC_REQ = 0x40 # Request
  140. # Request Frame Control function codes (FC_REQ set)
  141. FC_REQFUNC_MASK = 0x0F
  142. FC_TIME_EV = 0x00 # Time event
  143. FC_SDA_LO = 0x03 # SDA low prio
  144. FC_SDN_LO = 0x04 # SDN low prio
  145. FC_SDA_HI = 0x05 # SDA high prio
  146. FC_SDN_HI = 0x06 # SDN high prio
  147. FC_DDB = 0x07 # Req. diagnosis data
  148. FC_FDL_STAT = 0x09 # Req. FDL status
  149. FC_TE = 0x0A # Actual time event
  150. FC_CE = 0x0B # Actual counter event
  151. FC_SRD_LO = 0x0C # SRD low prio
  152. FC_SRD_HI = 0x0D # SRD high prio
  153. FC_IDENT = 0x0E # Req. ident
  154. FC_LSAP = 0x0F # Req. LSAP status
  155. # Frame Control Frame Count Bit (FC_REQ set)
  156. FC_FCV = 0x10 # Frame Count Bit valid
  157. FC_FCB = 0x20 # Frame Count Bit
  158. # Response Frame Control function codes (FC_REQ clear)
  159. FC_RESFUNC_MASK = 0x0F
  160. FC_OK = 0x00 # Positive ACK
  161. FC_UE = 0x01 # User error
  162. FC_RR = 0x02 # Resource error
  163. FC_RS = 0x03 # No service activated
  164. FC_DL = 0x08 # Res. data low
  165. FC_NR = 0x09 # ACK negative
  166. FC_DH = 0x0A # Res. data high
  167. FC_RDL = 0x0C # Res. data low, resource error
  168. FC_RDH = 0x0D # Res. data high, resource error
  169. # Response Frame Control Station Type (FC_REQ clear)
  170. FC_STYPE_MASK = 0x30
  171. FC_SLAVE = 0x00 # Slave station
  172. FC_MNRDY = 0x10 # Master, not ready to enter token ring
  173. FC_MRDY = 0x20 # Master, ready to enter token ring
  174. FC_MTR = 0x30 # Master, in token ring
  175. # Delimiter to size converstion table.
  176. delim2size = {
  177. SD1 : 6,
  178. SD3 : 14,
  179. SD4 : 3,
  180. SC : 1,
  181. }
  182. __slots__ = (
  183. "sd",
  184. "haveLE",
  185. "da",
  186. "sa",
  187. "fc",
  188. "dae",
  189. "sae",
  190. "du",
  191. "haveFCS",
  192. "ed",
  193. )
  194. @classmethod
  195. def getSizeFromRaw(cls, data):
  196. dataLen = len(data)
  197. if dataLen < 1:
  198. return -1 # Telegram too short.
  199. sd = data[0]
  200. if sd in cls.delim2size:
  201. return cls.delim2size[sd]
  202. if sd == cls.SD2:
  203. if dataLen < 3:
  204. return -1 # Telegram too short.
  205. le = data[1]
  206. if data[2] != le:
  207. return -1 # Repeated length field mismatch.
  208. if le < 3 or le > 249:
  209. return -1 # Invalid length field.
  210. return le + 6
  211. return -1 # Unknown start delimiter.
  212. def __init__(self, sd, haveLE=False, da=None, sa=None,
  213. fc=None, dae=b"", sae=b"", du=None,
  214. haveFCS=False, ed=None):
  215. self.sd = sd
  216. self.haveLE = haveLE
  217. self.da = (da & FdlTelegram.ADDRESS_MASK) if da is not None else None
  218. self.sa = (sa & FdlTelegram.ADDRESS_MASK) if sa is not None else None
  219. self.fc = fc
  220. self.dae = dae
  221. self.sae = sae
  222. self.du = du
  223. self.haveFCS = haveFCS
  224. self.ed = ed
  225. if self.haveLE:
  226. assert(self.du is not None)
  227. def __repr__(self):
  228. def sdVal(val):
  229. try:
  230. return {
  231. FdlTelegram.SD1 : "SD1",
  232. FdlTelegram.SD2 : "SD2",
  233. FdlTelegram.SD3 : "SD3",
  234. FdlTelegram.SD4 : "SD4",
  235. FdlTelegram.SC : "SC",
  236. }[val]
  237. except KeyError:
  238. return intToHex(val)
  239. return ("FdlTelegram(sd=%s, haveLE=%s, da=%s, sa=%s, "
  240. "fc=%s, dae=%s, sae=%s, du=%s, haveFCS=%s, ed=%s)" % (
  241. sdVal(self.sd),
  242. boolToStr(self.haveLE),
  243. intToHex(self.da),
  244. intToHex(self.sa),
  245. intToHex(self.fc),
  246. bytesToHex(self.dae),
  247. bytesToHex(self.sae),
  248. bytesToHex(self.du),
  249. boolToStr(self.haveFCS),
  250. intToHex(self.ed)))
  251. # Get real length of DU field
  252. def getRealDuLen(self):
  253. return len(self.du) + len(self.dae) + len(self.sae)
  254. @staticmethod
  255. def calcFCS(data):
  256. return sum(data) & 0xFF
  257. def getRawData(self):
  258. data = bytearray()
  259. if self.haveLE:
  260. le = 3 + self.getRealDuLen()
  261. data.append(self.sd)
  262. data.append(le)
  263. data.append(le)
  264. data.append(self.sd)
  265. if self.da is not None:
  266. data.append((self.da | FdlTelegram.ADDRESS_EXT) if self.dae
  267. else self.da)
  268. if self.sa is not None:
  269. data.append((self.sa | FdlTelegram.ADDRESS_EXT) if self.sae
  270. else self.sa)
  271. if self.fc is not None:
  272. data.append(self.fc)
  273. assert isinstance(self.dae, (bytes, bytearray))
  274. data.extend(self.dae)
  275. assert isinstance(self.sae, (bytes, bytearray))
  276. data.extend(self.sae)
  277. if self.du is not None:
  278. assert isinstance(self.du, (bytes, bytearray))
  279. data.extend(self.du)
  280. if self.haveFCS:
  281. if self.haveLE:
  282. fcs = self.calcFCS(data[4:])
  283. else:
  284. fcs = self.calcFCS(data[1:])
  285. data.append(fcs)
  286. if self.ed is not None:
  287. data.append(self.ed)
  288. return data
  289. # Extract address extension bytes from DU
  290. @staticmethod
  291. def __duExtractAe(du):
  292. ae = bytearray()
  293. while 1:
  294. if not du:
  295. raise FdlError("Address extension error: Data too short")
  296. aeByte = du[0]
  297. ae.append(aeByte)
  298. du = du[1:]
  299. if not aeByte & FdlTelegram.AE_EXT:
  300. break
  301. return (du, ae)
  302. @staticmethod
  303. def fromRawData(data):
  304. error = False
  305. try:
  306. sd = data[0]
  307. if sd == FdlTelegram.SD1:
  308. # No DU
  309. if len(data) != 6:
  310. raise FdlError("Invalid FDL packet length")
  311. if data[5] != FdlTelegram.ED:
  312. raise FdlError("Invalid end delimiter")
  313. if data[4] != FdlTelegram.calcFCS(data[1:4]):
  314. raise FdlError("Checksum mismatch")
  315. return FdlTelegram_stat0(
  316. da=data[1], sa=data[2], fc=data[3])
  317. elif sd == FdlTelegram.SD2:
  318. # Variable DU
  319. le = data[1]
  320. if data[2] != le:
  321. raise FdlError("Repeated length field mismatch")
  322. if le < 3 or le > 249:
  323. raise FdlError("Invalid LE field")
  324. if data[3] != sd:
  325. raise FdlError("Repeated SD mismatch")
  326. if data[5+le] != FdlTelegram.ED:
  327. raise FdlError("Invalid end delimiter")
  328. if data[4+le] != FdlTelegram.calcFCS(data[4:4+le]):
  329. raise FdlError("Checksum mismatch")
  330. du = data[7:7+(le-3)]
  331. if len(du) != le - 3:
  332. raise FdlError("FDL packet shorter than FE")
  333. da, sa, dae, sae = data[4], data[5], b"", b""
  334. if da & FdlTelegram.ADDRESS_EXT:
  335. du, dae = FdlTelegram.__duExtractAe(du)
  336. if sa & FdlTelegram.ADDRESS_EXT:
  337. du, sae = FdlTelegram.__duExtractAe(du)
  338. return FdlTelegram_var(
  339. da=da, sa=sa, fc=data[6], dae=dae, sae=sae, du=du)
  340. elif sd == FdlTelegram.SD3:
  341. # Static 8 byte DU
  342. if len(data) != 14:
  343. raise FdlError("Invalid FDL packet length")
  344. if data[13] != FdlTelegram.ED:
  345. raise FdlError("Invalid end delimiter")
  346. if data[12] != FdlTelegram.calcFCS(data[1:12]):
  347. raise FdlError("Checksum mismatch")
  348. du = data[4:12]
  349. da, sa, dae, sae = data[1], data[2], b"", b""
  350. if da & FdlTelegram.ADDRESS_EXT:
  351. du, dae = FdlTelegram.__duExtractAe(du)
  352. if sa & FdlTelegram.ADDRESS_EXT:
  353. du, sae = FdlTelegram.__duExtractAe(du)
  354. return FdlTelegram_stat8(
  355. da=da, sa=sa, fc=data[3], dae=dae, sae=sae, du=du)
  356. elif sd == FdlTelegram.SD4:
  357. # Token telegram
  358. if len(data) != 3:
  359. raise FdlError("Invalid FDL packet length")
  360. return FdlTelegram_token(
  361. da=data[1], sa=data[2])
  362. elif sd == FdlTelegram.SC:
  363. # ACK
  364. if len(data) != 1:
  365. raise FdlError("Invalid FDL packet length")
  366. return FdlTelegram_ack()
  367. else:
  368. raise FdlError("Invalid start delimiter")
  369. except IndexError:
  370. error = True
  371. if error:
  372. raise FdlError("Invalid FDL packet format")
  373. @classmethod
  374. def checkType(cls, telegram):
  375. return isinstance(telegram, cls)
  376. class FdlTelegram_var(FdlTelegram):
  377. __slots__ = (
  378. )
  379. def __init__(self, da, sa, fc, dae, sae, du):
  380. FdlTelegram.__init__(self, sd=FdlTelegram.SD2,
  381. haveLE=True, da=da, sa=sa, fc=fc,
  382. dae=dae, sae=sae, du=du,
  383. haveFCS=True, ed=FdlTelegram.ED)
  384. if self.getRealDuLen() > 246:
  385. raise FdlError("Invalid data length (> 246)")
  386. class FdlTelegram_stat8(FdlTelegram):
  387. __slots__ = (
  388. )
  389. def __init__(self, da, sa, fc, dae, sae, du):
  390. FdlTelegram.__init__(self, sd=FdlTelegram.SD3,
  391. da=da, sa=sa, fc=fc,
  392. dae=dae, sae=sae, du=du,
  393. haveFCS=True, ed=FdlTelegram.ED)
  394. if self.getRealDuLen() != 8:
  395. raise FdlError("Invalid data length (!= 8)")
  396. class FdlTelegram_stat0(FdlTelegram):
  397. __slots__ = (
  398. )
  399. def __init__(self, da, sa, fc):
  400. FdlTelegram.__init__(self, sd=FdlTelegram.SD1,
  401. da=da, sa=sa, fc=fc,
  402. haveFCS=True, ed=FdlTelegram.ED)
  403. class FdlTelegram_token(FdlTelegram):
  404. __slots__ = (
  405. )
  406. def __init__(self, da, sa):
  407. FdlTelegram.__init__(self, sd=FdlTelegram.SD4,
  408. da=da, sa=sa)
  409. class FdlTelegram_ack(FdlTelegram):
  410. __slots__ = (
  411. )
  412. def __init__(self):
  413. FdlTelegram.__init__(self, sd=FdlTelegram.SC)
  414. class FdlTelegram_FdlStat_Req(FdlTelegram_stat0):
  415. __slots__ = (
  416. )
  417. def __init__(self, da, sa):
  418. FdlTelegram_stat0.__init__(self, da=da, sa=sa,
  419. fc=FdlTelegram.FC_REQ |\
  420. FdlTelegram.FC_FDL_STAT)
  421. class FdlTelegram_FdlStat_Con(FdlTelegram_stat0):
  422. __slots__ = (
  423. )
  424. def __init__(self, da, sa,
  425. fc=FdlTelegram.FC_OK |
  426. FdlTelegram.FC_SLAVE):
  427. FdlTelegram_stat0.__init__(self, da=da, sa=sa, fc=fc)
  428. class FdlTelegram_Ident_Req(FdlTelegram_stat0):
  429. __slots__ = (
  430. )
  431. def __init__(self, da, sa):
  432. FdlTelegram_stat0.__init__(self, da=da, sa=sa,
  433. fc=FdlTelegram.FC_REQ |\
  434. FdlTelegram.FC_IDENT)
  435. class FdlTelegram_Lsap_Req(FdlTelegram_stat0):
  436. __slots__ = (
  437. )
  438. def __init__(self, da, sa):
  439. FdlTelegram_stat0.__init__(self, da=da, sa=sa,
  440. fc=FdlTelegram.FC_REQ |\
  441. FdlTelegram.FC_LSAP)