Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deconstruct Unknown UPS megatec sort of #2795

Open
aplayerv1 opened this issue Feb 2, 2025 · 4 comments
Open

Deconstruct Unknown UPS megatec sort of #2795

aplayerv1 opened this issue Feb 2, 2025 · 4 comments
Labels
documentation-protocol Submitted vendor-provided or user-discovered protocol information, or similar data (measurements...) impacts-release-2.8.2 Issues reported against NUT release 2.8.2 (maybe vanilla or with minor packaging tweaks) python question Qx protocol driver Driver based on Megatec Q<number> such as new nutdrv_qx, or obsoleted blazer and some others Volunteers welcome Core team members can not commit to these tasks, but community would benefit from their completion
Milestone

Comments

@aplayerv1
Copy link

aplayerv1 commented Feb 2, 2025

Ok so I have this annoying UPS that constantly gives me Zeros on nut and it was pissing me off that I ignored it for a long while.

I captured data I reversed the bullshit from UPSmart. I am sure some of you know what I am talking about.

Now I am no expert and nut kept giving me 0 "ZEROS"

    000.0 000.0 000.0 000 00.0 0.00 00.0 00000000

on what ever driver I tried and protocol.

I used python this code

      import usb.core
      import usb.util
      import time
      
      VENDOR_ID = 0x0001    # Vendor ID of the device
      PRODUCT_ID = 0x0000   # Product ID of the device
      TIMEOUT = 60000       # Timeout in milliseconds
      RETRY_LIMIT = 5
      RETRY_DELAY = 2       # seconds
      
      def log_debug(message):
          """Helper function to log debug messages with timestamps."""
          print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - {message}")
      
      def initialize_ups(dev):
          log_debug("Initializing MEC0003 UPS...")
          setup_commands = [
              (0x80, 0x06, 0x0300, 0x0000, 255),  # Get String Descriptor
              (0x80, 0x06, 0x0303, 0x0409, 255)   # Get Specific String Descriptor
          ]
          for request in setup_commands:
              try:
                  log_debug(f"Sending setup command: {request}")
                  response = dev.ctrl_transfer(*request, TIMEOUT)
                  log_debug(f"Response: {list(response)}")
              except usb.core.USBError as e:
                  log_debug(f"[ERROR] Failed to initialize UPS with command {request}: {e}")
      
      def find_ups():
          log_debug("Searching for MEC0003 UPS device...")
          dev = usb.core.find(idVendor=VENDOR_ID, idProduct=PRODUCT_ID)
          if dev is None:
              log_debug("[ERROR] MEC0003 UPS device not found.")
              return None
      
          log_debug("MEC0003 UPS device found! Resetting and configuring...")
          dev.reset()
          
          # Detach kernel driver if necessary
          if dev.is_kernel_driver_active(0):
              log_debug("Kernel driver is active. Detaching...")
              dev.detach_kernel_driver(0)
      
          dev.set_configuration()
          cfg = dev.get_active_configuration()
          intf = cfg[(0, 0)]
          
          # Claim the interface for our use
          usb.util.claim_interface(dev, 0)
          initialize_ups(dev)
      
          # Find the interrupt endpoints (for completeness)
          ep_in = usb.util.find_descriptor(
              intf,
              custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_IN
          )
          ep_out = usb.util.find_descriptor(
              intf,
              custom_match=lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_OUT
          )
          
          if not ep_in or not ep_out:
              log_debug("[ERROR] Could not find required interrupt endpoints.")
              return None
          
          log_debug(f"Interrupt IN Endpoint: {hex(ep_in.bEndpointAddress)}")
          log_debug(f"Interrupt OUT Endpoint: {hex(ep_out.bEndpointAddress)}")
          
          return dev, ep_in, ep_out
      
      def check_device_connection(dev):
          """Check if the device is still connected and accessible."""
          try:
              dev.ctrl_transfer(0x80, 0x06, 0x0300, 0x0000, 255)
              return True
          except usb.core.USBError as e:
              log_debug(f"[ERROR] Device not connected: {e}")
              return False
      
      def send_command(dev, cmd, name):
          """
          Send a command using a control transfer.
          For string descriptor queries, this is the standard method.
          """
          # full_cmd: Use the command bytes as-is; for control transfers, we pass the parameters directly.
          log_debug(f"\nSending {name} command: {[hex(x) for x in cmd]}")
          
          for attempt in range(RETRY_LIMIT):
              if not check_device_connection(dev):
                  log_debug("[ERROR] Device is not connected. Reinitializing...")
                  result = find_ups()
                  if result is None:
                      log_debug("[ERROR] Failed to reinitialize device.")
                      return None
                  else:
                      dev, _, _ = result
      
              try:
                  # For control transfers, the parameters are:
                  # bmRequestType, bRequest, wValue, wIndex, wLength
                  # Our Megatec query uses:
                  # bmRequestType = 0x80, bRequest = 0x06, wValue = 0x0303, wIndex = 0x0409, wLength = 102
                  response = dev.ctrl_transfer(0x80, 0x06, 0x0303, 0x0409, 102, TIMEOUT)
                  log_debug(f"Raw response: {list(response)}")
                  return response
              except usb.core.USBError as e:
                  if e.errno == 110:
                      log_debug(f"[WARNING] Timeout occurred on {name} (Attempt {attempt + 1}/{RETRY_LIMIT}). Retrying...")
                      time.sleep(RETRY_DELAY)
                      continue
                  else:
                      log_debug(f"[ERROR] Control transfer failed for {name}: {e}")
                      return None
      
          log_debug(f"Max retries reached for {name}. Operation failed.")
          return None
      
      def parse_device_response(response_bytes):
          """
          Parse the raw device response.
          USB string descriptors:
            - The first byte is bLength (e.g., 96)
            - The second byte is bDescriptorType (0x03 for string)
            - The rest is UTF-16LE encoded text.
          """
          if len(response_bytes) < 2:
              log_debug("[ERROR] Response too short to parse.")
              return
      
          # Remove the first two bytes
          string_data = bytes(response_bytes[2:])
          try:
              decoded = string_data.decode('utf-16le', errors='ignore').strip('\x00')
              log_debug(f"Decoded response: {decoded}")
          except Exception as e:
              log_debug(f"[ERROR] Failed to decode response: {e}")
              return
      
          # Remove any surrounding parentheses, if present.
          cleaned = decoded.strip("()")
          log_debug(f"Cleaned response: {cleaned}")
      
          # Split the string into parts (assuming space-separated values)
          parts = cleaned.split()
          log_debug(f"Response parts: {parts}")
      
          try:
              # Based on expected format, parse the parts.
              voltage = float(parts[0]) if parts[0] else 0.0
              # parts[1] might be unused or another parameter; adjust as needed.
              temperature = float(parts[4]) if len(parts) > 4 else 0.0
              battery_status = float(parts[5]) if len(parts) > 5 else 0.0
              load = float(parts[6]) if len(parts) > 6 else 0.0
              log_debug(f"Parsed Data: Voltage: {voltage} V, Temperature: {temperature}°C, Battery: {battery_status}%, Load: {load}%")
          except ValueError as e:
              log_debug(f"[ERROR] Failed to parse numeric values: {e}")
      
      def query_all_data(dev):
          # The Megatec "All Data Query" is sent as a control transfer.
          # The command parameters based on the raw capture are:
          # bmRequestType = 0x80, bRequest = 0x06, wValue = 0x0303, wIndex = 0x0409, wLength = 102
          response = send_command(dev, [0x80, 0x06, 0x03, 0x03, 0x04, 0x09, 0x66, 0x00], "All Data Query")
          if response:
              parse_device_response(response)
          else:
              log_debug("[ERROR] No valid response received for All Data Query.")
      
      def main():
          result = find_ups()
          if result is None:
              log_debug("Device initialization failed.")
              return
          dev, ep_in, ep_out = result
          query_all_data(dev)
      
      if __name__ == "__main__":
          main()

