async def get_attached_modules(request):
    """
    On success (including an empty "modules" list if no modules are detected):
    // status: 200
    {
        "modules": [
            {
                /** name of module */
                "name": "string",
                /** model identifier (i.e. part number) */
                "model": "string",
                /** unique serial number */
                "serial": "string",
                /** current firmware version */
                "fwVersion": "string",
                /** human readable status */
                "status": "string",
                /** human readable display name */
                "displayName": "string",
            },
            // ...
        ],
    }

    On failure:
    // status: 500
    {
        "message": "..."
    }
    """
    hw = hw_from_req(request)
    if ff.use_protocol_api_v2():
        hw_mods = await hw.discover_modules()
        module_data = [
            {
                'name': mod.name(),
                'displayName': mod.display_name(),
                'port': mod.port,
                'serial': mod.device_info.get('serial'),
                'model': mod.device_info.get('model'),
                'fwVersion': mod.device_info.get('version'),
                **mod.live_data
            }
            for mod in hw_mods
        ]
    else:
        hw.discover_modules()
        hw_mods = hw.attached_modules.values()
        module_data = [
            {
                'name': mod.name(),
                'displayName': mod.display_name(),
                'port': mod.port,
                'serial': mod.device_info and mod.device_info.get('serial'),
                'model': mod.device_info and mod.device_info.get('model'),
                'fwVersion': mod.device_info
                and mod.device_info.get('version'),
                **mod.live_data
            }
            for mod in hw_mods
        ]
    return web.json_response(data={"modules": module_data},
                             status=200)