yoossh’s diary

Microsoft MVP for Internet of Things の yoossh です。Windows系コンピュータビジョンや組み込みシステム、Azure IoTに関連するトピックスをお届けします。

Building a Cloud-Connected Digital Twin with ROS2 Robots and Microsoft Fabric

Note: Because I believe this content is valuable to the broader community, I am posting it in English in addition to Japanese so that it can reach a wider audience.

Introduction

In the field of IoT and robotics, there is a rapidly growing need to "faithfully reproduce and analyze real-world states in the cloud." This article provides a beginner-friendly walkthrough for building a "cloud-connected digital twin" that collects positional data from a ROS2-based robot in real time using Microsoft Fabric's Real-Time Intelligence (RTI), stores it long-term in OneLake, and visualizes it in real time with a Real-Time Dashboard.



Agenda

This article is structured as follows:

  1. System Overview — Understanding the architecture
  2. Step 1: Preparing the Fabric Workspace — Setting up the work environment
  3. Step 2: Converting ROS2 to MQTT — Configuring data transmission on the robot side
  4. Step 3: Receiving MQTT in the RTI EventStream — Configuring the cloud-side receiver
  5. Step 4: Storing Data in Eventhouse and KQL Database — Building the real-time DB
  6. Step 5: Long-Term Storage in OneLake — Storing in and referencing from the data lake
  7. Step 6: Real-Time Visualization with the Real-Time Dashboard — Live display



System Overview

[ROS2 Robot]
     │ MQTT conversion (ros2_mqtt_bridge, etc.)
     ▼
[Internet]
     │ MQTT over TLS
     ▼
[Microsoft Fabric RTI]
 ├─ EventStream (ingest)
 ├─ Eventhouse / KQL Database (storage)
 ├─ OneLake (long-term retention)
 └─ Real-Time Dashboard (visualization)
|



Step 1: Preparing the Microsoft Fabric Workspace

First, prepare a Microsoft Fabric tenant and workspace.

  1. Sign in to the Microsoft Fabric portal
  2. From the left menu, create a workspace via "Workspaces" → "New workspace"
  3. Assign a Fabric capacity (F SKU) or trial license

All subsequent resources (EventStream, Eventhouse, OneLake, etc.) will be managed inside this workspace.

Official documentation:



Step 2: Configuring MQTT Conversion on the ROS2 Robot

Convert ROS2 topic data into MQTT and send it to the cloud.

2-1. Installing the MQTT Bridge
# Example using ros2_mqtt_bridge
pip install paho-mqtt

# Or an OSS bridge
git clone https://github.com/yourusername/ros2_mqtt_bridge
2-2. Defining the Data to Send
ROS2 Topic Description Example MQTT Topic
/robot/pose Position / orientation (geometry_msgs/Pose) robot/001/pose
2-3. Example MQTT Publisher Script
import rclpy
from rclpy.node import Node
import paho.mqtt.client as mqtt
from paho.mqtt.packettypes import PacketTypes
from paho.mqtt.properties import Properties
import json
from geometry_msgs.msg import PoseStamped

class MQTTBridgeNode(Node):
    def __init__(self):
        super().__init__('mqtt_bridge')
        # Use MQTTv5 (required for session control via Properties and Content-Type)
        self.client = mqtt.Client(
            mqtt.CallbackAPIVersion.VERSION2,
            protocol=mqtt.MQTTv5,
        )
        self.client.tls_set()  # Enable TLS

        # Discard the session immediately on disconnect to avoid session exhaustion
        connect_props = Properties(PacketTypes.CONNECT)
        connect_props.SessionExpiryInterval = 0

        self.client.connect(
            "YOUR_EVENT_GRID_MQTT_ENDPOINT", 8883,
            clean_start=True, properties=connect_props,
        )
        self.client.loop_start()  # Run network processing on a background thread

        self.pose_sub = self.create_subscription(
            PoseStamped, '/robot/pose', self.pose_callback, 10)

    def pose_callback(self, msg):
        payload = {
            "robot_id": "robot_001",
            "timestamp": self.get_clock().now().to_msg().sec,
            "x": msg.pose.position.x,
            "y": msg.pose.position.y,
            "z": msg.pose.position.z
        }
        # By setting Content-Type to application/json,
        # Azure Event Grid routes the payload as `data` (JSON) instead of `data_base64`.
        pub_props = Properties(PacketTypes.PUBLISH)
        pub_props.ContentType = "application/json"
        self.client.publish("robot/001/pose", json.dumps(payload), properties=pub_props)

Official documentation:



Step 3: Receiving MQTT Data in the RTI EventStream

Use Microsoft Fabric's Real-Time Intelligence (RTI) EventStream to receive MQTT data from the robot.

3-1. Creating an EventStream
  1. In the Fabric workspace, choose "+ New item" → "EventStream"
  2. Give the EventStream a name (e.g., robot-telemetry-stream) and create it
3-2. Adding the MQTT Source

There are two ways to connect from EventStream to Azure Event Grid:

Option A: MQTT connector Option B: Azure Event Grid connector (recommended)
Authentication Client certificate (via Azure Key Vault) Managed identity (no certificate required)
Setup complexity High (Key Vault preparation required) Low (just pick a subscription)
Supported brokers Any MQTT broker Azure Event Grid only
Option A: MQTT Connector

⚠️ Note for trial (evaluation) tier users
The trial Namespace of Azure Event Grid has a low limit on concurrent MQTT connections (default: 1 connection per authentication name). Repeated connect/disconnect cycles cause sessions to accumulate and may result in Quota exceeded errors. If this happens, raise the limit with the following Azure CLI command:

az eventgrid namespace update \
  --name my-robot-iotns \
  --resource-group MyResource01 \
  --topic-spaces-configuration maximumClientSessionsPerAuthenticationName=3

Alternatively, wait 30 minutes to 1 hour for sessions to expire naturally, confirm MQTT: Connections has returned to 0 in Azure Portal → Event Grid namespace → "Monitoring" → "Metrics", and then retry. The production Standard tier supports up to 10,000 concurrent connections, so this limit is normally not a concern.

Because Azure Event Grid uses X.509 client certificate authentication, you must upload the EventStream-side client certificate (the .pem and .key generated in Appendix A-3) to Azure Key Vault beforehand.

Prerequisite: Uploading the certificate to Azure Key Vault

The following steps use the Azure CLI. There are two execution environments:

Environment Description
Azure Cloud Shell (recommended) Launch from the ">_" icon at the top of the Azure Portal. No installation; runs in the browser.
Local PC (macOS) Install with brew update && brew install azure-cli, then sign in with az login

Official documentation:

If Azure Key Vault has not been created yet, create it as follows. Before creating Key Vault, you need to register the resource provider in your subscription.

# Register the Microsoft.KeyVault resource provider
az provider register --namespace Microsoft.KeyVault

# Confirm registration is complete (takes 1-2 minutes to reach "Registered")
az provider show --namespace Microsoft.KeyVault --query registrationState

Once it shows "Registered", create the Key Vault.

# Create the Key Vault (skip if it already exists)
az keyvault create --name robot-key-001 --resource-group MyResource01 --location japanwest

The certificate uploaded to Azure Key Vault must be in PEM bundle format with the certificate and private key combined into a single file. The private key also needs to be converted to PKCS#8 format (BEGIN PRIVATE KEY), which Azure Key Vault requires.

# Move to /tmp/mqtt-cert-test (the A-3 working directory)
cd /tmp/mqtt-cert-test

# Convert the private key to PKCS#8 format
openssl pkcs8 -topk8 -nocrypt \
  -in robot-001-authn-ID.key \
  -out robot-001-authn-ID-pkcs8.key

# Create the PEM bundle (concatenate certificate + PKCS#8 private key)
cat robot-001-authn-ID.pem robot-001-authn-ID-pkcs8.key > robot-001-authn-ID-bundle.pem

Next, create the import policy file. To match an RSA 2048 certificate generated with --kty RSA --size 2048, set keyType to "RSA" and keySize to 2048.

{
  "secretProperties": {
    "contentType": "application/x-pem-file"
  },
  "keyProperties": {
    "exportable": true,
    "keyType": "RSA",
    "keySize": 2048,
    "reuseKey": false
  },
  "issuerParameters": {
    "name": "Unknown"
  }
}

Save the above as keyvault-cert-policy.json and import via the Azure CLI.

# Run from the project directory (where keyvault-cert-policy.json lives)
az keyvault certificate import \
  --vault-name robot-key-001 \
  --name robot-001-client-cert \
  --file /tmp/mqtt-cert-test/robot-001-authn-ID-bundle.pem \
  --policy @keyvault-cert-policy.json

Note: The user configuring the EventStream connection needs the "Key Vault Certificate User" or "Key Vault Administrator" role on the Key Vault. Assign the role from Azure Portal → Key Vault → "Access control (IAM)".

Adding the MQTT source to EventStream

  1. On the EventStream canvas, choose "Add source" → "Connect data sources" → "New (+)" → "MQTT" → "Connect"
  2. Select "New connection"
  3. Fill in the connection settings:
    • MQTT Broker URL: ssl://:8883 (e.g., ssl://my-robot-iotns.japanwest-1.ts.eventgrid.azure.net:8883)
    • Connection name: any
    • Username: the "Client Authentication Name" registered in A-4 (e.g., robot-001-authn-ID)
    • Password: leave blank
    • Select [Connect]
  4. Fill in the MQTT data source configuration:
    • Topic name: robot/001/pose
    • Version: choose V5 (or V3)
  5. Expanding "TLS/mTLS settings" shows two toggles:
    • "Trust CA certificate": enable when specifying a CA certificate to validate the broker's server certificate (not needed here)
    • "Client certificate and key": enable and specify the certificate stored in Azure Key Vault (robot-001-client-cert)

Authenticates via managed identity; no certificate management required. The following preparation is needed:

Prerequisites:

  1. Azure Portal → Event Grid namespace → left menu "Settings" → "Identity" → switch the system-assigned identity to "On" and click "Save"
  2. In the Fabric portal, click the Settings (gear icon) in the top right → in the side panel under "Governance and insights" select "Admin portal" → choose "Tenant settings" → in the "Developer settings" section enable "Service principals can use Fabric APIs"
  3. Open the Fabric workspace page, on the command bar select "Manage access" → "+ Add people or groups" → in the search box enter the Event Grid namespace name (e.g., my-robot-iotns), select the displayed "my-robot-iotns" and click "Add" → set the role to "Contributor"

Adding to EventStream:

  1. On the EventStream canvas, choose "Add source" → "Connect data sources" → "New (+)" → "Azure Event Grid Namespace" → "Connect"
  2. Fill in the connection settings:
    • Subscription: the Azure subscription that contains the Event Grid namespace
    • Namespace name: my-robot-iotns
  3. If MQTT is enabled and no routing is configured, pick the topic to use under "Namespace topic"
  4. Select "Next" → "Connect" → "Add"

Official documentation:



Step 4: Storing Data in Eventhouse and KQL Database

Store received data in an Eventhouse (KQL Database) that supports real-time search and analytics.

4-1. Creating the Eventhouse
  1. In the workspace, create an "+ New item" → "Eventhouse" (example name: robot-eventhouse)
  2. Click "Create" — a KQL database named robot-eventhouse is automatically created at the same time
4-2. Creating the KQL Table
  1. Open the robot-eventhouse you just created
  2. In the object tree on the left, select robot-eventhouse_queryset to open the query editor
  3. Paste the following into the editor and click "▶ Run"
// Pose table
.create table RobotPose (
    robot_id: string,
    timestamp: datetime,
    x: real,
    y: real,
    z: real
)
4-3. Adding the Eventhouse as an EventStream Destination and Publishing
  1. Open the robot-telemetry-stream EventStream, and from the output of the robot-telemetry-stream node choose "Add destination" → "Eventhouse"
  2. Choose a data ingestion mode:
Mode Behavior
Direct ingestion (recommended) Loads data into Eventhouse with no transformation
Event processing before ingestion (default) Applies filters / aggregations before loading

Since no transformation is needed here, choose "Direct ingestion".

  1. Enter the following and click "Save":
    • Destination name: any (e.g., robot-eventhouse-dest)
    • Workspace: the workspace you are using
    • Eventhouse: robot-eventhouse
    • KQL database: robot-eventhouse
    • Table: RobotPose (created in Step 4-2)
  1. Click the "Publish" button to activate the EventStream
4-4. Configuring the KQL Ingestion Mapping

After publishing the EventStream, configure the KQL ingestion mapping. This defines which fields in the incoming JSON map to which columns of the KQL table.

Why it's needed: Azure Event Grid wraps MQTT payloads in CloudEvents format for delivery, so the actual data lives under the data field rather than at the top level.

{
  "datacontenttype": "application/json",
  "data": { "robot_id": "robot_001", "x": 135.507, "y": 34.693, ... },
  "subject": "robot/001/pose",
  ...
}

To populate KQL columns (robot_id, x, etc.) correctly, you must specify paths under the data field such as $.data.robot_id and $.data.x.

Run the following as a single line in the robot-eventhouse_queryset query editor.

.create-or-alter table RobotPose ingestion json mapping 'RobotPose_mapping' '[{"column":"robot_id","path":"$.data.robot_id","datatype":"string"},{"column":"timestamp","path":"$.data.timestamp","datatype":"datetime","transform":"DateTimeFromUnixSeconds"},{"column":"x","path":"$.data.x","datatype":"real"},{"column":"y","path":"$.data.y","datatype":"real"},{"column":"z","path":"$.data.z","datatype":"real"}]'

Tip: "transform":"DateTimeFromUnixSeconds" automatically converts the Unix epoch seconds integer sent by the bridge script into a datetime value.

When you need to re-run this command:
This mapping only needs to be set up once — it then applies automatically to all subsequent data. You only need to re-run (or recreate) it in the following cases:

  • When the JSON field structure sent by the bridge script changes (add/remove/rename fields)
  • When the KQL table schema (column definitions) changes
  • Because .create-or-alter overwrites the existing mapping, schema changes are handled by editing the same command and re-running it.
