ncsi-cmd: Add 'discover' subcommand for MCTP

This commit introduces a new 'discover' subcommand to the ncsi-cmd tool,
enabling scanning for available NC-SI packages and channels via MCTP.
The discovery process starts from Package 0 and iterates through
channels to identify functional combinations.
Once a valid package and channel are found, the process stops.

Testing:
./ncsi-cmd -m 91 discover

Change-Id: I2e38b23c5995f3f34e2dc3d22fed4eead8194835
Signed-off-by: eddy lu <puzzy8338@gmail.com>
diff --git a/src/ncsi_cmd.cpp b/src/ncsi_cmd.cpp
index ea0bc12..67c30e8 100644
--- a/src/ncsi_cmd.cpp
+++ b/src/ncsi_cmd.cpp
@@ -45,8 +45,8 @@
 struct GlobalOptions
 {
     std::unique_ptr<Interface> interface;
-    std::optional<unsigned int> package;
-    std::optional<unsigned int> channel;
+    std::optional<uint8_t> package;
+    std::optional<uint8_t> channel;
 };
 
 struct MCTPAddress
@@ -75,6 +75,7 @@
         << "Usage:\n"
         "  " << progname << " <options> raw TYPE [PAYLOAD...]\n"
         "  " << progname << " <options> oem [PAYLOAD...]\n"
+        "  " << progname << " <options> discover\n"
         "  " << progname << " <options> core-dump FILE\n"
         "  " << progname << " <options> crash-dump FILE\n"
         "\n"
@@ -90,6 +91,9 @@
         "\n"
         "Subcommands:\n"
         "\n"
+        "discover\n"
+        "    Scan for available NC-SI packages and channels.\n"
+        "\n"
         "raw TYPE [PAYLOAD...]\n"
         "    Send a single command using raw type/payload data.\n"
         "        TYPE               NC-SI command type, in hex\n"
@@ -321,13 +325,24 @@
         return {};
     }
 
+    // For non-discovery commands, package is required.
+    // If the subcommand is "discover", leave opts.package as nullopt.
     if (!package.has_value())
     {
-        std::cerr << "Missing package, add a --package argument\n";
-        return {};
+        if (optind < argc && std::string(argv[optind]) == "discover")
+        {
+            // Do nothing; package remains nullopt for discovery.
+        }
+        else
+        {
+            std::cerr << "Missing package, add a --package argument\n";
+            return {};
+        }
     }
-
-    opts.package = *package;
+    else
+    {
+        opts.package = static_cast<uint8_t>(*package);
+    }
 
     return std::make_tuple(std::move(opts), optind);
 }