which would give me

025-02-02 13:25:31 - Searching for MEC0003 UPS device...
2025-02-02 13:25:31 - MEC0003 UPS device found! Resetting and configuring...
2025-02-02 13:25:31 - Initializing MEC0003 UPS...
2025-02-02 13:25:31 - Sending setup command: (128, 6, 768, 0, 255)
2025-02-02 13:25:31 - Response: [4, 3, 9, 4]
2025-02-02 13:25:31 - Sending setup command: (128, 6, 771, 1033, 255)
2025-02-02 13:25:31 - Response: [96, 3, 40, 0, 48, 0, 48, 0, 48, 0, 46, 0, 48, 0, 32, 0, 48, 0, 48, 0, 48, 0, 46, 0, 48, 0, 32, 0, 48, 0, 48, 0, 48, 0, 46, 0, 48, 0, 32, 0, 48, 0, 48, 0, 48, 0, 32, 0, 48, 0, 48, 0, 46, 0, 48, 0, 32, 0, 48, 0, 46, 0, 48, 0, 48, 0, 32, 0, 48, 0, 48, 0, 46, 0, 48, 0, 32, 0, 48, 0, 48, 0, 48, 0, 48, 0, 48, 0, 48, 0, 48, 0, 48, 0, 13, 0]
2025-02-02 13:25:31 - Interrupt IN Endpoint: 0x81
2025-02-02 13:25:31 - Interrupt OUT Endpoint: 0x2
2025-02-02 13:25:31 -
Sending All Data Query command: ['0x80', '0x6', '0x3', '0x3', '0x4', '0x9', '0x66', '0x0']
2025-02-02 13:25:31 - Raw response: [96, 3, 40, 0, 48, 0, 48, 0, 48, 0, 46, 0, 48, 0, 32, 0, 48, 0, 48, 0, 48, 0, 46, 0, 48, 0, 32, 0, 48, 0, 48, 0, 48, 0, 46, 0, 48, 0, 32, 0, 48, 0, 48, 0, 48, 0, 32, 0, 48, 0, 48, 0, 46, 0, 48, 0, 32, 0, 48, 0, 46, 0, 48, 0, 48, 0, 32, 0, 48, 0, 48, 0, 46, 0, 48, 0, 32, 0, 48, 0, 48, 0, 48, 0, 48, 0, 48, 0, 48, 0, 48, 0, 48, 0, 13, 0]
2025-02-02 13:25:31 - Decoded response: (000.0 000.0 000.0 000 00.0 0.00 00.0 00000000
2025-02-02 13:25:31 - Cleaned response: 000.0 000.0 000.0 000 00.0 0.00 00.0 00000000
2025-02-02 13:25:31 - Response parts: ['000.0', '000.0', '000.0', '000', '00.0', '0.00', '00.0', '00000000']
2025-02-02 13:25:31 - Parsed Data: Voltage: 0.0 V, Temperature: 0.0°C, Battery: 0.0%, Load: 0.0%

i ran it again out of frustration

2025-02-02 13:25:38 - Searching for MEC0003 UPS device...
2025-02-02 13:25:38 - MEC0003 UPS device found! Resetting and configuring...
2025-02-02 13:25:38 - Initializing MEC0003 UPS...
2025-02-02 13:25:38 - Sending setup command: (128, 6, 768, 0, 255)
2025-02-02 13:25:38 - Response: [4, 3, 9, 4]
2025-02-02 13:25:38 - Sending setup command: (128, 6, 771, 1033, 255)
2025-02-02 13:25:38 - Response: [96, 3, 40, 0, 50, 0, 50, 0, 54, 0, 46, 0, 48, 0, 32, 0, 48, 0, 48, 0, 48, 0, 46, 0, 48, 0, 32, 0, 50, 0, 50, 0, 55, 0, 46, 0, 48, 0, 32, 0, 48, 0, 50, 0, 57, 0, 32, 0, 52, 0, 57, 0, 46, 0, 57, 0, 32, 0, 50, 0, 55, 0, 46, 0, 49, 0, 32, 0, 50, 0, 57, 0, 46, 0, 48, 0, 32, 0, 48, 0, 48, 0, 48, 0, 48, 0, 49, 0, 48, 0, 48, 0, 48, 0, 13, 0]
2025-02-02 13:25:38 - Interrupt IN Endpoint: 0x81
2025-02-02 13:25:38 - Interrupt OUT Endpoint: 0x2
2025-02-02 13:25:38 -
Sending All Data Query command: ['0x80', '0x6', '0x3', '0x3', '0x4', '0x9', '0x66', '0x0']
2025-02-02 13:25:38 - Raw response: [96, 3, 40, 0, 50, 0, 50, 0, 54, 0, 46, 0, 48, 0, 32, 0, 48, 0, 48, 0, 48, 0, 46, 0, 48, 0, 32, 0, 50, 0, 50, 0, 55, 0, 46, 0, 48, 0, 32, 0, 48, 0, 50, 0, 57, 0, 32, 0, 52, 0, 57, 0, 46, 0, 57, 0, 32, 0, 50, 0, 55, 0, 46, 0, 49, 0, 32, 0, 50, 0, 57, 0, 46, 0, 48, 0, 32, 0, 48, 0, 48, 0, 48, 0, 48, 0, 49, 0, 48, 0, 48, 0, 48, 0, 13, 0]
2025-02-02 13:25:38 - Decoded response: (226.0 000.0 227.0 029 49.9 27.1 29.0 00001000
2025-02-02 13:25:38 - Cleaned response: 226.0 000.0 227.0 029 49.9 27.1 29.0 00001000
2025-02-02 13:25:38 - Response parts: ['226.0', '000.0', '227.0', '029', '49.9', '27.1', '29.0', '00001000']
2025-02-02 13:25:38 - Parsed Data: Voltage: 226.0 V, Temperature: 49.9°C, Battery: 27.1%, Load: 29.0%

Finally some results. hopefully this code can help some people in the future.
I must mention those parsed data are named incorrectly
here is csv of some data

<style> </style>
State Command
Send 80 06 03 03 09 04 00 66
Receive (226.0 000.0 227.0 034 50.1 27.1 29.0 00001000
   
Send 80 06 0d 03 09 04 00 66
Receive #   .   .  24.00   .
   
Send 80 06 03 03 09 04 00 66
Receive (225.0 000.0 226.0 035 50.0 27.1 29.0 00001000
   
Send 80 06 03 03 09 04 00 66
Receive (226.0 000.0 226.0 033 49.9 27.1 29.0 00001000
   
Send 80 06 03 03 09 04 00 66
Receive (226.0 000.0 226.0 032 49.9 27.1 29.0 00001000
   
Send 80 06 0c 03 09 04 00 66
Receive #                           V3.8
   
Send 80 06 03 03 09 04 00 66
Receive (225.0 000.0 225.0 033 49.9 27.1 29.0 00001000
   
Send 80 06 0d 03 09 04 00 66
Receive #   .   .  24.00   .
   
Send 80 06 03 03 09 04 00 66
Receive (226.0 000.0 226.0 034 50.1 27.1 29.0 00001000
   
Send 80 06 03 03 09 04 00 66
Receive (226.0 000.0 227.0 033 50.0 27.1 29.0 00001000
   
Send 80 06 03 03 09 04 00 66
Receive (226.0 000.0 225.0 034 50.1 27.1 29.0 00001000
   
Send 80 06 0c 03 09 04 00 66
Receive #                           V3.8
   
Send 80 06 03 03 09 04 00 66
Receive (224.0 000.0 225.0 034 50.3 27.1 29.0 00001000
   
Send 80 06 0d 03 09 04 00 66
Receive #   .   .  24.00   .
   
Send 80 06 03 03 09 04 00 66
Receive (226.0 000.0 226.0 034 50.0 27.1 29.0 00001000
   
Send 80 06 03 03 09 04 00 66
Receive (225.0 000.0 225.0 034 50.0 27.1 29.0 00001000
   
Send 80 06 03 03 09 04 00 66
Receive (225.0 000.0 226.0 034 50.1 27.1 29.0 00001000
   
Send 80 06 0c 03 09 04 00 66
Receive #                           V3.8
   
Send 80 06 03 03 09 04 00 66
Receive (225.0 000.0 226.0 034 50.1 27.1 29.0 00001000
   
Send 80 06 0d 03 09 04 00 66
Receive #   .   .  24.00   .
   
Send 80 06 03 03 09 04 00 66
Receive (227.0 000.0 227.0 034 49.8 27.1 29.0 00001000
   
Send 80 06 03 03 09 04 00 66
Receive (225.0 000.0 225.0 034 50.2 27.1 29.0 00001000
   
Send 80 06 03 03 09 04 00 66
Receive (225.0 000.0 226.0 032 49.8 27.1 29.0 00001000
   
Send 80 06 0c 03 09 04 00 66
Receive #
   
Send 80 06 03 03 09 04 00 66
Receive (225.0 000.0 226.0 031 49.8 27.1 29.0 00001000
   
Send 80 06 0d 03 09 04 00 66
Receive #000.0 0.0 00.00 00.0
   
Send 80 06 03 03 09 04 00 66
Receive
Send 80 06 03 03 09 04 00 66
Receive (000.0 000.0 000.0 000 00.0 0.00 00.0 00000000
   
Receive #
   
Send 80 06 03 03 09 04 00 66
Send 80 06 0c 03 09 04 00 66
Send 80 06 03 03 09 04 00 66
Receive (226.0 000.0 225.0 029 50.0 27.1 29.0 00001000
   
Send 80 06 0d 03 09 04 00 66
Receive #   .   .  24.00   .
   
Send 80 06 03 03 09 04 00 66
Receive (226.0 000.0 226.0 029 50.0 27.1 29.0 00001000
   
Send 80 06 03 03 09 04 00 66
Receive (225.0 000.0 226.0 029 50.1 27.1 29.0 00001000
   
Send 80 06 03 03 09 04 00 66
Receive (226.0 000.0 226.0 029 50.1 27.1 29.0 00001000
   
Send 80 06 0c 03 09 04 00 66
Receive #
   
Send 80 06 03 03 09 04 00 66
Receive (223.0 000.0 224.0 029 50.0 27.1 29.0 00001000
   
Send 80 06 0d 03 09 04 00 66
Receive #000.0 0.0 00.00 00.0
   
Send 80 06 03 03 09 04 00 66
Receive
Send 80 06 03 03 09 04 00 66
Receive (000.0 000.0 000.0 000 00.0 0.00 00.0 00000000
   
Receive #
   
Send 80 06 03 03 09 04 00 66

I hope that some people with better knowledge pertaining to nut can implement something. I tried 2.7 and compiled from git and nothing seemed to work.

@jimklimov jimklimov added question python Qx protocol driver Driver based on Megatec Q<number> such as new nutdrv_qx, or obsoleted blazer and some others documentation-protocol Submitted vendor-provided or user-discovered protocol information, or similar data (measurements...) impacts-release-2.8.2 Issues reported against NUT release 2.8.2 (maybe vanilla or with minor packaging tweaks) labels Feb 4, 2025
@jimklimov jimklimov added this to the 2.8.4 milestone Feb 4, 2025
@jimklimov
Copy link
Member

Thanks a lot for the data and script! Do I understand the logs above correctly, that same queries sometimes return good or bogus data or nothing at all, depending on... phase of the moon? mood? got any pattern there? :)

Is the cabling reliable (e.g. just wires, or a twisted cable wrapped into foil/net grounded by connectors to shield against EMI)?

@aplayerv1
Copy link
Author

aplayerv1 commented Feb 5, 2025

Thanks a lot for the data and script! Do I understand the logs above correctly, that same queries sometimes return good or bogus data or nothing at all, depending on... phase of the moon? mood? got any pattern there? :)

Is the cabling reliable (e.g. just wires, or a twisted cable wrapped into foil/net grounded by connectors to shield against EMI)?

I switched the cable and tried multiple others, including a very short one. I even disassembled the UPS to inspect the internals for any failed components. I also captured USB data, and while I'm just guessing, it seems like the UPS is too slow to send the data, causing the computer to read zeros instead.

The UPS needs to be initialized; otherwise, it will continuously send zeros—literally nothing but zeros.
If you need the wireshark data let me know.

@jimklimov
Copy link
Member

I think it would not hurt. Not much of a low-level developer myself, but if someone gets around to coding this - any inputs can help.

@jimklimov jimklimov added the Volunteers welcome Core team members can not commit to these tasks, but community would benefit from their completion label Feb 6, 2025
@aplayerv1
Copy link
Author

aplayerv1 commented Feb 6, 2025

here can get the pcapng for wireshark here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation-protocol Submitted vendor-provided or user-discovered protocol information, or similar data (measurements...) impacts-release-2.8.2 Issues reported against NUT release 2.8.2 (maybe vanilla or with minor packaging tweaks) python question Qx protocol driver Driver based on Megatec Q<number> such as new nutdrv_qx, or obsoleted blazer and some others Volunteers welcome Core team members can not commit to these tasks, but community would benefit from their completion
Projects
None yet
Development

No branches or pull requests

2 participants