4-5. Running the Bridge Script (sending test data)

Once the EventStream is published and the mapping is configured, simulate data from the robot. Open two terminals and run the following.

Terminal 1: Start the pose simulator

Make sure a local MQTT broker (Mosquitto) is running.

# macOS
brew services start mosquitto

# Ubuntu
sudo systemctl start mosquitto
python3 ros2_pose_simulator.py

Terminal 2: Start the MQTT bridge

# Move to /tmp/mqtt-cert-test (the A-3 working directory)
cd /tmp/mqtt-cert-test

python3 /path/to/ros2_mqtt_bridge.py \
  --fabric-host my-robot-iotns.japanwest-1.ts.eventgrid.azure.net \
  --username robot-001-authn-ID \
  --cert-file robot-001-authn-ID.pem \
  --key-file robot-001-authn-ID.key

If you see [Fabric connection successful], the connection to Azure Event Grid is established.

4-6. Verifying Operation

EventStream live preview

  1. Open robot-telemetry-stream in the Fabric portal
  2. Select the MQTT source node on the canvas and click "Data preview"
  3. While the bridge script is running, incoming messages should appear in real time

Verifying ingestion via KQL query

  1. Open robot-eventhouse from the workspace
  2. In the object tree on the left, select robot-eventhouse_queryset to open the query editor
  3. Paste the following query and click "▶ Run"
RobotPose
| where timestamp > ago(5m)
| order by timestamp desc
| take 10

If results come back, the entire EventStream → Eventhouse pipeline is working correctly.

Official documentation:



Step 5: Long-Term Storage in OneLake

Historical pose data is saved in Fabric's unified data lake OneLake for long-term management.

5-1. Using a OneLake Shortcut or Lakehouse
  1. In the workspace, create "+ New item" → "Lakehouse" (e.g., robot_datalake)
    • Leave "Lakehouse schema" at its default (checked) on the creation screen and click "Create"
  2. Enable the Eventhouse's "OneLake" "Availability":
    1. From the workspace, open robot-eventhouse
    2. Select the "Database" tab at the top
    3. In the left tree, select the robot-eventhouse database
    4. In the "Database details" panel on the right, in the "OneLake" section, switch the "Availability" toggle to "Enabled"
    5. Once the toggle is "Enabled", KQL data starts syncing to OneLake in Delta Parquet format

⏱️ Waiting for sync to complete
In the "Database details" panel, the "OneLake" section displays a "Latency" estimate (e.g., 3 hours). Immediately after enabling, "Pending size" shows the volume of unsynced data, which becomes 0 B when the sync finishes. Run the next step (referencing data from a notebook) after the sync completes.

5-2. Reading the Pose Logs Synced to OneLake

When OneLake availability is enabled, data in the RobotPose table is continuously synchronized to OneLake in Delta Parquet format. You can read it from a Fabric Notebook (Spark environment) as follows.

# Read the pose logs from OneLake in a Fabric Notebook (Spark environment)
df = spark.read.format("delta").load(
    "abfss://robot_datalake@onelake.dfs.fabric.microsoft.com/Tables/RobotPose"
)
df.show()

Official documentation:



Step 6: Real-Time Visualization with the Real-Time Dashboard

Use Microsoft Fabric's Real-Time Dashboard to visualize the robot's position in real time.

  1. In the Fabric workspace, select "+ New item" → "Real-Time Dashboard" to create a new dashboard (the default name NewRTDashboard_1 is fine)
  2. Connect a KQL Database as the data source: "+ Add data source" → choose robot-eventhouse → click "Connect"
  3. Choose "New tile" → "Map"
  4. In the "Query editor" at the bottom of the screen, paste the following query and click "Run"
// Query for real-time dashboard (trajectory over the past 5 minutes)
RobotPose
| where timestamp > ago(5m)
| where robot_id == "robot_001"
| project timestamp, x, y, z
| order by timestamp asc
  1. In the "Data" section of the left panel, choose "Define location by:" → "Latitude and longitude" and map the columns as follows:
    • Latitude: y (real)
    • Longitude: x (real)
  2. Click "Done" to save

Official documentation:



Appendix A: Configuring Azure Event Grid as an MQTT Broker

To connect an MQTT source to the EventStream in Step 3, you need an MQTT broker that both the robot (bridge script) and the Fabric EventStream can connect to. This appendix walks through using Azure Event Grid as the MQTT broker.

A-1. Creating an Event Grid Namespace
  1. Sign in to the Azure Portal and search for "Event Grid Namespaces"
  2. Click "+ Create"
  3. Enter the following, then click "Review + create" → "Create"
    • Namespace name: a unique name (3-50 chars, alphanumeric and hyphens). This article uses my-robot-iotns
    • Subscription / resource group: any
    • Region: any
A-2. Enabling the MQTT Broker and Checking the Endpoint
  1. Open the overview page of the created namespace
  2. On the overview page, click the "Disabled" link next to "MQTT broker" (you'll be redirected to the "Configuration" page)
  3. On the "Configuration" page, select "Enable MQTT broker"
  4. Click "Apply" to save the setting
  5. Return to the namespace overview page and note the "MQTT hostname" displayed there
my-robot-iotns.japanwest-1.ts.eventgrid.azure.net  (port 8883)
|

Note: Once the MQTT broker has been enabled, it cannot be disabled.

A-3. Creating a Client Certificate

Azure Event Grid uses X.509 certificate-based client authentication. First install the Step CLI and generate a local CA and client certificate. The working directory for the certificate files is /tmp/mqtt-cert-test.

# Install Step CLI (macOS)
brew install step

# Create and switch to the working directory
mkdir -p /tmp/mqtt-cert-test && cd /tmp/mqtt-cert-test

# Initialize the CA (you'll be prompted for the CA private key password and the provisioner password)
# Note: The CA private key password set here is also needed by the subsequent certificate
#       generation command, so be sure to record it.
step ca init --deployment-type standalone --name MqttAppSamplesCA \
  --dns localhost --address 127.0.0.1:443 \
  --provisioner MqttAppSamplesCAProvisioner

# Generate the client certificate (you'll be prompted for the CA password set in A-3)
# Note: ~/.step/ is the home directory subpath where step ca init placed the CA certificate.
step certificate create robot-001-authn-ID robot-001-authn-ID.pem robot-001-authn-ID.key \
  --ca ~/.step/certs/intermediate_ca.crt \
  --ca-key ~/.step/secrets/intermediate_ca_key \
  --no-password --insecure --not-after 2400h

# Get the thumbprint
step certificate fingerprint robot-001-authn-ID.pem
A-4. Registering the Client
  1. From the namespace left menu, choose "MQTT broker" → "Clients"
  2. Click "+ Client"
  3. Enter the following and click "Create"
Field Value
Client name any (e.g., robot-001)
Client Authentication Name the name used when generating the certificate (e.g., robot-001-authn-ID)
Client certificate authentication validation scheme choose Thumbprint Match

After selecting "Thumbprint Match," a Primary Thumbprint input field appears — enter the thumbprint value obtained in A-3.

A-5. Creating a Topic Space
  1. From the left menu, choose "MQTT broker" → "Topic spaces"
  2. Click "+ Topic space"
  3. Enter the following and click "Create"
    • Name: robot-topic-space
    • Topic template: robot/001/pose (enter via "+ Add topic template")
A-6. Configuring Permission Bindings

To allow two-way communication between the robot (publisher) and the Fabric EventStream (subscriber), create two bindings.

  1. From the left menu, choose "MQTT broker" → "Permission bindings"
  2. Using "+ Permission binding", create the following two entries
Binding name Client group Topic space Permission
robot-publisher-binding $all robot-topic-space Publisher
robot-subscriber-binding $all robot-topic-space Subscriber
A-7. Connecting from the Bridge Script
python3 ros2_mqtt_bridge.py \
  --fabric-host my-robot-iotns.ts.eventgrid.azure.net \
  --username robot-001-authn-ID \
  --cert-file robot-001-authn-ID.pem \
  --key-file robot-001-authn-ID.key
A-8. Verification (Azure Portal Metrics)

While the bridge script is running, you can verify message send/receive in the Azure Portal.

  1. Azure Portal → the Event Grid namespace you created → left menu "Monitoring" → "Metrics"
  2. Click "+ Add metric" and add the following metrics to monitor
Metric name What it confirms
MQTT: Connections Whether the bridge script is connected successfully
MQTT: Successful Published Messages Whether Event Grid is receiving messages
MQTT: Successful Delivered Messages Whether messages are being delivered to subscribers

If these values increase while the script is running, the connection to Azure Event Grid and message transmission are working correctly.

Official documentation:



Security Considerations

When a robot connects to the cloud over the internet, be sure to apply the following security measures.

  • Encryption in transit: Use MQTT over TLS (port 8883)
  • Authentication: Authenticate devices with X.509 certificates or SAS tokens
  • Network isolation: Protect the robot's network segment with a firewall
  • Principle of least privilege: Grant the robot only send-only permissions

Official documentation:



Summary

This article walked through building the following system step by step:

Step Content Technology
1 Prepare the Fabric workspace Microsoft Fabric
2 Convert ROS2 to MQTT ROS2 / paho-mqtt
3 Receive MQTT in the cloud RTI EventStream
4 Real-time DB ingestion Eventhouse / KQL
5 Data lake storage OneLake / Lakehouse
6 Real-time visualization RTI Real-Time Dashboard

Microsoft Fabric handles data collection, ingestion, storage, and visualization on a single platform, which dramatically reduces the cost and operational burden of building a digital twin. For preparing an MQTT broker, also see the Azure Event Grid configuration steps in the appendix. Use this article as a reference to try it with your own robots and IoT devices.

Thank you for reading. We hope this article helps you get started with connecting ROS2 robots and IoT devices to the cloud. If you have questions or feedback, please leave a comment.




Appendix B: Sample Source Code

The full source for the scripts used in this article.

B-1. ros2mqttbridge.py

A bridge script that subscribes to the /robot/pose topic on a local MQTT broker and forwards messages to the MQTT endpoint of Microsoft Fabric (Azure Event Grid).

"""
ROS2 -> Microsoft Fabric MQTT bridge

Subscribes to JSON equivalent to geometry_msgs/PoseStamped on the /robot/pose
topic of a local MQTT broker and forwards it to the MQTT endpoint of
Microsoft Fabric (Azure Event Grid) as the robot/001/pose topic.

Usage:
    python ros2_mqtt_bridge.py --fabric-host <ENDPOINT> [options]

Prerequisites:
    - ros2_pose_simulator.py is running in another terminal
    - pip install paho-mqtt
"""

import argparse
import json
import ssl

import paho.mqtt.client as mqtt
from paho.mqtt.packettypes import PacketTypes
from paho.mqtt.properties import Properties

LOCAL_TOPIC = "/robot/pose"
FABRIC_TOPIC = "robot/001/pose"
ROBOT_ID = "robot_001"


def on_local_connect(client, userdata, flags, reason_code, properties=None):
    if reason_code == 0:
        client.subscribe(LOCAL_TOPIC, qos=0)
        print(f"[Local connect OK] subscribed to '{LOCAL_TOPIC}'")
    else:
        print(f"[Local connect failed] code: {reason_code}")


def make_on_message(fabric_client: mqtt.Client):
    """Return a callback that receives messages from local MQTT and forwards to Fabric."""

    def on_message(client, userdata, msg):
        try:
            data = json.loads(msg.payload.decode())
            pos = data["pose"]["position"]
            stamp = data["header"]["stamp"]

            payload = {
                "robot_id": ROBOT_ID,
                "timestamp": stamp["sec"],
                "x": pos["x"],
                "y": pos["y"],
                "z": pos["z"],
            }
            # Setting Content-Type to application/json makes Azure Event Grid
            # route the payload as `data` (JSON) instead of `data_base64`.
            pub_props = Properties(PacketTypes.PUBLISH)
            pub_props.ContentType = "application/json"
            fabric_client.publish(FABRIC_TOPIC, json.dumps(payload), qos=1, properties=pub_props)
            print(f"[Forward] x={pos['x']:>8.4f}  y={pos['y']:>8.4f}  -> {FABRIC_TOPIC}")
        except (KeyError, json.JSONDecodeError) as e:
            print(f"[Error] failed to parse payload: {e}")

    return on_message


def on_fabric_connect(client, userdata, flags, reason_code, properties=None):
    if reason_code == 0:
        print(f"[Fabric connect OK] {userdata['host']}:{userdata['port']}")
    else:
        print(f"[Fabric connect failed] code: {reason_code}")


def main():
    parser = argparse.ArgumentParser(description="ROS2 -> Fabric MQTT bridge")
    parser.add_argument("--local-host", default="localhost")
    parser.add_argument("--local-port", type=int, default=1883)
    parser.add_argument("--fabric-host", required=True,
                        help="FQDN of the Fabric/Event Grid MQTT endpoint")
    parser.add_argument("--fabric-port", type=int, default=8883)
    parser.add_argument("--username", default=None,
                        help="Client authentication name (registered in A-4)")
    parser.add_argument("--password", default=None)
    parser.add_argument("--cert-file", default=None,
                        help="Path to the client certificate file (.pem)")
    parser.add_argument("--key-file", default=None,
                        help="Path to the client private key file (.key)")
    parser.add_argument("--no-tls", action="store_true",
                        help="Disable TLS (for testing)")
    args = parser.parse_args()

    # Fabric client (publisher)
    fabric_client = mqtt.Client(
        mqtt.CallbackAPIVersion.VERSION2,
        client_id=f"{ROBOT_ID}_bridge",
        protocol=mqtt.MQTTv5,
        userdata={"host": args.fabric_host, "port": args.fabric_port},
    )
    fabric_client.on_connect = on_fabric_connect

    if args.username:
        fabric_client.username_pw_set(args.username, args.password)

    if not args.no_tls:
        fabric_client.tls_set(
            certfile=args.cert_file,
            keyfile=args.key_file,
            cert_reqs=ssl.CERT_REQUIRED,
            tls_version=ssl.PROTOCOL_TLS_CLIENT,
        )

    # SessionExpiryInterval=0: discard the session on disconnect and avoid quota usage
    connect_props = Properties(PacketTypes.CONNECT)
    connect_props.SessionExpiryInterval = 0

    fabric_client.connect(args.fabric_host, args.fabric_port, clean_start=True, properties=connect_props)
    fabric_client.loop_start()

    # Local client (subscriber)
    local_client = mqtt.Client(
        mqtt.CallbackAPIVersion.VERSION2,
        client_id="ros2_bridge_local",
    )
    local_client.on_connect = on_local_connect
    local_client.on_message = make_on_message(fabric_client)
    local_client.connect(args.local_host, args.local_port)

    print("[Running] press Ctrl+C to stop")
    try:
        local_client.loop_forever()
    except KeyboardInterrupt:
        print("\n[Stop] received Ctrl+C")
    finally:
        fabric_client.loop_stop()
        fabric_client.disconnect()
        local_client.disconnect()


if __name__ == "__main__":
    main()
B-2. ros2posesimulator.py

A simulator that, in place of an actual ROS2 robot, publishes positions along a circular path centered on downtown Osaka to a local MQTT broker.

"""
ROS2 pose data generator (simulator)

Generates JSON payloads equivalent to geometry_msgs/PoseStamped and publishes
them to a local MQTT broker, emulating a ROS2 publisher. Use together with
ros2_mqtt_bridge.py.

Coordinate system:
    x = longitude, y = latitude
    Circular path of roughly 500 m radius centered on downtown Osaka
    (latitude 34.6937, longitude 135.5023)

Usage:
    python ros2_pose_simulator.py [options]

Prerequisites:
    - A local MQTT broker (e.g., Mosquitto) is running
      macOS: brew install mosquitto && brew services start mosquitto
      Ubuntu: sudo apt install mosquitto && sudo systemctl start mosquitto
    - pip install paho-mqtt
"""

import argparse
import json
import math
import time

import paho.mqtt.client as mqtt

LOCAL_TOPIC = "/robot/pose"
FRAME_ID = "map"

# Path parameters centered on downtown Osaka
CENTER_LAT = 34.6937   # latitude
CENTER_LON = 135.5023  # longitude
# Roughly 500 m radius (1 degree latitude ~= 111 km, so 0.005 deg ~= 556 m)
RADIUS_DEG = 0.005


def circular_path(step: int, total_steps: int = 360) -> tuple[float, float]:
    """Return (longitude, latitude) on a circular path centered on downtown Osaka."""
    angle = math.radians(step % total_steps)
    lon = CENTER_LON + RADIUS_DEG * math.cos(angle)
    lat = CENTER_LAT + RADIUS_DEG * math.sin(angle)
    return lon, lat


def build_pose_stamped(step: int) -> dict:
    """Build a JSON payload equivalent to geometry_msgs/PoseStamped.
    Store x = longitude and y = latitude.
    """
    now = time.time()
    lon, lat = circular_path(step)
    return {
        "header": {
            "stamp": {"sec": int(now), "nanosec": int((now % 1) * 1e9)},
            "frame_id": FRAME_ID,
        },
        "pose": {
            "position": {"x": round(lon, 6), "y": round(lat, 6), "z": 0.0},
            "orientation": {"x": 0.0, "y": 0.0, "z": 0.0, "w": 1.0},
        },
    }


def on_connect(client, userdata, flags, reason_code, properties=None):
    if reason_code == 0:
        print(f"[Connect OK] local broker {userdata['host']}:{userdata['port']}")
    else:
        print(f"[Connect failed] code: {reason_code}")


def main():
    parser = argparse.ArgumentParser(description="ROS2 pose simulator")
    parser.add_argument("--host", default="localhost")
    parser.add_argument("--port", type=int, default=1883)
    parser.add_argument("--interval", type=float, default=1.0,
                        help="Publish interval (seconds, default: 1.0)")
    parser.add_argument("--count", type=int, default=0,
                        help="Number of messages to send (0 = unlimited)")
    args = parser.parse_args()

    client = mqtt.Client(
        mqtt.CallbackAPIVersion.VERSION2,
        client_id="ros2_simulator",
        userdata={"host": args.host, "port": args.port},
    )
    client.on_connect = on_connect
    client.connect(args.host, args.port)
    client.loop_start()

    print(f"[Start] publishing to topic '{LOCAL_TOPIC}'... (Ctrl+C to stop)")
    step = 0
    try:
        while True:
            payload = build_pose_stamped(step)
            client.publish(LOCAL_TOPIC, json.dumps(payload), qos=0)
            pos = payload["pose"]["position"]
            print(f"[Send] step={step:>4d}  lon={pos['x']:>10.6f}  lat={pos['y']:>10.6f}")
            step += 1
            if args.count > 0 and step >= args.count:
                break
            time.sleep(args.interval)
    except KeyboardInterrupt:
        print("\n[Stop] received Ctrl+C")
    finally:
        client.loop_stop()
        client.disconnect()


if __name__ == "__main__":
    main()

Note: This article is based on information current as of 2025. Microsoft Fabric is continuously evolving, so please refer to the Microsoft Fabric official documentation for the latest details. Some features are in preview, and configuration items, behavior, and on-screen labels may change before general availability (GA).

Also note that at the time of writing, perhaps due to the preview status, the portal often mixed English and Japanese labels, which sometimes made the steps hard to follow. We hope localization and label consistency improve as the product approaches GA.

Isaac Sim上での強化学習の初心者向け完全ガイド

はじめに

前回は、「NVIDIAのIsaac SimとIsaac LabをWindows環境で試してみる」ということで、とりあえずIsaac Simでのシミュレーションと強化学習についての体験について説明しました。
今回は座学的な取り組みとして、Isaac Sim 上での強化学習について、初心者の方にも分かりやすくご説明できればと思います。とはいえ、私自身もまだ学び始めたばかりの初心者ですので、自分の勉強がてら備忘録としてまとめていきます。同じように学んでいる方の参考になれば幸いです。

目次

  1. Isaac Simとは
    • 概要
    • Isaac Labについて
  2. 強化学習の基本概念
  3. 報酬関数の設計
  4. ロボット犬から観測データを取得する方法
  5. 地面のデータ表現
  6. 実装例
  7. 学習ループの全体像
  8. よくある質問
  9. 参考リソース



Isaac Simとは

概要

Isaac Sim は、NVIDIA が提供する物理シミュレータです。ロボットの動きや環境との相互作用をコンピュータ上で正確に再現することができます。

なぜシミュレーションを使うのか?
実際のロボット シミュレーション
訓練に時間がかかる 高速に学習を繰り返せる
破損のリスクがある 何度失敗しても大丈夫
環境構築が大変 簡単に環境を変更できる
コストが高い 安価に実験できる
Isaac Labについて

Isaac Labは、Isaac Sim の上に構築された、強化学習用のフレームワークです。ロボットの学習環境を簡単に作成・管理できます。



強化学習の基本概念

強化学習とは?

強化学習 は、エージェント(ここではロボット)が試行錯誤を通じて最適な行動を学ぶ機械学習の手法です。

ロボットは「観測 → 行動 → 報酬」のループを繰り返しながら、どうすれば高い報酬を得られるかを学んでいきます。以下でそれぞれのステップを詳しく見ていきましょう。



ステップ1: 観測(Observation)

ロボットはまず、自分の状態や周囲の環境を「観測」として受け取ります。これはセンサーから得られるデータの集合です。

例えば4脚ロボットの場合、次のような情報が観測として使われます。

  • 足の現在位置・速度
  • 地面との接触状態
  • ロボット本体の移動速度・傾き

観測データが豊富であるほど、ロボットは状況をより正確に把握して行動を決定できます。観測データの種類や取得方法については「ロボット犬から観測データを取得する方法」セクションで詳しく解説します。



ステップ2: 行動選択(Action)

観測を受け取ったロボットは、「ポリシー(方針)」と呼ばれるニューラルネットワークを使って行動を決定します。

4脚ロボットの場合、行動とは各足の目標関節角度を設定することです。学習が進むにつれて、ポリシーはより良い行動(高い報酬につながる行動)を選べるようになっていきます。



ステップ3: 環境が反応

ロボットが選んだ行動をシミュレータに送ると、Isaac Sim の物理エンジンが環境を更新します。

例えば、指定した関節角度に向けて各足が動き、ロボット犬が移動します。地面の摩擦や重力なども物理的に計算されるため、現実に近い動きがシミュレートされます。地面の物理特性については「地面のデータ表現」セクションで詳しく解説します。



ステップ4: 報酬計算(Reward)

環境がどう変化したかをもとに、ロボットの行動を数値で評価するのが「報酬」です。報酬は「ロボットに何をしてほしいか」を表現するもので、強化学習の設計でもっとも重要なパートです。

報酬はプラスとマイナスの組み合わせで設計します。

良い報酬の例(4脚ロボットの前進学習):

報酬 = 0.5 × (目標速度との一致度)
      - 0.1 × (エネルギー消費)
      - 0.05 × (転ぶと減点)

プラスの報酬は「こういう行動をしてほしい」、マイナスのペナルティは「これはやめてほしい」を意味します。報酬関数の詳しい設計方法については「報酬関数の設計」セクションで解説します。



ステップ5: 学習

収集した報酬をもとに、ポリシー(ニューラルネットワーク)のパラメータを更新します。報酬が大きくなるような行動を選びやすくなるように調整することで、ロボットが徐々に上手くなっていきます。


ステップ2に戻ってループ — この観測 → 行動 → 報酬 → 学習のサイクルを何万回も繰り返すことで、ロボットは複雑なタスクを学習します。



報酬関数の設計

報酬関数の設計は、強化学習でもっとも重要かつ難しいパートです。設計が適切でないと学習がうまく進まなかったり、意図しない動作を学習してしまうことがあります。

Isaac Labが提供する標準報酬関数

Isaac Lab では isaaclab.envs.mdp モジュールに多数の報酬関数が標準提供されています。基本的にはこれらの中から目的に合ったものを選んで組み合わせます。

速度追跡(正の報酬)

関数名 説明
track_lin_vel_xy_exp 目標XY速度への追跡
track_ang_vel_z_exp 目標旋回速度への追跡

_exp とは?
exp(-error²/std²) の形で計算します。誤差が小さいほど1.0に近く、大きいほど0に近い値を返します。std パラメータで「どのくらいの誤差まで許容するか」を調整できます。

姿勢・安定性(ペナルティ)

関数名 説明
flat_orientation_l2 傾きペナルティ(転倒防止)
lin_vel_z_l2 上下方向のバウンドペナルティ
ang_vel_xy_l2 横揺れペナルティ
base_height_l2 目標高さからのずれ

関節・アクション制約(ペナルティ)

関数名 説明
joint_torques_l2 トルク消費ペナルティ(エネルギー効率)
joint_acc_l2 急激な加速ペナルティ
joint_pos_limits 関節の可動域超えペナルティ
action_rate_l2 動作の急変ペナルティ(滑らかさ)

接触(ペナルティ)

関数名 説明
undesired_contacts 足以外が地面に接触したときのペナルティ
contact_forces 接触力が大きすぎるときのペナルティ

生存報酬

関数名 説明
is_alive 転倒せず生き続けることへの報酬
is_terminated タイムアウト以外での終了に対するペナルティ
重みの設定方法

各報酬関数は報酬の「値」だけを計算します。重みは RewardTermCfgweight で設定します。

from isaaclab.managers import RewardTermCfg as RewTerm
import isaaclab.envs.mdp as mdp

@configclass
class RewardsCfg:
    # 速度追跡(正の報酬)
    track_lin_vel = RewTerm(
        func=mdp.track_lin_vel_xy_exp,
        weight=1.0,
        params={"std": 0.25, "command_name": "base_velocity"}
    )
    # エネルギーペナルティ(負の報酬)
    joint_torques = RewTerm(func=mdp.joint_torques_l2, weight=-1.0e-5)
    # 姿勢ペナルティ
    flat_orientation = RewTerm(func=mdp.flat_orientation_l2, weight=-0.2)

weight が正なら「これをやってほしい」、負なら「これはやめてほしい」という意味になります。

重み設計のガイドライン

重みに数学的に決まる正解はありませんが、実践的な指針があります。

  • 主目的に最大の重みを置く — 速度追跡など最重要項目を weight=1.0 の基準にする
  • スケールを揃える — 各項目の最終的な貢献が同程度になるよう調整する(ペナルティが強すぎると何もしなくなる)
  • 段階的に追加する — まず主目的の報酬だけで学習させ、動いたら補助ペナルティを追加する
  • 参考実装を活用する — Isaac Lab の公式タスク(ANYmal Cなど)の設定ファイルに実際の重みが載っており、参考になります
よくある失敗パターン
  • 抜け穴を見つけられる — 設計が単純すぎると、ロボットが意図しない方法で報酬を最大化する(例:前進せず回転して速度を稼ぐ)
  • ペナルティが強すぎる — エネルギーペナルティが大きすぎると「動かない」が最適解になってしまう
  • 報酬が疎(スパース)すぎる — 正しい動作をしてもなかなか報酬がもらえないと学習が進まない



ロボット犬から観測データを取得する方法

観測とは?

観測(Observation) は、ロボットが環境から受け取る情報です。センサーから得たデータと言えます。

主な観測データの種類
1. ロボット自身の状態
# 基本的な運動データ
- 線速度(Linear Velocity): 3次元 [vx, vy, vz]
  → ロボットがどの方向にどのくらいの速度で移動しているか

- 角速度(Angular Velocity): 3次元 [ωx, ωy, ωz]
  → ロボットがどのくらい回転しているか

- 重力の向き(Projected Gravity): 3次元
  → ロボットの向きを推測するために使用
  → 地球の重力がロボット座標系でどう見えるか

- 関節位置(Joint Position): 12次元(4本足 × 3関節/足)
  → 各関節の現在の角度

- 関節速度(Joint Velocity): 12次元(4本足 × 3関節/足)
  → 各関節がどのくらい速く動いているか

- 前回の行動(Last Action): 12次元
  → ロボットが前のステップで何をしたか
2. 指令情報
- 速度指令(Velocity Command): 3次元 [v_target_x, v_target_y, v_target_z]
  → 「このくらいの速度で移動してね」という指令
3. 地面との相互作用
- 高さマップ(Height Scan): 複数の点
  → ロボットの周辺の地形高さ
  → 凸凹を検出して段差を避ける

- 接触力(Contact Forces): 4本足 × 3次元
  → 各足がどのくらいの力で地面を押しているか
  → ロボットが地面に接しているかどうか
Isaac Labでの観測データ取得の実装例
# 観測設定の例
@configclass
class ObservationsCfg:
    @configclass
    class PolicyCfg(ObsGroup):
        # 基本的な運動観測
        base_lin_vel = ObsTerm(
            func=mdp.base_lin_vel,
            noise=Unoise(n_min=-0.1, n_max=0.1)  # ノイズを追加
        )
        
        base_ang_vel = ObsTerm(func=mdp.base_ang_vel)
        
        # 重力の向き
        projected_gravity = ObsTerm(func=mdp.projected_gravity)
        
        # 関節の状態
        joint_pos = ObsTerm(func=mdp.joint_pos_rel)
        joint_vel = ObsTerm(func=mdp.joint_vel_rel)
        
        # 地形センサー
        height_scan = ObsTerm(
            func=mdp.height_scan,
            params={"sensor_cfg": SceneEntityCfg("height_scanner")}
        )
        
        # 指令信号
        velocity_commands = ObsTerm(func=constant_commands)

    policy: PolicyCfg = PolicyCfg()
センサーの詳細

高さスキャナ(Height Scanner): レイキャスター(RayCaster)で周辺地形を測定。凸凹を検出して段差を避ける。

レイキャスターとは?
一般的に「レイキャスト(Ray Cast)」と呼ばれる技術を使ったセンサーです。
「レイ(ray)=光線」を仮想的に複数の方向に飛ばし、何かに当たった距離を計測する仕組みで、ゲームエンジン(Unity や Unreal Engine など)でも広く使われている技術です。
Isaac Lab では RayCaster というクラス名でこの技術がセンサーとして実装されています。
高さスキャナでは、ロボットの足元周辺に向けて複数のレイを下方向に飛ばし、地面までの距離(=高さ)を一度に測定することで、ロボット周辺の地形の形状を把握します。

接触センサー(Contact Sensor): 各足の地面との接触力を測定。足の接地状態を検出。



地面のデータ表現

地面の物理的性質

シミュレータの地面は、以下の情報で完全に定義されます:

1. ジオメトリ(形状)
# 方法1:単純な地面平面
ground = AssetBaseCfg(
    prim_path="/World/defaultGroundPlane",
    spawn=sim_utils.GroundPlaneCfg()
)

# 方法2:3D メッシュ(複雑な地形)
terrain = TerrainImporterCfg(
    prim_path="/World/ground",
    terrain_type="generator",
    terrain_generator=ROUGH_TERRAINS_CFG,  # 凸凹の地形を自動生成
    max_init_terrain_level=5,  # 難易度レベル(0-9)
)
2. 物理的性質(マテリアル)
physics_material=sim_utils.RigidBodyMaterialCfg(
    # 摩擦の計算方法
    friction_combine_mode="multiply",  # 衝突物同士の摩擦を掛け算で計算
    
    # 反発性
    restitution_combine_mode="multiply",
    
    # 静止摩擦係数(動いていない時の摩擦)
    static_friction=1.0,
    
    # 動摩擦係数(滑る時の摩擦)
    dynamic_friction=1.0,
)

摩擦とは?

摩擦が大きい地面: 足が滑りにくい、動きが安定
摩擦が小さい地面: 足が滑りやすい、移動が難しい

static_friction = 1.0, dynamic_friction = 1.0
→ 標準的なアスファルト

static_friction = 2.0, dynamic_friction = 2.0
→ ゴムのような高摩擦地面

static_friction = 0.1, dynamic_friction = 0.1
→ 氷のような低摩擦地面
シミュレーターが地面を処理する仕組み

前のセクションでは「どんな地面を作るか」を設定しました。ここでは、その設定を受けて Isaac Sim が内部でどのように地面を管理しているか を説明します。

Isaac Sim は地面を次の3つのデータに分けて管理しています。それぞれ役割が異なります:

  1. コリジョンメッシュ(Collision Mesh):物理計算用
    • ロボットと地面が「ぶつかったかどうか」を判定する形状データです
    • 接触点・法線・接触力を計算してロボットの動きに反映します
  2. ビジュアルメッシュ(Visual Mesh):表示用
    • 画面に映し出す見た目の形状データです
    • コリジョンメッシュと別管理なので、表示だけ高精細にするなど最適化できます
  3. マテリアル情報:物理特性用
    • 摩擦係数・反発係数などを保持します
    • 接触が発生したタイミングで取り出して物理計算に使用します

なぜ3つに分けるのか?

シミュレーションでは「見た目のリアルさ」と「物理計算の正確さ」と「処理速度」をバランスよく成立させる必要があります。例えば、画面表示用のメッシュは細かいほど綺麗に見えますが、そのまま衝突判定に使うと計算が重くなります。3つに分けることで、それぞれを目的に応じて最適化できます。

強化学習では特に コリジョンメッシュとマテリアル情報 がロボットの観測データ(接触力・接地状態)に直接影響するため、重要な設定項目となります。

難易度レベルによる地形のカリキュラム学習

Isaac Labでは、TerrainGeneratorCfgcurriculum モードを使うことで、ロボットの習熟度に応じて地形の難易度を段階的に上げるカリキュラム学習が実現できます。

地形グリッドの仕組み

まず、Isaac Lab の地形が「グリッド(格子状)」として管理されていることを理解するとわかりやすいです。

terrain level(行 / num_rows 凸凹地形 段差地形 斜面地形
Level 0(最も簡単) 難易度: 低 難易度: 低 難易度: 低
Level 1〜8 難易度: 中 難易度: 中 難易度: 中
Level 9(最も難しい) 難易度: 高 難易度: 高 難易度: 高

↑ 行方向が難易度レベル(num_rows で行数を指定)。列方向が sub-terrain の種類(num_cols で列数を指定)。

  • sub-terrain(列方向):地形の「種類」のことです。凸凹、段差、斜面など、どんな形の地形かを表します。sub_terrains に辞書形式で指定します。
  • terrain level(行方向):地形の「難易度」のことです。同じ種類の地形でも、Level 0 は緩やかで、Level が上がるほど険しくなります。行数(num_rows)がそのままレベル数になります。
  • max_init_terrain_level:学習開始時にロボットを何レベル目まで配置するかの上限です。低く設定するほど簡単な地形からスタートできます。
カリキュラム学習の設定例
terrain = TerrainImporterCfg(
    prim_path="/World/ground",
    terrain_type="generator",
    terrain_generator=TerrainGeneratorCfg(
        curriculum=True,              # True にすると難易度順に地形を配置
        num_rows=10,                  # 難易度レベル数(Level 0〜9)
        num_cols=20,                  # 各レベルに並べる sub-terrain の数
        difficulty_range=(0.0, 1.0),  # 難易度の範囲(0.0: 最低, 1.0: 最高)
        sub_terrains={...},           # 使用する sub-terrain の種類を指定
    ),
    max_init_terrain_level=5,         # 学習開始時は Level 0〜5 の範囲に配置
)

# ロボットが上達すると上位レベルに移動し、より難しい地形で学習を続ける

curriculum=True にすると、ロボットが簡単なレベルをクリアするにつれて自動的に難しいレベルへ進む仕組みになります。最初は max_init_terrain_level を低めに設定しておくと、初心者ロボットが無理なく学習をスタートできます。



実装例

最小限の強化学習環境

以下は、4脚ロボット(ANYmalなど)を使った実装例です:

import torch
from isaaclab.envs import ManagerBasedEnv, ManagerBasedEnvCfg
from isaaclab.managers import ObservationTermCfg as ObsTerm
from isaaclab.managers import ObservationGroupCfg as ObsGroup
from isaaclab.managers import SceneEntityCfg
from isaaclab.scene import InteractiveSceneCfg
from isaaclab.assets import AssetBaseCfg
from isaaclab.utils import configclass
from isaaclab.utils.noise import AdditiveUniformNoiseCfg as Unoise
import isaaclab.envs.mdp as mdp
import isaaclab.sim as sim_utils
from isaaclab_assets.robots.anymal import ANYMAL_C_CFG

def constant_commands(env: ManagerBasedEnv) -> torch.Tensor:
    """指令信号:ロボットに移動コマンドを送信"""
    return torch.tensor([[1, 0, 0]], device=env.device).repeat(env.num_envs, 1)

@configclass
class MySceneCfg(InteractiveSceneCfg):
    """シーン設定:ロボット、地面、センサーを定義"""
    ground = AssetBaseCfg(
        prim_path="/World/defaultGroundPlane",
        spawn=sim_utils.GroundPlaneCfg(),
    )
    
    robot = ANYMAL_C_CFG.replace(prim_path="{ENV_REGEX_NS}/Robot")

@configclass
class SimpleQuadrupedEnvCfg(ManagerBasedEnvCfg):
    """シンプルな4脚ロボット環境設定"""
    scene: MySceneCfg = MySceneCfg(num_envs=4, env_spacing=2.5)
    observations: ObservationsCfg = ObservationsCfg()
    actions: ActionsCfg = ActionsCfg()
    
    def __post_init__(self):
        self.sim.dt = 0.005
        self.decimation = 4  # 50Hz制御周期 (200Hz物理シミュレーション ÷ 4)

@configclass
class ObservationsCfg:
    """観測設定:ロボットが受け取る情報を定義"""
    @configclass
    class PolicyCfg(ObsGroup):
        """ポリシー用の観測グループ"""
        # ロボット本体の線速度 (3次元)
        base_lin_vel = ObsTerm(func=mdp.base_lin_vel)
        # ロボット本体の角速度 (3次元)
        base_ang_vel = ObsTerm(func=mdp.base_ang_vel)
        # 関節の相対位置 (12次元: 4本足 × 3関節)
        joint_pos = ObsTerm(func=mdp.joint_pos_rel)
        # 関節の相対速度 (12次元: 4本足 × 3関節)
        joint_vel = ObsTerm(func=mdp.joint_vel_rel)
        # 目標速度指令 (3次元)
        velocity_commands = ObsTerm(func=constant_commands)
    policy: PolicyCfg = PolicyCfg()

@configclass
class ActionsCfg:
    joint_pos = mdp.JointPositionActionCfg(
        asset_name="robot",
        joint_names=[".*"],
        scale=0.5,
        use_default_offset=True,
    )

def reward_tracking_lin_vel(env: ManagerBasedEnv, target_vel: torch.Tensor) -> torch.Tensor:
    """目標速度との一致度に基づいて報酬を計算"""
    current_vel = env.scene["robot"].data.root_lin_vel_b[:, :2]
    vel_error = torch.norm(current_vel - target_vel, dim=1)
    return torch.exp(-vel_error)

# 環境を作成
env_cfg = SimpleQuadrupedEnvCfg()
env = ManagerBasedEnv(cfg=env_cfg)

# 学習ループを実行
obs, _ = env.reset()
target_vel = torch.tensor([[1.0, 0.0]], device=env.device)

for step in range(10000):
    # (実装例)ランダム行動を実行
    action = torch.randn(env.num_envs, 12, device=env.device)
    
    # 環境ステップ(ManagerBasedEnv は obs と extras の2要素を返す)
    obs, extras = env.step(action)
    
    # (実装例)報酬計算
    reward = reward_tracking_lin_vel(env, target_vel)
    
    # リセット
    dones = extras.get("dones", torch.zeros(env.num_envs, dtype=torch.bool, device=env.device))
    if dones.any():
        obs, _ = env.reset()

env.close()

注:

  • このコードは構造例です。実装には policy_network(学習済みニューラルネットワーク)の追加が必要です
  • 実際のスクリプトでは AppLauncher による Isaac Sim の初期化が必要です(公式チュートリアル参照)
  • MySceneCfg にはロボット、地面、センサーを詳細に定義してください
  • 詳細な実装は公式チュートリアルを参照してください



学習ループの全体像

標準的な学習フロー

【初期化】 環境生成(シーン設定、ロボット配置、地面生成)

【メインループ:各エポック】

  1. リセット
    • 初期観測を取得: obs = env.reset()
  2. 制御ステップ(以下を複数回繰り返す)
    1. ポリシーが行動を決定: action = policy(obs)
    2. 環境にアクションを適用: obs, reward, done = env.step(action)
    3. Isaac Sim がロボットの物理状態を更新:
        • 関節トルクを計算
        • 物理シミュレーション実行
        • センサーデータ読み取り
        • 新しい状態を計算
    4. 新しい観測を受け取る
  3. 学習
    • 報酬に基づいてポリシーを更新: policy.backward(rewards)
  4. 1〜3 を繰り返す
タイミングの詳細

物理シミュレーション周期: dt = 0.005秒(200Hz)
各ステップで「物理計算 → 衝突検出 → センサー更新 → データ出力」を実行します。

制御周期: decimation = 4(50Hz)
4ステップの物理計算(0.005秒 × 4 = 0.02秒)ごとに新しい行動を受け取ります。

例:タイムライン
  T=0.000s: 行動1を受け取る
  T=0.005s: 物理計算
  T=0.010s: 物理計算
  T=0.015s: 物理計算
  T=0.020s: 観測更新、行動2を受け取る
  T=0.025s: 物理計算
  ...



よくある質問

Q1: 観測データの次元数は?

A: 環境ごとに異なります。一般的な4脚ロボット:

# 基本的な観測のサイズ
base_lin_vel:     3次元
base_ang_vel:     3次元
projected_gravity: 3次元
joint_pos:        12次元(4本足 × 3関節)
joint_vel:        12次元
velocity_commands: 3次元
height_scan:      32-64次元(スキャン点数による)
last_action:      12次元

合計: 約80-100次元
Q2: 高さスキャナと接触センサーの違いは?

A:

高さスキャナ 接触センサー
ロボット周辺の地形を遠くから測定 接触点での力を測定
段差を避けるのに有効 足の接地状態を検出
複数のレイで広く情報取得 接触点で詳細な情報取得
前進警備的な情報 反応的な情報
Q3: 物理マテリアル設定で何を変えるべき?

A: 訓練の目的に応じて:

# 滑りやすい環境での学習
static_friction=0.5, dynamic_friction=0.5
→ より難しい制御が必要、汎化性が高い

# 通常環境での学習
static_friction=1.0, dynamic_friction=1.0
→ バランスの取れた難易度

# 非常に滑りやすい環境
static_friction=0.1, dynamic_friction=0.1
→ 非常に難しい、高度な制御が学べる
Q4: 報酬関数をどう設計する?

A: 「報酬関数の設計」セクションで詳しく解説しています。基本的には Isaac Lab 標準の報酬関数を組み合わせ、RewardTermCfgweight で重みを調整します。まず主目的(速度追跡など)だけで学習させ、うまく動いたら補助ペナルティを追加していくのがおすすめです。

Q5: 複数環境での並列学習とは?

A: Isaac Labは複数の独立した環境を同時にシミュレートできます:

num_envs=32  # 32個の環境を並列実行
# 各環境は独立して走っており、
# 全体で32倍高速に学習できる



参考リソース

このドキュメントは初心者向けの入門ガイドです。詳細な実装情報や最新機能については、公式リソースを参照してください。

GitHub リポジトリ
  • Isaac Lab (GitHub)

https://github.com/isaac-sim/IsaacLab



まとめ

Isaac Sim上での強化学習の流れ:

  1. 環境構築: ロボット、地面、センサーを配置
  2. 観測設計: ロボットから何の情報を取得するか決定
  3. 行動設計: ロボットに何をさせるか定義
  4. 報酬設計: 良い行動をどう定義するか設計
  5. 学習: ニューラルネットワークポリシーを更新
  6. テスト: 実際の環境での転移学習
重要なポイント
  • 観測: ロボット自身の状態 + センサー情報 + 指令信号
  • 地面: 形状(ジオメトリ)+ 物理特性(マテリアル)で定義
  • 物理: dt(時間ステップ)とdecimation(制御周期)を理解する
  • センサー: 高さスキャナで地形検出、接触センサーで力を測定
  • 並列化: 複数環境で高速学習が可能

このガイドで基本を理解したら、公式チュートリアルで実装を進めましょう。



最後に

このガイドを読んでくれてありがとうございます。Isaac Lab での強化学習は、理論と実装を組み合わせることで初めて学習できる領域です。

次のステップとしてお勧めします:

  1. チュートリアルを実行する — このガイドの概念を実装例で確認
  2. パラメータを変更してみる — 報酬関数や観測データを試行錯誤
  3. 簡単なタスクから始める — 直進運動など単純なタスクでコツをつかむ
  4. 公式ドキュメントを深掘りする — より複雑な環境設定に挑戦

質問や詳細な実装については、Isaac Lab GitHub のディスカッションやIssueを参考にしてください。皆さんの強化学習の学習が成功することを願っています!

Microsoft Fabric Real-Time IntelligenceによるROSからのデータ受信を試す〜ROS2ロボット × Microsoft Fabricで作るクラウド連携デジタルツイン入門

はじめに

IoTやロボティクスの現場では、「リアルワールドの状態をクラウド上に忠実に再現・分析したい」というニーズが急増しています。本記事では、ROS2で動作するロボットの位置情報データをMicrosoft Fabric の Real-Time Intelligence(RTI)でリアルタイム収集し、OneLake に長期保存したうえで、Real-Time Dashboard によってリアルタイム可視化を行う「クラウド連携デジタルツイン」の構築手順を初心者向けに解説します。



本記事のアジェンダ

本記事では以下の流れで解説します。

  1. システム全体像の確認 — アーキテクチャの把握
  2. ステップ1:Fabricワークスペースの準備 — 作業環境のセットアップ
  3. ステップ2:ROS2からMQTT変換 — ロボット側のデータ送信設定
  4. ステップ3:RTI EventStreamでMQTT受信 — クラウド側の受信設定
  5. ステップ4:EventhouseとKQL Databaseへのデータ蓄積 — リアルタイムDB構築
  6. ステップ5:OneLakeへのデータ長期保存 — データレイクへの保存と参照
  7. ステップ6:Real-Time Dashboardによる可視化 — リアルタイム表示



システム全体像

[ROS2ロボット]
     │ MQTT変換(ros2_mqtt_bridge等)
     ▼
[インターネット]
     │ MQTT over TLS
     ▼
[Microsoft Fabric RTI]
 ├─ EventStream(受信)
 ├─ Eventhouse / KQL Database(蓄積)
 ├─ OneLake(長期保存)
 └─ Real-Time Dashboard(可視化)



ステップ1:Microsoft Fabric ワークスペースの準備

まず、Microsoft Fabricのテナントとワークスペースを用意します。

  1. Microsoft Fabric ポータル にサインイン
  2. 左メニューから 「ワークスペース」→「新しいワークスペース」 を作成
  3. ライセンスは Fabric 容量(F SKU) または 試用版 を割り当て

ワークスペース内で以降のすべてのリソース(EventStream、Eventhouse、OneLake等)を管理します。

公式ドキュメント:



ステップ2:ROS2ロボット側のMQTT変換設定

ROS2のトピックデータをMQTTに変換してクラウドへ送信します。

2-1. MQTTブリッジのインストール
# ros2_mqtt_bridge を使う例
pip install paho-mqtt

# またはOSSブリッジ
git clone https://github.com/yourusername/ros2_mqtt_bridge
2-2. 送信するデータの定義
ROS2トピック 内容 MQTTトピック例
/robot/pose 位置・姿勢(geometry_msgs/Pose) robot/001/pose
2-3. MQTT送信スクリプト例
import rclpy
from rclpy.node import Node
import paho.mqtt.client as mqtt
from paho.mqtt.packettypes import PacketTypes
from paho.mqtt.properties import Properties
import json
from geometry_msgs.msg import PoseStamped

class MQTTBridgeNode(Node):
    def __init__(self):
        super().__init__('mqtt_bridge')
        # MQTTv5 を使用(Properties によるセッション制御と Content-Type 指定に必要)
        self.client = mqtt.Client(
            mqtt.CallbackAPIVersion.VERSION2,
            protocol=mqtt.MQTTv5,
        )
        self.client.tls_set()  # TLS有効化

        # 切断時にセッションを即時破棄してセッション枯渇を防ぐ
        connect_props = Properties(PacketTypes.CONNECT)
        connect_props.SessionExpiryInterval = 0

        self.client.connect(
            "YOUR_EVENT_GRID_MQTT_ENDPOINT", 8883,
            clean_start=True, properties=connect_props,
        )
        self.client.loop_start()  # ネットワーク処理をバックグラウンドスレッドで開始

        self.pose_sub = self.create_subscription(
            PoseStamped, '/robot/pose', self.pose_callback, 10)

    def pose_callback(self, msg):
        payload = {
            "robot_id": "robot_001",
            "timestamp": self.get_clock().now().to_msg().sec,
            "x": msg.pose.position.x,
            "y": msg.pose.position.y,
            "z": msg.pose.position.z
        }
        # Content-Type を application/json に設定することで
        # Azure Event Grid が data_base64 ではなく data (JSON) としてルーティングする
        pub_props = Properties(PacketTypes.PUBLISH)
        pub_props.ContentType = "application/json"
        self.client.publish("robot/001/pose", json.dumps(payload), properties=pub_props)

公式ドキュメント:



ステップ3:RTI EventStreamでMQTTデータを受信

Microsoft FabricのReal-Time Intelligence(RTI)のEventStreamを使って、ロボットからのMQTTデータを受け取ります。

3-1. EventStreamの作成
  1. Fabricワークスペースで 「+ 新しい項目」→「EventStream」 を選択
  2. EventStreamに任意の名前を付けて作成(例:robot-telemetry-stream
3-2. MQTTソースの追加

EventStream から Azure Event Grid に接続する方法は2通りあります。

方法A:MQTT コネクタ 方法B:Azure Event Grid コネクタ(推奨)
認証 クライアント証明書(Azure Key Vault経由) マネージドID(証明書不要)
設定の複雑さ 高(Key Vault準備が必要) 低(サブスクリプション選択のみ)
対応ブローカー 任意のMQTTブローカー Azure Event Grid 専用
方法A:MQTT コネクタ

⚠️ 評価版(試用版)ご利用時の注意
Azure Event Grid の試用版 Namespace は MQTT 同時接続数の上限が低く設定されています(デフォルト:1接続/認証名)。接続・切断を繰り返すとセッションが蓄積し Quota exceeded エラーが発生する場合があります。エラーが発生した場合は、以下の Azure CLI コマンドで上限を引き上げてください。

az eventgrid namespace update \
  --name my-robot-iotns \
  --resource-group MyResource01 \
  --topic-spaces-configuration maximumClientSessionsPerAuthenticationName=3

または 30分〜1時間待機してセッションが自然消滅するのを待ち、Azure Portal → Event Grid 名前空間 → 「監視」→「メトリクス」MQTT: Connections が 0 に戻ったことを確認してから再試行してください。本番環境の Standard tier では最大 10,000 同時接続をサポートしており、この制限には通常抵触しません。

Azure Event Grid はX.509証明書によるクライアント認証を使用するため、EventStream 側のクライアント証明書(付録A-3で生成した .pem.key)をあらかじめ Azure Key Vault にアップロードしておく必要があります。

事前準備:Azure Key Vault への証明書アップロード

以下の手順では Azure CLI を使用します。実行環境は2通りあります。

実行環境 説明
Azure Cloud Shell(推奨) Azure Portal 上部の 「>_」アイコン を選択して起動。インストール不要でブラウザから直接実行できます
ローカル PC(macOS) brew update && brew install azure-cli でインストール後、az login でサインイン

公式ドキュメント:

Azure Key Vault が未作成の場合は、以下で作成してください。Key Vault の作成前に、サブスクリプションへのリソースプロバイダー登録が必要です。

# Microsoft.KeyVault リソースプロバイダーを登録
az provider register --namespace Microsoft.KeyVault

# 登録完了を確認("Registered" になるまで1〜2分かかります)
az provider show --namespace Microsoft.KeyVault --query registrationState

"Registered" と表示されたら Key Vault を作成します。

# Key Vault の作成(既存の場合はスキップ)
az keyvault create --name robot-key-001 --resource-group MyResource01 --location japanwest

Azure Key Vault にアップロードする証明書は、証明書と秘密鍵を1ファイルに結合した PEM バンドル形式である必要があります。また、秘密鍵は Azure Key Vault が要求する PKCS#8 形式BEGIN PRIVATE KEY)に変換が必要です。

# /tmp/mqtt-cert-test に移動(A-3 の作業ディレクトリ)
cd /tmp/mqtt-cert-test

# 秘密鍵を PKCS#8 形式に変換
openssl pkcs8 -topk8 -nocrypt \
  -in robot-001-authn-ID.key \
  -out robot-001-authn-ID-pkcs8.key

# PEM バンドルファイルの作成(証明書 + PKCS#8形式の秘密鍵を結合)
cat robot-001-authn-ID.pem robot-001-authn-ID-pkcs8.key > robot-001-authn-ID-bundle.pem

次に、インポートポリシーファイルを作成します。--kty RSA --size 2048 で生成した RSA 2048 証明書に合わせて keyType"RSA"keySize2048 を指定します。

{
  "secretProperties": {
    "contentType": "application/x-pem-file"
  },
  "keyProperties": {
    "exportable": true,
    "keyType": "RSA",
    "keySize": 2048,
    "reuseKey": false
  },
  "issuerParameters": {
    "name": "Unknown"
  }
}

上記を keyvault-cert-policy.json として保存し、Azure CLI でインポートします。

# プロジェクトディレクトリ(keyvault-cert-policy.json があるディレクトリ)から実行
az keyvault certificate import \
  --vault-name robot-key-001 \
  --name robot-001-client-cert \
  --file /tmp/mqtt-cert-test/robot-001-authn-ID-bundle.pem \
  --policy @keyvault-cert-policy.json

注意: EventStream の接続設定を行うユーザーには、Key Vault の 「Key Vault Certificate User」 または 「Key Vault Administrator」 ロールが必要です。Azure Portal → Key Vault → 「Access control (IAM)」 からロールを割り当ててください。

EventStream への MQTT ソース追加手順

  1. EventStreamのキャンバスで 「ソースの追加」→「データ ソースの接続」→「新規(+)」→「MQTT」-「接続」 を選択
  2. 「新しい接続」 を選択
  3. 接続設定を入力:
    • MQTTブローカーURLssl://:8883(例:ssl://my-robot-iotns.japanwest-1.ts.eventgrid.azure.net:8883
    • 接続名:任意
    • ユーザー名:A-4 で登録した「クライアント認証名」(例:robot-001-authn-ID
    • パスワード:空欄
    • [接続] を選択
  4. MQTTデータソースの構成を入力:
    • トピック名robot/001/pose
    • バージョン:V5(またはV3)を選択
  5. 「TLS/mTLS 設定」 を展開すると以下の2つのトグルが表示されます:
    • 「Trust CA certificate」:ブローカーのサーバー証明書を検証するCA証明書を指定する場合に有効化(今回は不要)
    • 「Client certificate and key」:有効にして、Azure Key Vault に保存した証明書(robot-001-client-cert)を指定
方法B:Azure Event Grid コネクタ(推奨)

マネージドIDによる認証で、証明書の管理が不要です。事前に以下の準備が必要です。

事前準備:

  1. Azure Portal → Event Grid 名前空間 → 左メニューの 「Settings」→「Identity」 → システム割り当てIDのスイッチを 「On」 にして 「保存」
  2. Fabric ポータル右上の 歯車アイコン(設定) を選択 → サイドパネルの 「ガバナンスと管理」 下にある 「管理ポータル」 を選択 → 「テナント設定」 を選択 → 「開発者向け設定」 セクションの 「サービス プリンシパルは Fabric の公開用 API を呼び出すことができます」 を有効化
  3. Fabric ワークスペースのページを開き、コマンドバーの 「アクセスの管理」「+ ユーザーまたはグループの追加」 を選択 → 検索欄に Event Grid 名前空間名(例:my-robot-iotns)を入力して表示された 「my-robot-iotns」 を選択して 「追加」 を選択 → ロールに 「共同作業者」 を選択

EventStreamへの追加:

  1. EventStreamのキャンバスで 「ソースの追加」→「データ ソースの接続」→「新規(+)」→「Azure Event Grid Namespace」→「接続」 を選択
  2. 接続設定を入力:
    • サブスクリプション:Event Grid 名前空間が属するAzureサブスクリプション
    • 名前空間名my-robot-iotns
  3. MQTTが有効でルーティング未設定の場合は、「名前空間トピック」 で使用するトピックを選択
  4. 「次へ」→「接続」→「追加」 を選択

公式ドキュメント:



ステップ4:EventhouseとKQL Databaseへのデータ蓄積

受信したデータをリアルタイム検索・分析が可能なEventhouse(KQL Database)に保存します。

4-1. Eventhouseの作成
  1. ワークスペースで 「+ 新しい項目」→「イベントハウス」 を作成(名前例:robot-eventhouse
  2. 「作成」 を選択すると、robot-eventhouse という名前の KQL データベースが同時に自動生成されます
4-2. KQLテーブルの作成
  1. 作成した robot-eventhouse を開く
  2. 左側のオブジェクトツリーに表示される robot-eventhouse_queryset を選択してクエリエディターを開く
  3. エディターに以下のコマンドを貼り付け、「▶ 実行」 をクリック
// 位置情報テーブル
.create table RobotPose (
    robot_id: string,
    timestamp: datetime,
    x: real,
    y: real,
    z: real
)
4-3. EventStreamの宛先にEventhouseを追加・発行
  1. robot-telemetry-stream EventStream を開き、robot-telemetry-stream ノードの出力から 「宛先の追加」→「イベントハウス」 を選択
  2. データインジェストモード を選択:
モード 動作
直接インジェスト(推奨) 変換なしでそのままEventhouseへ取り込む
インジェスト前のイベント処理(デフォルト) フィルター・集計などの変換を挟んでから取り込む

今回はデータ加工が不要なため 「直接インジェスト」 を選択します。

  1. 以下を入力して 「保存」 を選択:
    • 宛先名:任意(例:robot-eventhouse-dest
    • ワークスペース:使用中のワークスペース
    • イベントハウスrobot-eventhouse
    • KQL データベースrobot-eventhouse
    • テーブルRobotPose(ステップ4-2で作成済み)
  1. 「発行」 ボタンをクリックしてEventStreamを有効化
4-4. KQLインジェストマッピングの設定

EventStreamを発行したら、KQLインジェストマッピングを設定します。これにより、受信したJSONデータのどのフィールドをKQLテーブルのどのカラムに格納するかを定義します。

なぜ必要か: Azure Event Grid は MQTT ペイロードを CloudEvents 形式でラップして配信します。そのため、実際のデータは最上位ではなく data フィールドの中に格納されています。

{
  "datacontenttype": "application/json",
  "data": { "robot_id": "robot_001", "x": 135.507, "y": 34.693, ... },
  "subject": "robot/001/pose",
  ...
}

KQLのカラム(robot_idx など)に正しく値を取り込むには、$.data.robot_id$.data.x のように data フィールド配下のパスを指定する必要があります。

robot-eventhouse_queryset のクエリエディターで以下を 1行で 実行してください。

.create-or-alter table RobotPose ingestion json mapping 'RobotPose_mapping' '[{"column":"robot_id","path":"$.data.robot_id","datatype":"string"},{"column":"timestamp","path":"$.data.timestamp","datatype":"datetime","transform":"DateTimeFromUnixSeconds"},{"column":"x","path":"$.data.x","datatype":"real"},{"column":"y","path":"$.data.y","datatype":"real"},{"column":"z","path":"$.data.z","datatype":"real"}]'

ポイント: "transform":"DateTimeFromUnixSeconds" により、ブリッジスクリプトが送信するUnixエポック秒の整数値を自動的に datetime 型へ変換します。

このコマンドの再実行が必要なケース:
このマッピング設定は初回のみ実行すれば、それ以降は自動的に適用され続けます。ただし、以下の場合は再実行(または新規作成)が必要です。

  • ブリッジスクリプトが送信するJSONのフィールド構成を変更した場合(フィールドの追加・削除・リネーム)
  • KQLテーブルのカラム定義(スキーマ)を変更した場合
  • .create-or-alter を使うと既存マッピングを上書きできるため、スキーマ変更時は同じコマンドを修正して再実行するだけで対応できます。
4-5. ブリッジスクリプトの実行(テストデータ送信)

EventStream の発行とマッピング設定が完了したら、ロボットからのデータ送信をシミュレートします。ターミナルを2つ用意して以下を実行します。

ターミナル1:位置情報シミュレーター起動

事前にローカルMQTTブローカー(Mosquitto)を起動しておきます。

# macOS
brew services start mosquitto

# Ubuntu
sudo systemctl start mosquitto
python3 ros2_pose_simulator.py

ターミナル2:MQTTブリッジ起動

# /tmp/mqtt-cert-test に移動(A-3 の作業ディレクトリ)
cd /tmp/mqtt-cert-test

python3 /path/to/ros2_mqtt_bridge.py \
  --fabric-host my-robot-iotns.japanwest-1.ts.eventgrid.azure.net \
  --username robot-001-authn-ID \
  --cert-file robot-001-authn-ID.pem \
  --key-file robot-001-authn-ID.key

[Fabric接続成功] と表示されれば Azure Event Grid への接続が確立されています。

4-6. 動作確認

EventStream ライブプレビュー

  1. Fabric ポータルで robot-telemetry-stream を開く
  2. キャンバス上の MQTT ソースノードを選択し、「データのプレビュー」 をクリック
  3. ブリッジスクリプトが実行中であれば、受信中のメッセージがリアルタイムで表示されます

KQL クエリによる蓄積確認

  1. ワークスペースから robot-eventhouse を開く
  2. 左側のオブジェクトツリーに表示される robot-eventhouse_queryset を選択してクエリエディターを開く
  3. 以下のクエリを貼り付け、「▶ 実行」 をクリック
RobotPose
| where timestamp > ago(5m)
| order by timestamp desc
| take 10

データが返ってくれば、EventStream → Eventhouse のパイプライン全体が正常に動作しています。


公式ドキュメント:



ステップ5:OneLakeへのデータ長期保存

位置情報の履歴データは、Fabricの統合データレイク OneLake に保存して長期管理します。

5-1. OneLakeのショートカットまたはレイクハウスを利用
  1. ワークスペースで 「+ 新しい項目」→「レイクハウス」 を作成(例:robot_datalake
    • 作成画面の 「レイクハウス スキーマ」 はデフォルト(チェックあり)のまま 「作成」 を選択
  2. Eventhouseの 「OneLake」「Availability」 を有効化します:
    1. ワークスペースから robot-eventhouse を開く
    2. 上部タブの 「データベース」 を選択
    3. 左ツリーの robot-eventhouse データベースを選択
    4. 右側の 「Database details」 パネルの 「OneLake」 セクションで 「Availability」 のトグルを 「Enabled」 に切り替える
    5. トグルが 「Enabled」 になると、KQLのデータがDelta Parquet形式でOneLakeへの同期が開始されます

⏱️ 同期完了までの待機について
「Database details」パネルの 「OneLake」 セクションには 「Latency」 という同期遅延の目安が表示されます(例:3 hours)。有効化直後は 「Pending size」 に未同期のデータ量が表示され、同期が完了すると 0 B になります。次のステップ(Notebookからのデータ参照)は同期完了後に実施してください。

5-2. OneLakeに同期された位置情報ログの参照

OneLake可用性を有効にすると、RobotPose テーブルのデータが Delta Parquet 形式で OneLake に継続的に同期されます。Fabric Notebook(Spark 環境)から以下のように参照できます。

# Fabric Notebook(Spark環境)でOneLakeの位置情報ログを参照
df = spark.read.format("delta").load(
    "abfss://robot_datalake@onelake.dfs.fabric.microsoft.com/Tables/RobotPose"
)
df.show()

公式ドキュメント:



ステップ6:Real-Time Dashboardによるリアルタイム可視化

Microsoft Fabric の Real-Time Dashboard を使って、ロボットの位置情報をリアルタイムに可視化します。

  1. Fabricワークスペースで 「+ 新しい項目」→「リアルタイム ダッシュボード」 を選択して新規作成(名前はデフォルトの NewRTDashboard_1 のままでも可)
  2. KQL Databaseをデータソースとして接続:「+ データソースの追加」robot-eventhouse を選択 → 「Connect」 をクリック
  3. 「新しいタイル」→「マップ」 を選択
  4. 画面下部の 「Query editor」 に以下のクエリを貼り付け、「実行」 をクリック
// リアルタイムダッシュボード用クエリ(直近5分の軌跡)
RobotPose
| where timestamp > ago(5m)
| where robot_id == "robot_001"
| project timestamp, x, y, z
| order by timestamp asc
  1. 左パネルの 「データ」 セクションで 「場所の定義方法:」→「緯度と経度」 を選択し、以下のように列をマッピング:
    • 緯度y (real)
    • 経度x (real)
  2. 「Done」 をクリックして保存

公式ドキュメント:



付録:Azure Event GridをMQTTブローカーとして設定する

ステップ3のEventStreamにMQTTソースを接続するには、ロボット(ブリッジスクリプト)とFabric EventStreamの両方が接続できるMQTTブローカーが必要です。ここではAzure Event GridをMQTTブローカーとして利用する手順を解説します。

A-1. Event Grid 名前空間の作成
  1. Azure Portal にサインインし、「Event Grid 名前空間」 を検索して選択
  2. 「+ 作成」 をクリック
  3. 以下を入力して 「確認 + 作成」→「作成」
    • 名前空間名:一意の名前(3〜50文字、英数字とハイフンのみ)。本記事では my-robot-iotns を使用
    • サブスクリプション / リソースグループ:任意
    • リージョン:任意
A-2. MQTTブローカーの有効化とエンドポイント確認
  1. 作成した名前空間の概要ページを開く
  2. 概要ページで 「MQTT ブローカー」「無効」 と表示されている 「無効」 リンクを選択(「構成」ページへリダイレクトされます)
  3. 「構成」ページで 「MQTT ブローカーを有効にする」 を選択
  4. 「適用」 をクリックして設定を保存
  5. 名前空間の概要ページに戻り、表示される 「MQTT ホスト名」 を控える
my-robot-iotns.japanwest-1.ts.eventgrid.azure.net  (ポート 8883)

注意: いったん有効にした MQTT ブローカーは無効に戻すことができません。

A-3. クライアント証明書の作成

Azure Event Grid はX.509証明書によるクライアント認証を使用します。まず Step CLI をインストールし、ローカルCAとクライアント証明書を生成します。証明書ファイルの作業ディレクトリは /tmp/mqtt-cert-test とします。

# Step CLI のインストール(macOS)
brew install step

# 作業ディレクトリの作成と移動
mkdir -p /tmp/mqtt-cert-test && cd /tmp/mqtt-cert-test

# CA の初期化(実行中にCAの秘密鍵パスワードとプロビジョナーのパスワードを求められます)
# ※ここで設定したCAの秘密鍵パスワードは、後続の証明書生成コマンドでも必要になります。必ず記録しておいてください。
step ca init --deployment-type standalone --name MqttAppSamplesCA \
  --dns localhost --address 127.0.0.1:443 \
  --provisioner MqttAppSamplesCAProvisioner

# クライアント証明書の生成(実行中にA-3で設定したCAパスワードを求められます)
# ※ ~/.step/ は step ca init がCA証明書を生成したホームディレクトリ配下のパス
step certificate create robot-001-authn-ID robot-001-authn-ID.pem robot-001-authn-ID.key \
  --ca ~/.step/certs/intermediate_ca.crt \
  --ca-key ~/.step/secrets/intermediate_ca_key \
  --no-password --insecure --not-after 2400h

# サムプリントの取得
step certificate fingerprint robot-001-authn-ID.pem
A-4. クライアントの登録
  1. 名前空間の左メニュー 「MQTT ブローカー」→「クライアント」 を選択
  2. 「+ クライアント」 をクリック
  3. 以下を入力して 「作成」
項目
クライアント名 任意(例:robot-001
クライアント認証名 証明書生成時の名前(例:robot-001-authn-ID
クライアント証明書認証検証スキーム Thumbprint Match を選択

「Thumbprint Match」を選択後に表示される「プライマリ拇印」入力フィールドに、A-3 で取得したサムプリントの値を入力します。

A-5. トピック空間の作成
  1. 左メニュー 「MQTT ブローカー」→「トピック空間」 を選択
  2. 「+ トピック空間」 をクリック
  3. 以下を入力して 「作成」
    • 名前robot-topic-space
    • トピックテンプレートrobot/001/pose(「+ トピック テンプレートの追加」から入力)
A-6. アクセス許可バインドの設定

ロボット(パブリッシャー)とFabric EventStream(サブスクライバー)の両方向で通信できるよう、2件のバインドを作成します。

  1. 左メニュー 「MQTT ブローカー」→「アクセス許可のバインド」 を選択
  2. 「+ アクセス許可のバインド」 で以下を2件作成
バインド名 クライアントグループ トピック空間 権限
robot-publisher-binding $all robot-topic-space パブリッシャー
robot-subscriber-binding $all robot-topic-space サブスクライバー
A-7. ブリッジスクリプトからの接続
python3 ros2_mqtt_bridge.py \
  --fabric-host my-robot-iotns.ts.eventgrid.azure.net \
  --username robot-001-authn-ID \
  --cert-file robot-001-authn-ID.pem \
  --key-file robot-001-authn-ID.key
A-8. 動作確認(Azure Portal メトリクス)

ブリッジスクリプトを実行しながら、Azure Portal でメッセージの送受信を確認できます。

  1. Azure Portal → 作成した Event Grid 名前空間 → 左メニュー 「監視」→「メトリクス」 を選択
  2. 「+ メトリクスの追加」 をクリックし、以下のメトリクスを追加して確認
メトリクス名 確認できること
MQTT: Connections ブリッジスクリプトが正常に接続できているか
MQTT: Successful Published Messages Event Grid がメッセージを受信しているか
MQTT: Successful Delivered Messages サブスクライバーへメッセージが配信されているか

スクリプト実行中にこれらの値が増加していれば、Azure Event Grid との接続およびメッセージ送信が正常に動作しています。

公式ドキュメント:



セキュリティに関する注意事項

ロボットがインターネット経由でクラウドへ接続する際は、以下のセキュリティ対策を必ず実施してください。

  • 通信の暗号化:MQTT over TLS(ポート8883)を使用
  • 認証:X.509証明書またはSASトークンによるデバイス認証
  • ネットワーク分離:ロボットのネットワークセグメントをファイアウォールで保護
  • 最小権限の原則:ロボットには送信専用の権限のみ付与

公式ドキュメント:



まとめ

本記事では以下のシステムを段階的に構築しました:

ステップ 内容 使用技術
1 Fabricワークスペース準備 Microsoft Fabric
2 ROS2からMQTT変換 ROS2 / paho-mqtt
3 クラウドでMQTT受信 RTI EventStream
4 リアルタイムDB蓄積 Eventhouse / KQL
5 データレイク保存 OneLake / レイクハウス
6 リアルタイム可視化 RTI Real-Time Dashboard

Microsoft Fabricはデータの収集・蓄積・保存・可視化を単一プラットフォームで完結できるため、デジタルツインの構築コストと運用負荷を大幅に削減できます。MQTTブローカーの準備については付録のAzure Event Grid設定手順もご参照ください。本記事を参考に、ぜひご自身のロボットやIoTデバイスでお試しください。

最後まで読んでいただきありがとうございます。本記事がROS2ロボットやIoTデバイスのクラウド連携を始める際のお役に立てれば幸いです。ご質問やフィードバックがあれば、ぜひコメントでお知らせください。




付録B:サンプルソースコード

本記事で使用したスクリプトの全文を掲載します。

B-1. ros2mqttbridge.py

ローカルMQTTブローカーの /robot/pose トピックを購読し、Azure Event Grid(Fabric)のMQTTエンドポイントへ転送するブリッジスクリプトです。

"""
ROS2 → Microsoft Fabric MQTTブリッジ

ローカルMQTTブローカーの /robot/pose トピックから geometry_msgs/PoseStamped
相当のJSONを受信し、Microsoft Fabric(Azure Event Grid)のMQTTエンドポイントへ
robot/001/pose トピックとして転送します。

使用方法:
    python ros2_mqtt_bridge.py --fabric-host <ENDPOINT> [オプション]

前提:
    - ros2_pose_simulator.py が別ターミナルで起動済みであること
    - pip install paho-mqtt
"""

import argparse
import json
import ssl

import paho.mqtt.client as mqtt
from paho.mqtt.packettypes import PacketTypes
from paho.mqtt.properties import Properties

LOCAL_TOPIC = "/robot/pose"
FABRIC_TOPIC = "robot/001/pose"
ROBOT_ID = "robot_001"


def on_local_connect(client, userdata, flags, reason_code, properties=None):
    if reason_code == 0:
        client.subscribe(LOCAL_TOPIC, qos=0)
        print(f"[ローカル接続成功] '{LOCAL_TOPIC}' をサブスクライブ")
    else:
        print(f"[ローカル接続失敗] コード: {reason_code}")


def make_on_message(fabric_client: mqtt.Client):
    """ローカルMQTTからメッセージを受信してFabricへ転送するコールバックを返す。"""

    def on_message(client, userdata, msg):
        try:
            data = json.loads(msg.payload.decode())
            pos = data["pose"]["position"]
            stamp = data["header"]["stamp"]

            payload = {
                "robot_id": ROBOT_ID,
                "timestamp": stamp["sec"],
                "x": pos["x"],
                "y": pos["y"],
                "z": pos["z"],
            }
            # Content-Type を application/json に設定することで
            # Azure Event Grid が data_base64 ではなく data (JSON) としてルーティングする
            pub_props = Properties(PacketTypes.PUBLISH)
            pub_props.ContentType = "application/json"
            fabric_client.publish(FABRIC_TOPIC, json.dumps(payload), qos=1, properties=pub_props)
            print(f"[転送] x={pos['x']:>8.4f}  y={pos['y']:>8.4f}  → {FABRIC_TOPIC}")
        except (KeyError, json.JSONDecodeError) as e:
            print(f"[エラー] ペイロードの解析に失敗: {e}")

    return on_message


def on_fabric_connect(client, userdata, flags, reason_code, properties=None):
    if reason_code == 0:
        print(f"[Fabric接続成功] {userdata['host']}:{userdata['port']}")
    else:
        print(f"[Fabric接続失敗] コード: {reason_code}")


def main():
    parser = argparse.ArgumentParser(description="ROS2 → Fabric MQTTブリッジ")
    parser.add_argument("--local-host", default="localhost")
    parser.add_argument("--local-port", type=int, default=1883)
    parser.add_argument("--fabric-host", required=True,
                        help="Fabric/Event Grid MQTTエンドポイントのFQDN")
    parser.add_argument("--fabric-port", type=int, default=8883)
    parser.add_argument("--username", default=None,
                        help="クライアント認証名(A-4で登録した名前)")
    parser.add_argument("--password", default=None)
    parser.add_argument("--cert-file", default=None,
                        help="クライアント証明書ファイルのパス(.pem)")
    parser.add_argument("--key-file", default=None,
                        help="クライアント秘密鍵ファイルのパス(.key)")
    parser.add_argument("--no-tls", action="store_true",
                        help="TLSを無効にする(テスト用)")
    args = parser.parse_args()

    # Fabricクライアント(パブリッシャー)
    fabric_client = mqtt.Client(
        mqtt.CallbackAPIVersion.VERSION2,
        client_id=f"{ROBOT_ID}_bridge",
        protocol=mqtt.MQTTv5,
        userdata={"host": args.fabric_host, "port": args.fabric_port},
    )
    fabric_client.on_connect = on_fabric_connect

    if args.username:
        fabric_client.username_pw_set(args.username, args.password)

    if not args.no_tls:
        fabric_client.tls_set(
            certfile=args.cert_file,
            keyfile=args.key_file,
            cert_reqs=ssl.CERT_REQUIRED,
            tls_version=ssl.PROTOCOL_TLS_CLIENT,
        )

    # SessionExpiryInterval=0: 切断時にセッションを即時破棄しクォータを消費しない
    connect_props = Properties(PacketTypes.CONNECT)
    connect_props.SessionExpiryInterval = 0

    fabric_client.connect(args.fabric_host, args.fabric_port, clean_start=True, properties=connect_props)
    fabric_client.loop_start()

    # ローカルクライアント(サブスクライバー)
    local_client = mqtt.Client(
        mqtt.CallbackAPIVersion.VERSION2,
        client_id="ros2_bridge_local",
    )
    local_client.on_connect = on_local_connect
    local_client.on_message = make_on_message(fabric_client)
    local_client.connect(args.local_host, args.local_port)

    print("[実行中] Ctrl+C で停止")
    try:
        local_client.loop_forever()
    except KeyboardInterrupt:
        print("\n[停止] Ctrl+C を受信しました")
    finally:
        fabric_client.loop_stop()
        fabric_client.disconnect()
        local_client.disconnect()


if __name__ == "__main__":
    main()
B-2. ros2posesimulator.py

ROS2ロボットの代わりに、大阪市中心部を基点とした円形経路の位置情報をローカルMQTTブローカーへ送信するシミュレーターです。

"""
ROS2 位置情報データ生成シミュレーター

geometry_msgs/PoseStamped に相当するJSONペイロードを生成し、ローカルMQTT
ブローカーに送信することで ROS2 パブリッシャーを疑似的に再現します。
ros2_mqtt_bridge.py と組み合わせて使用してください。

座標系:
    x = 経度(Longitude)、y = 緯度(Latitude)
    大阪市中心部(緯度 34.6937、経度 135.5023)を基点に半径約500mの円形経路を走行

使用方法:
    python ros2_pose_simulator.py [オプション]

前提:
    - ローカルMQTTブローカー(例: Mosquitto)が起動済みであること
      macOS: brew install mosquitto && brew services start mosquitto
      Ubuntu: sudo apt install mosquitto && sudo systemctl start mosquitto
    - pip install paho-mqtt
"""

import argparse
import json
import math
import time

import paho.mqtt.client as mqtt

LOCAL_TOPIC = "/robot/pose"
FRAME_ID = "map"

# 大阪市中心部を基点とする経路パラメータ
CENTER_LAT = 34.6937   # 緯度
CENTER_LON = 135.5023  # 経度
# 約500m半径(緯度1度 ≈ 111km なので 0.005度 ≈ 556m)
RADIUS_DEG = 0.005


def circular_path(step: int, total_steps: int = 360) -> tuple[float, float]:
    """大阪市中心部を基点とした円形経路上の (longitude, latitude) を返す。"""
    angle = math.radians(step % total_steps)
    lon = CENTER_LON + RADIUS_DEG * math.cos(angle)
    lat = CENTER_LAT + RADIUS_DEG * math.sin(angle)
    return lon, lat


def build_pose_stamped(step: int) -> dict:
    """geometry_msgs/PoseStamped 相当のJSONペイロードを生成する。
    x = 経度(Longitude)、y = 緯度(Latitude)として格納する。
    """
    now = time.time()
    lon, lat = circular_path(step)
    return {
        "header": {
            "stamp": {"sec": int(now), "nanosec": int((now % 1) * 1e9)},
            "frame_id": FRAME_ID,
        },
        "pose": {
            "position": {"x": round(lon, 6), "y": round(lat, 6), "z": 0.0},
            "orientation": {"x": 0.0, "y": 0.0, "z": 0.0, "w": 1.0},
        },
    }


def on_connect(client, userdata, flags, reason_code, properties=None):
    if reason_code == 0:
        print(f"[接続成功] ローカルブローカー {userdata['host']}:{userdata['port']}")
    else:
        print(f"[接続失敗] コード: {reason_code}")


def main():
    parser = argparse.ArgumentParser(description="ROS2 位置情報シミュレーター")
    parser.add_argument("--host", default="localhost")
    parser.add_argument("--port", type=int, default=1883)
    parser.add_argument("--interval", type=float, default=1.0,
                        help="送信間隔(秒、デフォルト: 1.0)")
    parser.add_argument("--count", type=int, default=0,
                        help="送信回数(0=無制限)")
    args = parser.parse_args()

    client = mqtt.Client(
        mqtt.CallbackAPIVersion.VERSION2,
        client_id="ros2_simulator",
        userdata={"host": args.host, "port": args.port},
    )
    client.on_connect = on_connect
    client.connect(args.host, args.port)
    client.loop_start()

    print(f"[開始] トピック '{LOCAL_TOPIC}' へ送信中... (Ctrl+C で停止)")
    step = 0
    try:
        while True:
            payload = build_pose_stamped(step)
            client.publish(LOCAL_TOPIC, json.dumps(payload), qos=0)
            pos = payload["pose"]["position"]
            print(f"[送信] step={step:>4d}  lon={pos['x']:>10.6f}  lat={pos['y']:>10.6f}")
            step += 1
            if args.count > 0 and step >= args.count:
                break
            time.sleep(args.interval)
    except KeyboardInterrupt:
        print("\n[停止] Ctrl+C を受信しました")
    finally:
        client.loop_stop()
        client.disconnect()


if __name__ == "__main__":
    main()

注意: 本記事の執筆時点(2026年5月)における情報をもとにしています。Microsoft Fabricの機能は継続的に更新されているため、最新情報はMicrosoft Fabric 公式ドキュメントをご確認ください。一部機能はプレビュー段階のものも含まれており、GA(一般提供)までの間に設定項目・動作・画面上の表記が変更される可能性があります。

なお、執筆時点ではプレビュー版の影響か、ポータル画面上で英語と日本語の表記が混在している箇所が多く見受けられ、手順の把握が難しい場面がありました。GA に向けて UI の日本語化・表記統一が進むことを期待しています。

僻地で実用レベルのブロードバンドは可能か?Starlink ホームLite導入レポート

山間部や郊外では、いまだに安定したブロードバンド回線の確保が難しいケースがあります。
私の環境でも、

  • ドコモ/KDDIの4G/5G回線は利用可能
  • しかし時間帯によるスループット低下が顕著
  • 上り帯域が特に不安定

という問題を抱えていました。
そこで今回、衛星通信を利用したインターネットサービスStarlink ホームLiteを導入しました。
今回の投稿では、契約から開通までのプロセスと、実際の接続挙動を技術的観点で整理します。

1. 契約から発送までのリードタイム

一般的には「発送まで約1週間」という情報もありますが、私の場合は以下の通りでした。

  • 4月1日(水) 18:33 契約完了
  • 4月1日(水) 23:07 発送通知

契約から約4.5時間で出荷処理。
在庫状況や地域による差はあると思われますが、想定以上に短いリードタイムでした。
受領は 4月6日。
契約から実質5日で物理レイヤーが手元に揃いました。

2. 事前準備:Starlinkアプリによる障害物評価

StarlinkはLEO(Low Earth Orbit)衛星を利用します。
静止衛星とは異なり、

  • 衛星は高速で移動
  • 低軌道ゆえレイテンシが小さい
  • ただし視界確保が重要

という特徴があります。
発送通知後、受領までの期間を使い、Starlinkアプリの「障害物の確認」機能を使用しました。
この機能はスマートフォンのカメラとセンサーを用いて、

  • 上空視界の確保状況
  • 障害物の分布
  • 期待される遮蔽率

を可視化します。
庭の複数地点で測定し、最も遮蔽率の低い位置を仮決定しました。
こんな感じで良好な場所を見つけることが出来ました。

3. ハードウェア構成

4月6日に無事受領しました。パッケージは結構大きくて、14インチMacBook Proと比較するとこんな感じです。

同梱物は極めてシンプルです。

  • アンテナ(フェーズドアレイアンテナ)
  • Wi-Fiルーター
  • ACアダプタ
  • 専用ケーブル




フェーズドアレイ方式のため、物理的な回転機構に依存せず、電子的ビームフォーミングにより衛星を追尾します。
これは可動部品が少なく、耐久性の観点でも合理的な設計です。

4. 物理接続と初期セットアップ

手順は以下の通り。

  • アンテナとルーターを接続
  • AC投入
  • スマートフォンを「Starlink」SSIDへ接続
  • アプリで初期設定


PPPoE設定やAPN設定などは不要でとても簡単です。

電源投入後、アプリの「調整」機能を使用して、アンテナが最適な向き・傾きになるよう設置向きと傾きを調整します。

5. 衛星リンク確立と通信開始

数分以内に衛星リンクが確立。
アプリ上で接続ステータスを確認できます。
LEO衛星経由のため、理論上は

  • GEOより低レイテンシ
  • ただしセル混雑状況により帯域変動あり

という構造です。
ホームLiteは通常プランより優先度が低い設計ですが、実環境では十分なスループットを確保できました。

6. 実効性能の体感

これまでの4G/5G回線では、

  • 上り帯域不足
  • 混雑時間帯での速度低下
  • RTTのジッター増大

が顕著でした。
しかしStarlinkでは、

  • 動画配信は安定
  • クラウド同期は実用レベル
  • オンライン会議も安定動作

体感としては、都市部の固定回線に近い品質。

「地理的制約からの解放」という意味では、インフラのパラダイムが変わったと感じました。

7. なぜ僻地でも性能が出るのか

モバイル回線は、

  • 基地局密度
  • セル内ユーザー数
  • 地形による電波減衰

の影響を受けます。
一方Starlinkは、

  • 衛星からの直接リンク
  • 地上インフラ依存度が低い
  • 広域カバレッジ設計

というアーキテクチャです。
物理的距離よりも「空が見えるかどうか」が支配的要因になる点が大きな違いです。

8. ケーブルを外す時の注意点

アンテナからのケーブルを再配線する際に、アンテナからケーブルを外そうとしたのですが、ラッチで固定されているので無理やり外すとラッチが破損しそうでなかなか勇気が入りました。結果として、真っ直ぐ手前に力を入れて抜けばスポッと抜けます。ただ、コツとして一度前に押してから引っ張る方が良い感じです。また、あまり左右に引っ張らないことが注意点です。
ケーブルのラッチがこんな感じでRJ45に似た感じですが、RJ45とは違って力を入れて引っ張れば抜けます。

まとめ:僻地インフラの現実解になり得るか

Starlink ホームLiteは、

  • 光回線未整備地域
  • モバイル回線が不安定な地域
  • 工事困難な環境

において、現実的なブロードバンド代替になり得ると感じました。
特に、「モバイル回線は入るが、業務用途には厳しい」という中途半端な環境に対して、有効な選択肢です。

アンテナの屋根への設置(2026/05/03追記)

その後、アンテナを屋根に設置しました。業者さんにお願いして足場を設置してアンテナを屋根に取り付けてもらいました。工事はこんな様子です。

取り付け後はいい感じに宇宙を見ています。

Starlinkアプリで障害物を確認しましたが、ほぼ障害物はなく、通信は絶好調です。

スピードテストの結果も絶好調です。これでテレワーク時のリモートデスクトップ経由でのファイル編集も全く問題なく作業できます。

都市部で近所に高いビルなどがあると厳しいですが、普通の住宅街なら屋根への取り付けでも十分に性能が出せるのはないでしょうか。

Microsoft Fabric IQ とは?

データドリブン経営が一般化する今、単にデータを蓄積・分析するだけでなく、データの意味そのものを理解し、活用につなげる仕組みが求められています。
Microsoft Fabric IQ は「意味と関係性をつなぐインテリジェンス」です。Microsoft Fabric IQ の全体像を一言で言えば、データに「意味」を与え、AI で理解・推論できる基盤です。
これにより、単なるデータの保存や分析ではなく、

  • 組織内で共通に理解できる意味づけ
  • AI による高度な自動化・推論
  • 複雑な関係性を直感的に活用

といった 次世代のインテリジェンス基盤が実現できます
今回は、Microsoft Fabric IQ の概要と、主要機能をそれぞれわかりやすくご紹介します。
なお、現時点(2026年3月時点)では、Microsoft Fabric IQ はまだパブリックプレビュー(Preview)段階の機能として提供されています。

そもそも Microsoft Fabric IQ とは何か?

Microsoft Fabric IQ(以下、Fabric IQ)は、Microsoft Fabric 上の「意味づけ・知識レイヤー」です。
従来のデータ分析プラットフォームでは、データそのものの構造や意味を理解するために、多くの手作業や解釈コストが必要でした。
たとえば:

  • 「Customer」ってどのテーブルの何を指すの?
  • 同じ指標なのに分析ごとに定義が違う…
  • AI に“企業独自のルール”を理解させたいけど難しい…

こうした課題を解決するために、Fabric IQ は単なる「データ接続」ではなく、データとビジネスの意味をつなぐ知識基盤を提供します。

Fabric IQ でできること(活用シーン)

Fabric IQ の特長は、データを単に分析対象として扱うだけでなく、以下のような 「意味」や「関係性」 を表現・利用できる点にあります。

  • 複数データソースを 共通の意味づけ(セマンティクス)で統合
  • AI がビジネスコンテキストを理解して推論・回答できる
  • 関連性の高いデータ同士を グラフ構造で可視化・分析
  • 設計書・分析モデルの一貫性を保った データガバナンス を実現

Fabric IQ の主な機能概要

以下では、Fabric IQ を構成する主要なコンポーネントを、それぞれわかりやすく説明します。

1. オントロジ(Ontology) 〜 データの意味を“辞書化”する

オントロジとは、簡単に言うと 「企業の共通言語」 です。
日常生活で例えるなら:
「犬」って何?
「犬」は「動物」の一種で、名前・種類・年齢といった属性を持つ
というような「意味の定義」を体系化したものがオントロジです。
Fabric IQ では、次のようなものを定義できます:

  • ビジネス用語(例:顧客、注文、商品、地域など)
  • 用語の属性(例:顧客ID、氏名、受注日など)
  • 用語同士の関係性(例:顧客が注文をする)

こうした定義を元に、データソースに共通の意味づけが加わります。
つまり、データそのものではなく、「データが表す意味」を理解できるようになるのです。

2. プラン(Plan) 〜 意図ある設計を支える計画図

「プラン」は、オントロジを活かした 設計指針・計画図 の役割を持ちます。
データモデルや分析モデルを作る前に、どのような構造で意味づけを行うのか、どのような粒度でデータを整理するのかといった視点を整理できます。
言い換えれば、Fabric IQ での “導入設計の設計図” のようなものです。

3. Graph 〜 関係性を“視覚的・構造的に”表現

Graph(グラフ)は、ノード(点)とエッジ(線)で表すデータの関係性モデルです。
たとえば、ある顧客が

  • どの製品を購入したか
  • どの地域で取引しているか
  • どの販売担当者と関係があるか

といった 複雑なつながり を視覚的に捉えることができます。
これは、従来のテーブル結合では見えづらい “関連性”を分析する力 につながります。

4. データ エージェント(Data Agent) 〜 質問に意味で答える AI

Data Agent は、自然言語で質問すると、Fabric IQ のオントロジやグラフを元に ビジネスコンテキストを理解して答える AI です。
例:
「先月、関東エリアで最も売上が高かった商品を教えて」
この問いに対して、単なる数値ではなく、定義された意味を用いて 正確な回答 を導きます。
つまり、従来の BI ダッシュボードやクエリよりも、直感的な「問いかけ」で答えが得られるAI支援機能です。

5. 運用エージェント(Operations Agent) 〜 自動化・リアルタイム対応の支援

運用エージェントは、リアルタイムデータやイベント発生を監視し、設定されたルールやビジネス意味に基づいて アクションや通知を行う自動化エージェントです。
例:

  • 特定 KPI が閾値を超えたら通知
  • 割れた関係性を自動分析
  • リアルタイムイベントから推奨アクションを生成

運用の「高度な自動化と迅速な意思決定」をサポートします。

6. Power BI セマンティック モデルとの連携

Fabric IQ は、Power BI のセマンティック モデルとも連携できます。
その結果:

  • Power BI の指標も オントロジで定義された用語と統一
  • レポートやダッシュボードの意味が 共通言語化
  • レポート利用者とデータ設計者の解釈差を解消

といったメリットが得られます。

以上がFabric IQの機能説明です。
このFabric IQは、特定のワークロードに「閉じた機能」ではなく、Microsoft Fabric 全体を横断する“意味づけ(セマンティック)レイヤー”という位置づけになっています。
つまり、以前にご紹介したReal-Time Intelligence のデータも扱えるので、IoTやDigital Twinといったデータに対しても「意味」を与え、AI で理解・推論できるようになリマス。このため、これまでのような単純にデータをクラウドにあげてログ解析するといった使い方ではなく、もっと容易にインテリジェンスに現状分析や予測などが実現できる、とても優れた基盤です。

以上、簡単でしたが、Microsoft Fabric IQの紹介でした。

Windowsで意外と知られていない「ドラッグ&ドロップ」の小ワザ

Windowsを使い始めたばかりの方や、プログラマーのようにショートカットな小ワザを駆使している方以外のユーザーを見ていると、意外とよくあるのがこんな場面です。

「ファイルを別のウィンドウにドラッグしたいけど、そのウィンドウが他のウィンドウの裏に隠れて見えない…」

この場合、多くの初心者の方は、

  • いったんドラッグをやめる
  • ドロップ先のウィンドウをクリックして前面に出す
  • もう一度ファイルをドラッグし直す

という操作をしていることが多いです。

実はこれ、ドラッグをやめなくても解決できる方法があります。
タスクバーを使えば、ドラッグしたままウィンドウを前面に出せるんです!

やり方はとても簡単です。

  • ファイルをドラッグしたままタスクバー上の「ドロップ先アプリのアイコン」まで移動
  • そのまま少し待つ

すると、次のような動きになります。

同じアプリで複数ウィンドウを開いている場合
そのウィンドウの一覧が小さなプレビュー(アイコン)で表示される


  • その状態で、目的のウィンドウのアイコン上にドラッグしたまま移動
  • そのまま少し待つ
  • そのウィンドウが自動的に前面に表示される

あとは、そのまま目的の場所にドロップするだけです。

「ドラッグしたまま」がポイントです。

この操作のポイントは、途中でマウスボタンを離さないことです。

  1. ドラッグ開始
  2. タスクバーへ移動
  3. ウィンドウを前面に表示
  4. そのままドロップ

という流れを、一連の操作として続けられるのが最大のメリットです。

慣れてくると、
「あ、裏にあるな → タスクバーへスッ」
という感じで、無意識に使えるようになります。

ぜひ、試してみてください!

ウェザーニュースの温度ウィジェットが「!」表示になったときの対処メモ

ある日(多分iOS26に上げたあと)、iPhoneのホーム画面に置いているウェザーニュース(weatherenews)の温度ウィジェットが「!」表示になってしまい、何も表示されなくなりました。
iPhoneの再起動では直らず、位置情報設定をしても直らず、アプリ再インストールしても直らずでした。
色々試行錯誤した結果、以下の手順で復旧したので備忘録として残します。
同じところで詰まった人の参考になれば。

症状

ウェザーニュースの温度ウィジェットが「!」表示
他のウェザーニュース系ウィジェットも同様
ウェザーニュース以外のウィジェットは問題なし
iPhoneを再起動しても改善しない

環境

iPhone 14 Pro Max
iOS 26.2

やったこと(これで直った)

1. ウィジェットを削除

まずは問題のウィジェットを一度削除。
ホーム画面で
ウィジェット長押し → ウィジェットを削除

2. アプリを再インストール

次にウェザーニュースアプリを入れ直します。
App Storeで更新があれば適用
その後、アプリ削除 → 再インストール

3. アプリにログイン

再インストール後、
アプリを起動してログイン。
※ ログインしていないとウィジェットが動かない??

4. 位置情報の設定を確認

ここが一番大事。
設定 → プライバシーとセキュリティ → 位置情報サービス → weatherenews
許可:「このAppの使用中」または「常に」
正確な位置情報:ON

位置情報サービスの設定画面
5. モバイル通信の設定を確認

念のため通信設定も確認。
設定 → モバイル通信 → weatherenews
モバイルデータ通信:オン
Wi-Fiだけ使っていても、ここがオフだとダメなことがある。

モバイル通信の設定画面
6. ウィジェットを追加し直す

最後に、ホーム画面にウィジェットを追加。

これで
「!」表示が消えて、温度が正常に表示されました。

正常に表示された温度ウィジェット

ひとことメモ

いろんなパターンを試しても改善せず、上記手順で治ったわけですが、おそらくポイントは
アプリ再インストール直後の位置情報の再設定(再インストールせずにこれをやっても改善せず)、その後にウィジェット追加という順序あたりかなと。
iOSアップデート後に設定が微妙にズレたのかもしれません。
同じ症状が出たら、
ウィジェット削除 → アプリ再インストール → 位置情報確認 → ウィジェット追加
を一通りやるのが早そうです。