Skip to content

Deconstruct Unknown UPS megatec sort of #2795

Open
@aplayerv1

Description

@aplayerv1

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Qx protocol driverDriver based on Megatec Q<number> such as new nutdrv_qx, or obsoleted blazer and some othersVolunteers welcomeCore team members can not commit to these tasks, but community would benefit from their completiondocumentation-protocolSubmitted vendor-provided or user-discovered protocol information, or similar data (measurements...)impacts-release-2.8.2Issues reported against NUT release 2.8.2 (maybe vanilla or with minor packaging tweaks)pythonquestion

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions