j0h

SDCard_DeleteFiles.py

j0h
Jun 7th, 2026
122
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 5.28 KB | None | 0 0
  1. #!/usr/bin/env python3
  2. import argparse
  3. import time
  4. import serial
  5.  
  6.  
  7. PORT = "/dev/ttyACM0"
  8. BAUD = 9600
  9.  
  10.  
  11. def bcc_xor(data: bytes) -> bytes:
  12. b = 0
  13. for x in data:
  14. b ^= x
  15. return f"{b:02X}".encode("ascii")
  16.  
  17.  
  18. def frame_bcc(body: bytes) -> bytes:
  19. msg = b"\x05" + body
  20. return msg + bcc_xor(msg) + b"\r"
  21.  
  22.  
  23. def printable(data: bytes) -> str:
  24. out = []
  25. for b in data:
  26. if 32 <= b <= 126:
  27. out.append(chr(b))
  28. elif b == 0x00:
  29. out.append("<NUL>")
  30. elif b == 0x05:
  31. out.append("<ENQ>")
  32. elif b == 0x06:
  33. out.append("<ACK>")
  34. elif b == 0x15:
  35. out.append("<NAK>")
  36. elif b == 0x0D:
  37. out.append("<CR>")
  38. else:
  39. out.append(f"<{b:02X}>")
  40. return "".join(out)
  41.  
  42.  
  43. def recv_until_cr(ser, timeout=2.0, limit=4096) -> bytes:
  44. end = time.time() + timeout
  45. buf = bytearray()
  46.  
  47. while time.time() < end and len(buf) < limit:
  48. b = ser.read(1)
  49. if not b:
  50. continue
  51. buf += b
  52. if b == b"\r":
  53. break
  54.  
  55. return bytes(buf)
  56.  
  57.  
  58. def strip_reply(rx: bytes) -> bytes:
  59. if rx and rx[0] in (0x06, 0x15) and len(rx) >= 6:
  60. return rx[4:-3]
  61. return b""
  62.  
  63.  
  64. def delete_body(path: str) -> bytes:
  65. path = "/" + path.strip("/")
  66. path_bytes = path.encode("ascii") + b"\x00"
  67. length = len(path_bytes)
  68. return b"FF0FC" + f"{length:03X}".encode("ascii") + path_bytes
  69.  
  70.  
  71. def delete_path(ser, path: str, debug=False) -> bool:
  72. body = delete_body(path)
  73. pkt = frame_bcc(body)
  74.  
  75. if debug:
  76. print()
  77. print("TX delete:", path)
  78. print("TX hex :", pkt.hex(" ").upper())
  79. print("TX txt :", printable(pkt))
  80.  
  81. ser.reset_input_buffer()
  82. ser.write(pkt)
  83. ser.flush()
  84.  
  85. rx = recv_until_cr(ser)
  86. data = strip_reply(rx)
  87.  
  88. if debug:
  89. print("RX hex :", rx.hex(" ").upper() if rx else "")
  90. print("RX txt :", printable(rx) if rx else "")
  91. print("DATA :", printable(data))
  92.  
  93. if rx.startswith(b"\x06"):
  94. print(f"deleted or accepted: {path}")
  95. return True
  96.  
  97. print(f"delete failed: {path}")
  98. return False
  99.  
  100.  
  101. def main():
  102. ap = argparse.ArgumentParser()
  103. ap.add_argument("port", nargs="?", default=PORT)
  104. ap.add_argument("--baud", type=int, default=BAUD)
  105. ap.add_argument("--debug", action="store_true")
  106. ap.add_argument("--yes", action="store_true")
  107. ap.add_argument(
  108. "paths",
  109. nargs="*",
  110. default=[
  111. "/FCDATA01/DATALOG/1-secLog/20260406",
  112. "/FCDATA01/DATALOG/1-secLog/20260407",
  113. "/FCDATA01/DATALOG/1-secLog/LULZ",
  114. ],
  115. )
  116. args = ap.parse_args()
  117.  
  118. print("Paths to delete:")
  119. for p in args.paths:
  120. print(" ", p)
  121.  
  122. if not args.yes:
  123. print()
  124. print("Dry run only. Add --yes to actually delete.")
  125. return
  126.  
  127. with serial.Serial(
  128. args.port,
  129. args.baud,
  130. bytesize=serial.SEVENBITS,
  131. parity=serial.PARITY_EVEN,
  132. stopbits=serial.STOPBITS_ONE,
  133. timeout=0.05,
  134. write_timeout=1.0,
  135. ) as ser:
  136. time.sleep(0.5)
  137.  
  138. for path in args.paths:
  139. delete_path(ser, path, debug=args.debug)
  140. time.sleep(0.1)
  141.  
  142.  
  143. if __name__ == "__main__":
  144. main()
  145. '''
  146. listing specified folders from SD Card in IDEC PLC.
  147. SD Card Read differs from Maintenance protocol standard command structure in that:
  148. There is no ENQ, and no BCC when asking for a listing.
  149. The Delete command however :
  150. ENQ FF0FC <LEN> <PATH> NUL BCC CR
  151.  
  152.  
  153. Paths are not returned initialy, instead you get a count of files in the path specified.
  154. So then you read the entries by reading the number of entries with the FF1FR20
  155. command, which is basicly a list command.
  156. ((There is a timeout, I havent worked out what it is exactly, think 500ms))
  157.  
  158. Each reply from FF1FR20 contains 1 entry.
  159. #Folder Example:
  160. 1000000002026040618334200920260406
  161.  
  162. 1 = type folder
  163. 00000000 = size
  164. 20260406 = date
  165. 183342 = time
  166. 009 = name length
  167. 20260406 = name
  168.  
  169. #For Files:
  170. Example:
  171. 00002D6D452026040700000401020260406_00.csv
  172. 0 = type file
  173. 0002D6D4 = size
  174. 20260407 = date
  175. 000004 = time
  176. 010 = name length
  177. 20260406_00.csv = file name
  178.  
  179. These are my current thoughts, and it seems to be working.
  180. ((I Could be confussed though, all of this was confussing.
  181. There is what I just called name length, could be the itteration number,
  182. and I have an off by one type scenario ))
  183.  
  184. So then, the delete command:
  185. FF Device
  186. 0 Continuation/end
  187. FC File/Folder delete command
  188. LEN 3-digit hex path length
  189. PATH Full SD path
  190. NUL 0x00 terminator
  191. BCC XOR checksum
  192. CR 0x0D
  193.  
  194. ENQ FF0FC <LEN> <PATH> NUL BCC CR
  195.  
  196. The working flow is:
  197. opendir() -> FR
  198. readdir() -> FR20
  199. closedir() -> FR21
  200.  
  201. remove() / rmdir() -> FC
  202.  
  203. Closedir() FR21:
  204. Failing to close a directory may result in the PLC being unable to read,
  205. but unseatig the SD Card, or just waiting, seems to resolve this. Successive reads
  206. as part of some program may fail though.
  207.  
  208.  
  209. FC Removes Files, and folders, and the files in the folder if only the folder is specified.
  210. FF0FC<len><path><NUL>
  211.  
  212. '''
  213.  
Advertisement
Add Comment
Please, Sign In to add comment