@@ -360,10 +375,11 @@
 static int ncsiCommand(GlobalOptions& options, uint8_t type,
                        std::vector<unsigned char> payload)
 {
-    NCSICommand cmd(type, static_cast<uint8_t>(*options.package),
-                    options.channel,
-                    std::span<unsigned char>(payload.data(), payload.size()));
+    uint8_t pkg = options.package.value_or(0);
+    auto ch = options.channel;
 
+    NCSICommand cmd(type, pkg, ch,
+                    std::span<unsigned char>(payload.data(), payload.size()));
     lg2::debug("Command: type {TYPE}, payload {PAYLOAD_LEN} bytes: {PAYLOAD}",
                "TYPE", lg2::hex, type, "PAYLOAD_LEN", payload.size(), "PAYLOAD",
                toHexStr(payload));
@@ -553,9 +569,9 @@
         auto payloadArray = generateDumpCmdPayload(chunkNum, handle, false);
         std::span<unsigned char> payload(payloadArray.data(),
                                          payloadArray.size());
-
-        NCSICommand cmd(ncsiCmdDump, static_cast<uint8_t>(*options.package),
-                        options.channel, payload);
+        uint8_t pkg = options.package.value_or(0);
+        auto ch = options.channel;
+        NCSICommand cmd(ncsiCmdDump, pkg, ch, payload);
         auto resp = options.interface->sendCommand(cmd);
         if (!resp)
         {
@@ -634,9 +650,9 @@
         auto abortPayloadArray = generateDumpCmdPayload(chunkNum, handle, true);
         std::span<unsigned char> abortPayload(abortPayloadArray.data(),
                                               abortPayloadArray.size());
-        NCSICommand abortCmd(ncsiCmdDump,
-                             static_cast<uint8_t>(*options.package),
-                             options.channel, abortPayload);
+        uint8_t pkg = options.package.value_or(0);
+        auto ch = options.channel;
+        NCSICommand abortCmd(ncsiCmdDump, pkg, ch, abortPayload);
         auto abortResp = options.interface->sendCommand(abortCmd);
         if (!abortResp)
         {
@@ -674,6 +690,99 @@
     return ncsiDump(options, handle, argv[1]);
 }
 
+static int ncsiDiscover(GlobalOptions& options)
+{
+    constexpr uint8_t ncsiClearInitialState = 0x00;
+    constexpr uint8_t ncsiGetCapabilities = 0x16;
+    constexpr unsigned int maxPackageIndex = 8;  // Packages 0–7
+    constexpr unsigned int maxChannelCount = 32; // Channels 0–31
+
+    std::cout << "Starting NC-SI Package and Channel Discovery...\n";
+
+    unsigned int startPackage = 0, endPackage = maxPackageIndex;
+    if (options.package.has_value())
+    {
+        startPackage = *options.package;
+        endPackage = *options.package;
+        std::cout << "Restricting discovery to Package " << *options.package
+                  << ".\n";
+    }
+
+    for (unsigned int packageIndex = startPackage; packageIndex <= endPackage;
+         ++packageIndex)
+    {
+        std::cout << "Checking Package " << packageIndex << "...\n";
+
+        // For each channel from 0..7, we:
+        //   1) Send Clear Initial State.
+        //   2) If that succeeds, send Get Capabilities.
+        //   3) If we get a valid response, we parse and stop.
+        bool foundChannel = false;
+        for (unsigned int channelIndex = 0; channelIndex < maxChannelCount;
+             ++channelIndex)
+        {
+            std::cout << "  Clearing Initial State on Channel " << channelIndex
+                      << "...\n";
+            {
+                std::vector<unsigned char> clearPayload; // No payload
+                NCSICommand clearCmd(ncsiClearInitialState,
+                                     static_cast<uint8_t>(packageIndex),
+                                     static_cast<uint8_t>(channelIndex),
+                                     std::span<unsigned char>(clearPayload));
+
+                auto clearResp = options.interface->sendCommand(clearCmd);
+                if (!clearResp || clearResp->response != 0x0000)
+                {
+                    std::cout << "    Clear Initial State failed on Channel "
+                              << channelIndex << ". Trying next channel.\n";
+                    continue; // Try next channel
+                }
+            }
+
+            // Now that Clear Initial State succeeded, try Get Capabilities
+            std::vector<unsigned char> payload; // No payload
+            NCSICommand getCapCmd(ncsiGetCapabilities,
+                                  static_cast<uint8_t>(packageIndex),
+                                  static_cast<uint8_t>(channelIndex),
+                                  std::span<unsigned char>(payload));
+
+            auto resp = options.interface->sendCommand(getCapCmd);
+            if (resp && resp->response == 0x0000 &&
+                resp->full_payload.size() >= 52)
+            {
+                uint8_t channelCount = resp->full_payload[47];
+                std::cout << "    Package " << packageIndex << " supports "
+                          << static_cast<int>(channelCount) << " channels.\n";
+                std::cout << "    Found available Package " << packageIndex
+                          << ", Channel " << channelIndex
+                          << ". Stopping discovery.\n";
+                foundChannel = true;
+                break;
+            }
+            else
+            {
+                std::cout << "    Channel " << channelIndex
+                          << " not responding. Trying next channel.\n";
+            }
+        } // end channel loop
+
+        if (foundChannel)
+        {
+            // We found a channel on this package, so we stop discovery
+            return 0;
+        }
+        else
+        {
+            std::cout << "  No valid channels found for Package "
+                      << packageIndex << ". Moving to next package.\n";
+        }
+    } // end package loop
+
+    std::cout
+        << "No available NC-SI packages or channels found. Discovery complete.\n";
+    return -1;
+}
+
 /* A note on log output:
  * For output that relates to command-line usage, we just output directly to
  * stderr. Once we have a properly parsed command line invocation, we use lg2
@@ -706,6 +815,14 @@
     argv += consumed;
 
     std::string subcommand = argv[0];
+
+    // For non-discovery commands, package must be provided.
+    if (subcommand != "discover" && !globalOptions.package.has_value())
+    {
+        std::cerr << "Missing package, add a --package argument\n";
+        return EXIT_FAILURE;
+    }
+
     int ret = -1;
 
     if (subcommand == "raw")
@@ -720,6 +837,10 @@
     {
         ret = ncsiCommandReceiveDump(globalOptions, subcommand, argc, argv);
     }
+    else if (subcommand == "discover")
+    {
+        return ncsiDiscover(globalOptions);
+    }
     else
     {
         std::cerr << "Unknown subcommand '" << subcommand << "'\n";