j0h

sp.py

j0h
May 1st, 2026
156
0
Never
1
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 7.56 KB | None | 0 0
  1. #!/usr/bin/env python3
  2. import csv
  3. import time
  4. import os
  5. import datetime
  6. import sys
  7. from collections import defaultdict, deque
  8.  
  9. try:
  10. from fc6a import FC6AMaint
  11. except ImportError:
  12. print("fc6a module not found, downloading from GitHub...")
  13. import requests
  14. url = "https://raw.githubusercontent.com/Makerspace-Bangor/fc6a/main/src/fc6a.py"
  15. code = requests.get(url, timeout=20).text
  16. exec(code, globals())
  17. print("fc6a module loaded from GitHub.")
  18.  
  19. PLC_CONFIGS = [
  20. {
  21. "name": "HP1",
  22. "ip": "192.168.1.2",
  23. "device": "FF",
  24. "endian": "1",
  25. "registers": [
  26. ("RIO1", "D0516", "F", True),
  27. ],
  28. },
  29. {
  30. "name": "HP2",
  31. "ip": "192.168.1.3",
  32. "device": "FF",
  33. "endian": "1",
  34. "registers": [
  35. ("RIO2", "D0516", "F", True),
  36. ],
  37. },
  38. {
  39. "name": "HP3",
  40. "ip": "192.168.1.4",
  41. "device": "FF",
  42. "endian": "1",
  43. "registers": [
  44. ("RIO3", "D0516", "F", True),
  45. ],
  46. },
  47. {
  48. "name": "MCP6",
  49. "ip": "192.168.1.50",
  50. "device": "FF",
  51. "endian": "0",
  52. "registers": [
  53. ("HP1", "D0520", "W", True),
  54. ("HP2", "D0521", "W", True),
  55. ("HP3", "D0522", "W", True),
  56. ],
  57. }
  58. ]
  59.  
  60. # Fixed colors per PLC source
  61. PLC_COLORS = {
  62. "HP1": "green",
  63. "HP2": "blue",
  64. "HP3": "orange",
  65. "MCP6": "red",
  66. }
  67.  
  68. SAMPLE_INTERVAL_SECONDS = 1
  69. DISPLAY_HISTORY_POINTS = 4 * 60 * 60
  70. LOG_DIR = "."
  71.  
  72.  
  73. def get_csv_filename():
  74. date_str = datetime.date.today().strftime("%Y-%m-%d")
  75. return os.path.join(LOG_DIR, f"logs_{date_str}.csv")
  76.  
  77.  
  78. def build_combined_headers(active_plcs):
  79. source_header = ["timestamp"]
  80. register_header = ["timestamp"]
  81.  
  82. for cfg in active_plcs:
  83. for tag, _reg, _dtype, _visual in cfg["registers"]:
  84. source_header.append(cfg["name"])
  85. register_header.append(tag)
  86.  
  87. return source_header, register_header
  88.  
  89.  
  90. def ensure_csv_header(filename, source_header, register_header):
  91. if not os.path.exists(filename) or os.path.getsize(filename) == 0:
  92. with open(filename, "w", newline="") as f:
  93. writer = csv.writer(f)
  94. writer.writerow(source_header)
  95. writer.writerow(register_header)
  96.  
  97.  
  98. def read_plc_values(cfg):
  99. plc = FC6AMaint(cfg["ip"])
  100. swapped = bool(int(cfg.get("endian", "0")))
  101. data = {}
  102.  
  103. for tag, reg, dtype, _visual in cfg["registers"]:
  104. try:
  105. rnum = int(reg[1:])
  106.  
  107. if dtype == "F":
  108. val = round(plc.read_float(rnum, swapped), 2)
  109. elif dtype == "W":
  110. val = plc.read_word(rnum)
  111. elif dtype == "B":
  112. val = plc.read_bits(rnum)
  113. else:
  114. val = None
  115.  
  116. except Exception as e:
  117. ts = datetime.datetime.now().isoformat(timespec="seconds")
  118. print(
  119. f"\r{ts} Error reading {tag} from {cfg['name']}: {e}\033[K",
  120. file=sys.stderr,
  121. flush=True
  122. )
  123. val = None
  124.  
  125. data[tag] = val
  126.  
  127. return data
  128.  
  129.  
  130. def log_all_plcs(active_plcs):
  131. now = datetime.datetime.now()
  132. timestamp = now.isoformat(timespec="seconds")
  133. readings = {}
  134.  
  135. source_header, register_header = build_combined_headers(active_plcs)
  136. filename = get_csv_filename()
  137.  
  138. ensure_csv_header(filename, source_header, register_header)
  139.  
  140. row = [timestamp]
  141.  
  142. for cfg in active_plcs:
  143. row_values = read_plc_values(cfg)
  144.  
  145. readings[cfg["name"]] = {
  146. "timestamp": now,
  147. "values": row_values
  148. }
  149.  
  150. for tag, _reg, _dtype, _visual in cfg["registers"]:
  151. row.append(row_values.get(tag, ""))
  152.  
  153. with open(filename, "a", newline="") as f:
  154. csv.writer(f).writerow(row)
  155.  
  156. return readings
  157.  
  158.  
  159. def selected_visual_items(active_plcs):
  160. items = []
  161.  
  162. for cfg in active_plcs:
  163. for tag, _reg, dtype, visual in cfg["registers"]:
  164. if visual and dtype in ("F", "W", "B"):
  165. items.append((cfg["name"], tag))
  166.  
  167. return items
  168.  
  169.  
  170. def update_visuals(fig, axes, lines, history, visual_items, mdates):
  171. for ax, (plc_name, tag) in zip(axes, visual_items):
  172. points = list(history[(plc_name, tag)])
  173. line = lines[(plc_name, tag)]
  174.  
  175. if points:
  176. x_vals = [p[0] for p in points]
  177. y_vals = [p[1] for p in points]
  178.  
  179. line.set_data(x_vals, y_vals)
  180.  
  181. ax.relim()
  182. ax.autoscale_view()
  183.  
  184. ax.xaxis.set_major_locator(mdates.AutoDateLocator())
  185. ax.xaxis.set_major_formatter(
  186. mdates.DateFormatter("%Y-%m-%d %H:%M")
  187. )
  188.  
  189. fig.autofmt_xdate()
  190. fig.tight_layout()
  191. fig.canvas.draw_idle()
  192.  
  193.  
  194. def main():
  195. active_plcs = PLC_CONFIGS[:5]
  196. visual_items = selected_visual_items(active_plcs)
  197.  
  198. history = defaultdict(
  199. lambda: deque(maxlen=DISPLAY_HISTORY_POINTS)
  200. )
  201.  
  202. plt = None
  203. fig = None
  204. axes = []
  205. lines = {}
  206.  
  207. if visual_items:
  208. import matplotlib.pyplot as plt_module
  209. import matplotlib.dates as mdates
  210.  
  211. plt = plt_module
  212.  
  213. fig, axes_obj = plt.subplots(
  214. len(visual_items),
  215. 1,
  216. figsize=(11, max(3, len(visual_items) * 2.4)),
  217. sharex=True
  218. )
  219.  
  220. axes = (
  221. axes_obj.flatten().tolist()
  222. if hasattr(axes_obj, "flatten")
  223. else [axes_obj]
  224. )
  225.  
  226. for ax, (plc_name, tag) in zip(axes, visual_items):
  227. line, = ax.plot(
  228. [],
  229. [],
  230. label=f"{plc_name}:{tag}",
  231. color=PLC_COLORS.get(plc_name, None)
  232. )
  233.  
  234. lines[(plc_name, tag)] = line
  235.  
  236. ax.set_ylabel(tag)
  237. #ax.set_title(plc_name)
  238. ax.grid(True)
  239. ax.legend(loc="upper right")
  240.  
  241. axes[-1].set_xlabel("Time")
  242.  
  243. fig.suptitle("FC6A Live Trends - Last 4 Hours at 1 Hz")
  244.  
  245. plt.ion()
  246. plt.show(block=False)
  247.  
  248. print(
  249. f"Logging {len(active_plcs)} PLC(s) every "
  250. f"{SAMPLE_INTERVAL_SECONDS} second(s) into: "
  251. f"{os.path.abspath(LOG_DIR)}"
  252. )
  253.  
  254. print("CSV file pattern: logs_YYYY-MM-DD.csv")
  255.  
  256. if visual_items:
  257. print("Visualizing checked numeric registers:")
  258.  
  259. for plc_name, tag in visual_items:
  260. print(f" {plc_name}:{tag}")
  261.  
  262. else:
  263. print(
  264. "No numeric registers checked for visualization. "
  265. "Logging only."
  266. )
  267.  
  268. while True:
  269. readings = log_all_plcs(active_plcs)
  270.  
  271. if visual_items:
  272. for plc_name, tag in visual_items:
  273. reading = readings.get(plc_name)
  274.  
  275. if not reading:
  276. continue
  277.  
  278. val = reading["values"].get(tag)
  279.  
  280. if isinstance(val, (int, float)):
  281. history[(plc_name, tag)].append(
  282. (reading["timestamp"], val)
  283. )
  284.  
  285. update_visuals(
  286. fig,
  287. axes,
  288. lines,
  289. history,
  290. visual_items,
  291. mdates
  292. )
  293.  
  294. plt.pause(0.05)
  295.  
  296. time.sleep(SAMPLE_INTERVAL_SECONDS)
  297.  
  298.  
  299. if __name__ == "__main__":
  300. try:
  301. main()
  302.  
  303. except KeyboardInterrupt:
  304. print("\nExiting gracefully.")
  305.  
Advertisement
Comments
  • j0h
    j0h
    194 days
    # text 0.07 KB | 0 0
    1. all the fixes are in there, we just need to make the script gen make them.
Add Comment
Please, Sign In to add comment