j0h

pepsi.py

j0h
Apr 30th, 2026
144
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 6.45 KB | None | 0 0
  1. #!/usr/bin/env python3
  2. import csv, time, os, datetime
  3. from collections import defaultdict, deque
  4.  
  5. try:
  6.     from fc6a import FC6AMaint
  7. except ImportError:
  8.     print("fc6a module not found, downloading from GitHub...")
  9.     import requests
  10.     url = "https://raw.githubusercontent.com/Makerspace-Bangor/fc6a/main/src/fc6a.py"
  11.     code = requests.get(url, timeout=20).text
  12.     exec(code, globals())
  13.     print("fc6a module loaded from GitHub.")
  14.  
  15. PLC_CONFIGS = [
  16.     {
  17.         "name": "MCP6",
  18.         "ip": "192.168.1.50",
  19.         "device": "FF",
  20.         "endian": "0",
  21.         "registers": [
  22.             ("Active1", "D0520", "W", True),
  23.             ("Active2", "D0521", "W", True),
  24.             ("Active3", "D0522", "W", True),
  25.         ],
  26.     },
  27.     {
  28.         "name": "HP1",
  29.         "ip": "192.168.1.2",
  30.         "device": "FF",
  31.         "endian": "1",
  32.         "registers": [
  33.             ("LWT1", "D3938", "F", True),
  34.         ],
  35.     },
  36.     {
  37.         "name": "Hp2",
  38.         "ip": "192.168.1.3",
  39.         "device": "FF",
  40.         "endian": "1",
  41.         "registers": [
  42.             ("LWT2", "D3938", "F", True),
  43.         ],
  44.     },
  45.     {
  46.         "name": "Hp3",
  47.         "ip": "192.168.1.4",
  48.         "device": "FF",
  49.         "endian": "1",
  50.         "registers": [
  51.             ("LWT3", "D3938", "F", True),
  52.         ],
  53.     }
  54. ]
  55.  
  56. SAMPLE_INTERVAL_SECONDS = 1
  57. DISPLAY_HISTORY_POINTS = 4 * 60 * 60  # 4 hours at 1 Hz
  58. LOG_DIR = "."
  59.  
  60. def get_csv_filename():
  61.     date_str = datetime.date.today().strftime("%Y-%m-%d")
  62.     return os.path.join(LOG_DIR, f"logs_{date_str}.csv")
  63.  
  64. def build_combined_headers(active_plcs):
  65.     source_header = ["timestamp"]
  66.     register_header = ["timestamp"]
  67.  
  68.     for cfg in active_plcs:
  69.         for tag, _reg, _dtype, _visual in cfg["registers"]:
  70.             source_header.append(cfg["name"])
  71.             register_header.append(tag)
  72.  
  73.     return source_header, register_header
  74.  
  75. def ensure_csv_header(filename, source_header, register_header):
  76.     if not os.path.exists(filename) or os.path.getsize(filename) == 0:
  77.         with open(filename, "w", newline="") as f:
  78.             writer = csv.writer(f)
  79.             writer.writerow(source_header)
  80.             writer.writerow(register_header)
  81.  
  82. def read_plc_values(cfg):
  83.     plc = FC6AMaint(cfg["ip"])
  84.     swapped = bool(int(cfg.get("endian", "0")))
  85.     data = {}
  86.  
  87.     for tag, reg, dtype, _visual in cfg["registers"]:
  88.         try:
  89.             rnum = int(reg[1:])
  90.             if dtype == "F":
  91.                 val = round(plc.read_float(rnum, swapped), 2)
  92.             elif dtype == "W":
  93.                 val = plc.read_word(rnum)
  94.             elif dtype == "B":
  95.                 val = plc.read_bits(rnum)
  96.             else:
  97.                 val = None
  98.         except Exception as e:
  99.             print(f"Error reading {tag} from {cfg['name']}: {e}")
  100.             val = None
  101.  
  102.         data[tag] = val
  103.  
  104.     return data
  105.  
  106. def log_all_plcs(active_plcs):
  107.     now = datetime.datetime.now()
  108.     timestamp = now.isoformat(timespec="seconds")
  109.     readings = {}
  110.  
  111.     source_header, register_header = build_combined_headers(active_plcs)
  112.     filename = get_csv_filename()
  113.  
  114.     ensure_csv_header(filename, source_header, register_header)
  115.  
  116.     row = [timestamp]
  117.  
  118.     for cfg in active_plcs:
  119.         row_values = read_plc_values(cfg)
  120.         readings[cfg["name"]] = {"timestamp": now, "values": row_values}
  121.  
  122.         for tag, _reg, _dtype, _visual in cfg["registers"]:
  123.             row.append(row_values.get(tag, ""))
  124.  
  125.     with open(filename, "a", newline="") as f:
  126.         csv.writer(f).writerow(row)
  127.  
  128.     return readings
  129.  
  130. def selected_visual_items(active_plcs):
  131.     items = []
  132.     for cfg in active_plcs:
  133.         for tag, _reg, dtype, visual in cfg["registers"]:
  134.             # objects to plot
  135.             if visual and dtype in ("F", "W", "B"):
  136.                 items.append((cfg["name"], tag))
  137.     return items
  138.  
  139. def update_visuals(fig, axes, history, visual_items):
  140.     for ax, (plc_name, tag) in zip(axes, visual_items):
  141.         ax.clear()
  142.         points = list(history[(plc_name, tag)])
  143.         if points:
  144.             ax.plot([p[0] for p in points], [p[1] for p in points], label=f"{plc_name}:{tag}")
  145.         ax.set_ylabel(tag)
  146.         #ax.set_title(plc_name)
  147.         if ax is axes[0]:
  148.             ax.set_title(plc_name)
  149.         else:
  150.             ax.set_title("")
  151.  
  152.         ax.grid(True) # I think the grid is helpful
  153.         ax.legend(loc="upper right") # matplotlib does some annoying bs I havent fixed yet.
  154.  
  155.     if axes:
  156.         axes[-1].set_xlabel("Time")
  157.  
  158.     fig.autofmt_xdate()
  159.     fig.tight_layout()
  160.     fig.canvas.draw_idle()
  161.  
  162. def main():
  163.     # Limit to 5 PLCs max
  164.     active_plcs = PLC_CONFIGS[:5]
  165.     visual_items = selected_visual_items(active_plcs)
  166.     history = defaultdict(lambda: deque(maxlen=DISPLAY_HISTORY_POINTS))
  167.  
  168.     plt = None
  169.     fig = None
  170.     axes = []
  171.  
  172.     if visual_items:
  173.         import matplotlib.pyplot as plt_module
  174.         plt = plt_module
  175.  
  176.         fig, axes_obj = plt.subplots(
  177.             len(visual_items),
  178.             1,
  179.             figsize=(11, max(3, len(visual_items) * 2.4)),
  180.             sharex=True
  181.         )
  182.  
  183.         axes = axes_obj.flatten().tolist() if hasattr(axes_obj, "flatten") else [axes_obj]
  184.  
  185.         fig.suptitle("FC6A Live Trends - Last 4 Hours at 1 Hz")
  186.         plt.ion()
  187.         plt.show(block=False)
  188.  
  189.     print(f"Logging {len(active_plcs)} PLC(s) every {SAMPLE_INTERVAL_SECONDS} second(s) into: {os.path.abspath(LOG_DIR)}")
  190.     print(f"CSV file pattern: logs_YYYY-MM-DD.csv")
  191.  
  192.     if visual_items:
  193.         print("Visualizing checked numeric registers:")
  194.         for plc_name, tag in visual_items:
  195.             print(f"  {plc_name}:{tag}")
  196.     else:
  197.         print("No numeric registers checked for visualization. Logging only.")
  198.  
  199.     while True:
  200.         readings = log_all_plcs(active_plcs)
  201.  
  202.         if visual_items:
  203.             for plc_name, tag in visual_items:
  204.                 reading = readings.get(plc_name)
  205.                 if not reading:
  206.                     continue
  207.  
  208.                 val = reading["values"].get(tag)
  209.                 if isinstance(val, (int, float)):
  210.                     history[(plc_name, tag)].append((reading["timestamp"], val))
  211.  
  212.             update_visuals(fig, axes, history, visual_items)
  213.             plt.pause(0.05)
  214.  
  215.         time.sleep(SAMPLE_INTERVAL_SECONDS)
  216.  
  217. if __name__ == "__main__":
  218.     try:
  219.         main()
  220.     except KeyboardInterrupt:
  221.         print("\nExiting gracefully.")
Advertisement
Add Comment
Please, Sign In to add comment