Nodejs react-switch to send command to Chirpstack

Good day all I am new to using Chirpstack and HTML I am needing help. I am trying to build a nodejs website application to be able to view the devices and send on/off commands to devices that have digital outputs with toggle switches on the nodejs webpage I am building. So I have managed to get toggle switches where I need them and the state of the toggle changes when i use the milesight toolbox app to change the state. but now I am struggling to figure out how do I get the toggle switch to send the command to Chirpstack.

this is the base64 command layout
gpio_out_1 on ‘{“devEui”:“24e124445d354713”,“confirmed”:true,“fPort”:85,“data”:“BwEB”}’ gpio_out_1 off ‘{“devEui”:“24e124445d354713”,“confirmed”:true,“fPort”:85,“data”:“BwEA”}’ gpio_out_2 on ‘{“devEui”:“24e124445d354713”,“confirmed”:true,“fPort”:85,“data”:“CAEB”}’ gpio_out_2 off ‘{“devEui”:“24e124445d354713”,“confirmed”:true,“fPort”:85,“data”:“CAEA”}’

this is what shows in Chirpstack when i change the state of the output with the toolbox

Screenshot 2025-04-14 113649

Screenshot 2025-04-14 113743

Screenshot 2025-04-14 113804

Screenshot 2025-04-14 113823

You will want to use Chirpstack’s gRPC api and the enqueueDownlink function.

Thanks, but i am trying to use the mqtt method
here is the downlink section in my mqtt.js

function sendRawHexCommand(devEui, hexData, fPort = 85, confirmed = false) {
    const downlinkTopic = DOWNLINK_TOPIC_TEMPLATE.replace('{devEui}', devEui);
    
    const payload = {
        devEui: devEui,
        confirmed: confirmed,
        fPort: fPort,
        data: hexData // Send raw hex string directly
    };

    client.publish(downlinkTopic, JSON.stringify(payload), (err) => {
        if (err) {
            console.error('MQTT Publish Error:', err);
            return false;
        }
        console.log('Raw hex command sent:', {
            devEui,
            hexData,
            topic: downlinkTopic
        });
        return true;
    });
}

the section in my DeviceContext.jsx

  // Control output function
  const controlOutput = useCallback(async (deviceId, payload) => {
    try {
        // For GPIO output commands
        if (payload.data && typeof payload.data === 'string') {
            await sendRawHexCommand( // Make sure this matches your exported function name
                deviceId,
                payload.data,
                payload.fPort || 85,
                payload.confirmed || true
            );
        } else {
            throw new Error('Invalid payload format - hex data required');
        }
    } catch (err) {
        console.error('Downlink failed:', {
            deviceId,
            payload,
            error: err.message
        });
        throw new Error(`Command failed: ${err.message}`); // More descriptive error
    }
}, []);

then the GeneratorController.jsx

  const handleToggleOutput = async (deviceId, outputName) => {
    setError(null);
    setLoadingOutput(outputName);
    
    // Get current value BEFORE the try block
    const currentValue = getDeviceData(deviceId)[outputName];
    const newValue = currentValue === "on" ? "off" : "on";
    
    try {
        // Update local state immediately - use functional update
        setLocalDeviceState(prev => {
            const newState = {...prev};
            newState[deviceId] = {
                ...(newState[deviceId] || {}), // Ensure device exists
                [outputName]: newValue
            };
            return newState;
        });

        // Create the payload
        const payload = {
            devEui: deviceId,
            confirmed: true,
            fPort: 85,
            data: outputName === 'gpio_out_1' 
                ? (newValue === 'on' ? '070100' : '070101')
                : (newValue === 'on' ? '080100' : '080101')
        };
        
        await controlOutput(deviceId, payload);
        
    } catch (err) {
        setError(`Failed to toggle ${outputName}. Please try again.`);
        console.error('Toggle failed:', {
            deviceId,
            outputName,
            currentValue, // Now accessible
            error: err.message
        });
        
        // Revert local state safely
        setLocalDeviceState(prev => {
            const newState = {...prev};
            if (newState[deviceId]) {
                newState[deviceId] = {
                    ...newState[deviceId],
                    [outputName]: currentValue // Use the captured value
                };
            }
            return newState;
        });
    } finally {
        setLoadingOutput(null);
    }
};

the error when i inspect my webpage is this

DeviceContext.jsx:51 Downlink failed: 
Object
deviceId
: 
"24e124445d354713"
error
: 
"sendRawHexCommand is not defined"
payload
: 
confirmed
: 
false
data
: 
"070101"
devEui
: 
"24e124445d354713"
fPort
: 
85
[[Prototype]]
: 
Object
[[Prototype]]
: 
Object

GeneratorController.jsx:65 Toggle failed: 
Object
currentValue
: 
"on"
deviceId
: 
"24e124445d354713"
error
: 
"Command failed: sendRawHexCommand is not defined"
outputName
: 
"gpio_out_1"

and on Chirpstack events for the device I am trying to control there is no events coming though not even errors.

Not sure if I am missing something

Nothing about this code jumps out at me as wrong. Are you sure you are posting the downlink commands to the correct topic?

application/APPLICATION_ID/device/DEV_EUI/event/EVENT

If you subscribe to your MQTT broker do you see them appearing there when you trigger your script? Is your MQTT integration enabled on that broker?

I will double check the topic and I will try see if I can monitor the triggers with mqtt explorer

I checked the downlink topic it is this
// Add downlink topic template
const DOWNLINK_TOPIC_TEMPLATE = application/${TENANT_intellisec_office_APP_ID}/device/{devEui}/command/down;

So I am guessing the way you explained it i need to change the end so it looks like this
const DOWNLINK_TOPIC_TEMPLATE = application/${TENANT_intellisec_office_APP_ID}/device/{devEui}/event/EVENT;

I did some looking into my code and in my devices.js file I think the error is not sure where I got help with this but I am pretty sure it is causing problems

// Send command to device
router.post('/devices/:devEui/command/:gpio/:state', async (req, res) => {
    const { devEui, gpio, state } = req.params;
    
    try {
        // Validate inputs
        if (!['gpio_out_1', 'gpio_out_2'].includes(gpio)) {
            return res.status(400).json({ error: 'Invalid GPIO specified' });
        }
        
        if (!['on', 'off'].includes(state)) {
            return res.status(400).json({ error: 'Invalid state specified' });
        }
        
        // Construct the command payload based on your template
        let data;
        if (gpio === 'gpio_out_1') {
            data = state === 'on' ? 'BwEB' : 'BwEA';
        } else if (gpio === 'gpio_out_2') {
            data = state === 'on' ? 'CAEB' : 'CAEA';
        }
        
        const commandPayload = {
            devEui: devEui,
            confirmed: true,
            fPort: 85,
            data: data
        };
        
        // Publish the command via MQTT
        const commandTopic = `eu868/gateway/${req.body.gatewayId || 'default-gateway'}/command/${gpio}`;
        mqtt.client.publish(commandTopic, JSON.stringify(commandPayload), (err) => {
            if (err) {
                console.error('Error publishing command:', err);
                return res.status(500).json({ error: 'Failed to send command' });
            }
            
            res.json({ 
                success: true,
                message: `Command sent to ${gpio}: ${state}`,
                topic: commandTopic,
                payload: commandPayload
            });
        });
    } catch (error) {
        console.error('Error sending command:', error);
        res.status(500).json({ error: 'Failed to process command' });
    }
});

so when I am in Chirpstack and I go to the device and queue I enter the port number and select base64 and with these prompts BwAA and BwEA I can turn the output on and off.
In the Chirpstack document I found this

Scheduling a downlink

The default topic for scheduling downlink payloads is: application/APPLICATION_ID/device/DEV_EUI/command/down.

The Application ID and DevEUI of the device will be taken from the topic.

Example payload:

{
    "devEui": "0102030405060708",             // this must match the DEV_EUI of the MQTT topic
    "confirmed": true,                        // whether the payload must be sent as confirmed data down or not
    "fPort": 10,                              // FPort to use (must be > 0)
    "data": "...."                            // base64 encoded data (plaintext, will be encrypted by ChirpStack)
    "object": {                               // decoded object (when application coded has been configured)
        "temperatureSensor": {"1": 25},       // when providing the 'object', you can omit 'data'
        "humiditySensor": {"1": 32}
    }
}

In mqtt explorer i have been trying to see if i can publish the command but don’t seem to be getting it right I have tried these command lines.

application/5fp57dfa-7e1b-4d23-858a-ed60389aa741/device/24e124445d354713/command/down/{"confirmed":false,"fport":85,"data":"BwAA"}
application/5fp57dfa-7e1b-4d23-858a-ed60389aa741/device/24e124445d354713/command/{"confirmed":false,"fport":85,"data":"BwAA"}
application/5fp57dfa-7e1b-4d23-858a-ed60389aa741/device/24e124445d354713/event/{"confirmed":false,"fport":85,"data":"BwAA"}
application/5fp57dfa-7e1b-4d23-858a-ed60389aa741/device/24e124445d354713/event/EVENT/{"confirmed":false,"fport":85,"data":"BwAA"}

And nothing gets received in chirpstack and no change in the device output

Hey sorry you’re totally right. The correct topic is:

application/APPLICATION_ID/device/DEV_EUI/command/down

Not sure how I messed that up.

and that first downlink topic you shared would be right:

except there shouldn’t be a ’ / ’ at the end. Also these payloads are missing devEui.

For your script, your command topic here seems a little fishy.

There is no reason to have the end of the string pulled from a variable. It should always be ‘down’. As well, the ‘gpio’ value seems to evaluate to ‘gpio_out_1’ or similar, which certainly isn’t the topic you should post to.

If you can share what you are seeing in MQTT explorer when you trigger your downlink script (rather than the command lines you’ve tried) it should be easy to clear up where the topic or payload are wrong.

EDIT: looking at it again the whole command topic is wrong. It is using the <region-prefix.>/gateway/… topic structure not application/APPLICATION_ID/device/DEV_EUI/command/down

The region-prefix/… messages are what Chirpstack will translate your application/… messages into after it encrypts them. Posting on that topic entirely bypasses chirpstack, sending the command directly to the gateway (in the wrong format as Chirpstack encrypts and encodes the regular downlink payloads) which is not what you want to do.

So I am trying to sort out code, I have removed the fishy looking code. Still no response when I try to toggle the switch on the web platform. Still got errors when I inspect the page
so when i send the command in MQTT explorer


Screenshot 2025-04-23 112805

application/5fp57dfa-7e1b-4d23-858a-ed60389aa741/device/24e124445d354713/command/down { "devEui": "24e124445d354713", "confirmed": true, "fPort": 85, "data": "BwAA" }

this is what I get in chirpstack logs when I publish in mqtt explorer

2025-04-23 11:39:04 2025-04-23T09:39:04.113784Z  INFO chirpstack::integration::mqtt: Command received for device topic=application/5fp57dfa-7e1b-4d23-858a-ed60389aa741/device/24e124445d354713/command/down { "devEui": "24e124445d354713", "confirmed": true, "fPort": 85, "data": "BwEA" } qos=AtMostOnce
2025-04-23 11:39:04 2025-04-23T09:39:04.113878Z  WARN chirpstack::integration::mqtt: Processing command error: EOF while parsing a value at line 1 column 0 topic=application/5fp57dfa-7e1b-4d23-858a-ed60389aa741/device/24e124445d354713/command/down { "devEui": "24e124445d354713", "confirmed": true, "fPort": 85, "data": "BwEA" } qos=AtMostOnce

So I finally got the command to work with mqtt explorer
topic
application/5fb57dfa-7e1b-4d23-858a-ed60389aa741/device/24e124445d354713/command/down

{
  "devEui": "24e124445d354713",
  "confirmed": true,
  "fPort": 85,
  "data": "BwAA"
}

now to figure out how to get the website buttons to send the commands to chirpstack

@Liam_Philipp good day hope you are well. So I have been trying to get this mqtt commands to work and I am not coming right. I am at a point where i am tempted to try the gRPC API connection instead. I feel like it will be less of a headache but will be something I need to learn from scratch are there any tips for setting up gRPC API to link chirpstack to the nodejs application. thanks in advance.

It’s your lucky day!

Nice premade functions. name this device_service.js:

const grpc = require("@grpc/grpc-js");
const device_grpc = require("@chirpstack/chirpstack-api/api/device_grpc_pb");
const device_pb = require("@chirpstack/chirpstack-api/api/device_pb");

/**
 * Create and return a gRPC client for the DeviceService.
 * @param {string} server - The gRPC server address.
 * @param {string} apiToken - The API token for authentication.
 * @returns {{client: Object, metadata: Object}} - The gRPC client and metadata.
 */
function createDeviceClient(server, apiToken) {
  const client = new device_grpc.DeviceServiceClient(
    server,
    grpc.credentials.createSsl()
  );

  const metadata = new grpc.Metadata();
  metadata.set("authorization", "Bearer " + apiToken);

  return { client, metadata };
}

/**
 * Create a new device.
 * @param {Object} client - The gRPC client.
 * @param {Object} metadata - The gRPC metadata containing the API token.
 * @param {string} devEui - The unique identifier of the device (16-character hexadecimal).
 * @param {string} name - The name of the device.
 * @param {string} applicationId - The UUID of the application the device belongs to.
 * @param {string} deviceProfileId - The UUID of the device profile.
 * @returns {Promise<void>} - Resolves when the device is created.
 */
async function createDevice(client, metadata, devEui, name, applicationId, deviceProfileId) {
  return new Promise((resolve, reject) => {
    // Validate inputs
    if (!/^[0-9A-Fa-f]{16}$/.test(devEui)) {
      return reject(new Error("Invalid devEUI. It must be a 16-character hexadecimal string."));
    }
    if (!/^[a-zA-Z0-9\s]+$/.test(name)) {
      return reject(new Error("Invalid name. It must only contain letters, numbers, or spaces."));
    }
    if (!/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(applicationId)) {
      return reject(new Error("Invalid applicationId. It must be a valid UUID string."));
    }
    if (!/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(deviceProfileId)) {
      return reject(new Error("Invalid deviceProfileId. It must be a valid UUID string."));
    }

    // Create the Device object
    const device = new device_pb.Device();
    device.setDevEui(devEui);
    device.setName(name);
    device.setApplicationId(applicationId);
    device.setDeviceProfileId(deviceProfileId);
    device.setDescription("Device created via gRPC");

    // Create the CreateDeviceRequest
    const createDeviceReq = new device_pb.CreateDeviceRequest();
    createDeviceReq.setDevice(device);

    // Send the CreateDeviceRequest
    client.create(createDeviceReq, metadata, (err) => {
      if (err) {
        return reject(err);
      }
      console.log("Device successfully created with DevEUI:", devEui);
      resolve();
    });
  });
}

/**
 * Delete a device.
 * @param {Object} client - The gRPC client.
 * @param {Object} metadata - The gRPC metadata.
 * @param {string} devEui - The unique identifier of the device (16-character hexadecimal).
 * @returns {Promise<void>} - Resolves when the device is deleted.
 */
async function deleteDevice(client, metadata, devEui) {
  return new Promise((resolve, reject) => {
    // Validate devEUI
    if (!/^[0-9A-Fa-f]{16}$/.test(devEui)) {
      return reject(new Error("Invalid devEUI. It must be a 16-character hexadecimal string."));
    }

    // Create the DeleteDeviceRequest
    const deleteDeviceReq = new device_pb.DeleteDeviceRequest();
    deleteDeviceReq.setDevEui(devEui);

    // Send the DeleteDeviceRequest
    client.delete(deleteDeviceReq, metadata, (err) => {
      if (err) {
        return reject(err);
      }
      console.log("Device successfully deleted with DevEUI:", devEui);
      resolve();
    });
  });
}

/**
 * Create keys for a device.
 * @param {Object} client - The gRPC client.
 * @param {Object} metadata - The gRPC metadata.
 * @param {string} devEui - The unique identifier of the device (16-character hexadecimal).
 * @param {string} nwkKey - The network session key (32-character hexadecimal).
 * @param {string} [appKey] - (Optional) The application session key (32-character hexadecimal).
 * @returns {Promise<void>} - Resolves when the keys are created.
 */
async function createDeviceKeys(client, metadata, devEui, nwkKey, appKey) {
  return new Promise((resolve, reject) => {
    // Validate devEUI
    if (!/^[0-9A-Fa-f]{16}$/.test(devEui)) {
      return reject(new Error("Invalid devEUI. It must be a 16-character hexadecimal string."));
    }

    // Validate nwkKey
    if (!/^[0-9A-Fa-f]{32}$/.test(nwkKey)) {
      return reject(new Error("Invalid nwkKey. It must be a 32-character hexadecimal string."));
    }

    // Validate appKey if provided
    if (appKey && !/^[0-9A-Fa-f]{32}$/.test(appKey)) {
      return reject(new Error("Invalid appKey. It must be a 32-character hexadecimal string."));
    }

    // Create the DeviceKeys object
    const deviceKeys = new device_pb.DeviceKeys();
    deviceKeys.setDevEui(devEui);
    deviceKeys.setNwkKey(nwkKey);

    // Set the appKey only if it's provided
    if (appKey) {
      deviceKeys.setAppKey(appKey);
    }

    // Create the CreateDeviceKeysRequest
    const createDeviceKeysReq = new device_pb.CreateDeviceKeysRequest();
    createDeviceKeysReq.setDeviceKeys(deviceKeys);

    // Send the CreateDeviceKeysRequest
    client.createKeys(createDeviceKeysReq, metadata, (err) => {
      if (err) {
        return reject(err);
      }
      console.log("Device keys successfully created for DevEUI:", devEui);
      resolve();
    });
  });
}

/**
 * Update keys for a device.
 * @param {Object} client - The gRPC client.
 * @param {Object} metadata - The gRPC metadata.
 * @param {string} devEui - The unique identifier of the device (16-character hexadecimal).
 * @param {string} nwkKey - The network session key (32-character hexadecimal).
 * @param {string} [appKey] - (Optional) The application session key (32-character hexadecimal).
 * @returns {Promise<void>} - Resolves when the keys are updated.
 */
async function updateDeviceKeys(client, metadata, devEui, nwkKey, appKey) {
  return new Promise((resolve, reject) => {
    // Validate devEUI
    if (!/^[0-9A-Fa-f]{16}$/.test(devEui)) {
      return reject(new Error("Invalid devEUI. It must be a 16-character hexadecimal string."));
    }

    // Validate nwkKey
    if (!/^[0-9A-Fa-f]{32}$/.test(nwkKey)) {
      return reject(new Error("Invalid nwkKey. It must be a 32-character hexadecimal string."));
    }

    // Validate appKey if provided
    if (appKey && !/^[0-9A-Fa-f]{32}$/.test(appKey)) {
      return reject(new Error("Invalid appKey. It must be a 32-character hexadecimal string."));
    }

    // Create the DeviceKeys object
    const deviceKeys = new device_pb.DeviceKeys();
    deviceKeys.setDevEui(devEui);
    deviceKeys.setNwkKey(nwkKey);

    // Set the appKey only if it's provided
    if (appKey) {
      deviceKeys.setAppKey(appKey);
    }

    // Create the UpdateDeviceKeysRequest
    const updateDeviceKeysReq = new device_pb.UpdateDeviceKeysRequest();
    updateDeviceKeysReq.setDeviceKeys(deviceKeys);

    // Send the UpdateDeviceKeysRequest
    client.updateKeys(updateDeviceKeysReq, metadata, (err) => {
      if (err) {
        return reject(err);
      }
      console.log("Device keys successfully updated for DevEUI:", devEui);
      resolve();
    });
  });
}

/**
 * Disable or enable a device.
 * @param {Object} client - The gRPC client.
 * @param {Object} metadata - The gRPC metadata.
 * @param {string} devEui - The unique identifier of the device (16-character hexadecimal).
 * @param {boolean} isDisabled - Whether the device should be disabled (true) or enabled (false).
 * @returns {Promise<void>} - Resolves when the device is updated.
 */
async function disableDevice(client, metadata, devEui, isDisabled) {
  return new Promise((resolve, reject) => {
    // Validate devEUI
    if (!/^[0-9A-Fa-f]{16}$/.test(devEui)) {
      return reject(new Error("Invalid devEUI. It must be a 16-character hexadecimal string."));
    }

    // Validate and convert isDisabled
    if (![true, false, 1, 0].includes(isDisabled)) {
      return reject(new Error("Invalid isDisabled. It must be true, false, 1, or 0."));
    }

    // Create the GetDeviceRequest
    const getDeviceReq = new device_pb.GetDeviceRequest();
    getDeviceReq.setDevEui(devEui);

    // Retrieve existing device details
    client.get(getDeviceReq, metadata, (err, resp) => {
      if (err) {
        return reject(new Error(`Error while retrieving device details: ${err.message}`));
      }

      const device = resp.getDevice();

      // Update the `is_disabled` field
      device.setIsDisabled(isDisabled);

      // Create the UpdateDeviceRequest
      const updateDeviceReq = new device_pb.UpdateDeviceRequest();
      updateDeviceReq.setDevice(device);

      // Send the UpdateDeviceRequest
      client.update(updateDeviceReq, metadata, (err) => {
        if (err) {
          return reject(new Error(`Error while updating the device: ${err.message}`));
        }
        console.log(`Device with DevEUI ${devEui} successfully updated. is_disabled set to ${isDisabled}.`);
        resolve();
      });
    });
  });
}

/**
 * Enqueue a downlink message for a device.
 * @param {Object} client - The gRPC client.
 * @param {Object} metadata - The gRPC metadata.
 * @param {string} devEui - The unique identifier of the device (16-character hexadecimal).
 * @param {number} fport - The FPort number (0-255) on which the downlink will be sent.
 * @param {string} data - The hex payload to send in the downlink (comma-separated bytes, e.g., "A1,B2,C3").
 * @returns {Promise<string>} - Resolves with the ID of the enqueued downlink.
 */
async function enqueueDownlink(client, metadata, devEui, fport, data) {
  return new Promise((resolve, reject) => {
    // Validate devEUI
    if (!/^[0-9A-Fa-f]{16}$/.test(devEui)) {
      return reject(new Error("Invalid devEUI. It must be a 16-character hexadecimal string."));
    }

    // Validate fport
    const fportInt = parseInt(fport, 10);
    if (isNaN(fportInt) || fportInt < 0 || fportInt > 255) {
      return reject(new Error("Invalid fport. It must be an integer between 0 and 255."));
    }

    // Validate and convert data
    let dataBytes;
    try {
      dataBytes = new Uint8Array(data.split(",").map(hex => {
        const byte = parseInt(hex, 16);
        if (isNaN(byte) || byte < 0x00 || byte > 0xFF) {
          throw new Error(`Invalid byte: ${hex}`);
        }
        return byte;
      }));
    } catch (err) {
      return reject(new Error("Invalid data. Ensure all values are valid hexadecimal bytes (e.g., A1,B2,C3)."));
    }

    // Create the DeviceQueueItem
    const item = new device_pb.DeviceQueueItem();
    item.setDevEui(devEui);
    item.setFPort(fportInt);
    item.setConfirmed(true);
    item.setData(dataBytes);

    // Create the EnqueueDeviceQueueItemRequest
    const enqueueReq = new device_pb.EnqueueDeviceQueueItemRequest();
    enqueueReq.setQueueItem(item);

    // Send the request
    client.enqueue(enqueueReq, metadata, (err, resp) => {
      if (err) {
        return reject(new Error(`Error during enqueue: ${err.message}`));
      }
      const downlinkId = resp.getId();
      console.log("Downlink has been enqueued with ID:", downlinkId);
      resolve(downlinkId);
    });
  });
}

module.exports = { createDeviceClient, createDevice, deleteDevice, createDeviceKeys, updateDeviceKeys, disableDevice, enqueueDownlink };

Usage example for downlinks, where the device_service.js is in the same folder:

const { createDeviceClient, enqueueDownlink } = require("./device_service");

(async () => {
  const server = "<your-server>:<grpc-port>";
  const apiToken = "<token-retrieved-from-ui>";

  // Initialize the gRPC client and metadata
  const { client, metadata } = createDeviceClient(server, apiToken);

  try {
    const devEui = "0101010101010101";
    const fport = 10;
    const data = "A1,B2,C3"; // Comma-separated hex bytes

    // Enqueue a downlink
    const downlinkId = await enqueueDownlink(client, metadata, devEui, fport, data);
    console.log("Enqueued downlink ID:", downlinkId);
  } catch (err) {
    console.error("Error:", err.message);
  } finally {
    // Close the client
    client.close();
  }
})();

You’ll need to install the Chirpstack API and grpc modules as well.
npm install @chirpstack/chirpstack-api
npm install @grpc/grpc-js

Also I’m not sure how much you’ve done to your actual Chirpstack setup but ideally you’d have TLS on the frontend through a reverse-proxy for securing the web UI and grpc API. That is what my scripts above are for, and if you do not have TLS you will have to adjust the client.create call to match the example here: JavaScript examples - ChirpStack open-source LoRaWAN® Network Server documentation

Although I am surprised you are not able to get the MQTT working. You were able to send the proper downlink through mosquitto_pub but are struggling to reproduce that with nodered?

thanks for this, just making sure i am on the right page so in the chirpstack on github the folder API i added all the proto files can i remove them and put the device_service.js in its place or do i add it into the folder with the proto files.

And then create a downlinks.js with the second code example that will handle the downlink commands to chirpstack from the node.js application.

Then with any other info needed like tenant and user make another .js file that includes them for setting up a login page. Am I correct in thinking this way?

for now I am trying to create a test environment once I have everything working then will add TLS.

with the MQTT i was struggling to get the react-switch toggle to send the commands and the other reason why i am thinking the API approach would be better for when i create a login page and there are multiple tenants and multiple users, think it will be better for the structure and integrity of the platform i am trying to put together.

@Liam_Philipp
Really appreciate the help so far could you please help show me how I need to add the gRPC API so I use it instead of the mqtt

this is my current server.js

const express = require("express");
const cors = require('cors');
const path = require('path');
const EventEmitter = require('events');

global.mqttEventEmitter = new EventEmitter();

const app = express();
app.use(cors());
app.use(express.json());

// Serve static files
app.use(express.static(path.join(__dirname, 'public')));

// If your static files are in 'dist', also serve them
app.use('/dist', express.static(path.join(__dirname, 'dist')));

// Import device routes
const deviceRoutes = require('./src/routes/devices');
app.use('/api', deviceRoutes);

// Serve the main HTML file
app.get('/', (req, res) => {
    res.sendFile(path.join(__dirname, 'public', 'dist', 'index.html'));
});

// Import and initialize MQTT client
const mqtt = require('./public/mqtt');

// Error handling middleware
app.use((err, req, res, next) => {
    console.error(err.stack);
    res.status(500).json({ error: 'Something went wrong!' });
});

const PORT = process.env.PORT || 4001;
app.listen(PORT, () => {
    console.log(`Server running at http://localhost:${PORT}`);
});

this is my current docker-compose.yml

services:
  chirpstack:
    image: chirpstack/chirpstack:4
    command: -c /etc/chirpstack
    restart: unless-stopped
    volumes:
      - ./configuration/chirpstack:/etc/chirpstack
    depends_on:
      - postgres
      - mosquitto
      - redis
    environment:
      - MQTT_BROKER_HOST=mosquitto
      - REDIS_HOST=redis
      - POSTGRESQL_HOST=postgres
    ports:
      - "8080:8080"
    networks:
      - chirpstack-net

  chirpstack-gateway-bridge:
    image: chirpstack/chirpstack-gateway-bridge:4
    restart: unless-stopped
    ports:
      - "1700:1700/udp"
    volumes:
      - ./configuration/chirpstack-gateway-bridge:/etc/chirpstack-gateway-bridge
    environment:
      - INTEGRATION__MQTT__EVENT_TOPIC_TEMPLATE=eu868/gateway/{{ .GatewayID }}/event/{{ .EventType }}
      - INTEGRATION__MQTT__STATE_TOPIC_TEMPLATE=eu868/gateway/{{ .GatewayID }}/state/{{ .StateType }}
      - INTEGRATION__MQTT__COMMAND_TOPIC_TEMPLATE=eu868/gateway/{{ .GatewayID }}/command/#
    depends_on:
      - mosquitto
    networks:
      - chirpstack-net

  chirpstack-gateway-bridge-basicstation:
    image: chirpstack/chirpstack-gateway-bridge:4
    restart: unless-stopped
    command: -c /etc/chirpstack-gateway-bridge/chirpstack-gateway-bridge-basicstation-eu868.toml
    ports:
      - "3001:3001"
    volumes:
      - ./configuration/chirpstack-gateway-bridge:/etc/chirpstack-gateway-bridge
    depends_on:
      - mosquitto
    networks:
      - chirpstack-net

  chirpstack-rest-api:
    image: chirpstack/chirpstack-rest-api:4
    restart: unless-stopped
    command: --server chirpstack:8080 --bind 0.0.0.0:8090 --insecure
    ports:
      - "8091:8090"
    depends_on:
      - chirpstack
    networks:
      - chirpstack-net

  postgres:
    image: postgres:14-alpine
    restart: unless-stopped
    volumes:
      - ./configuration/postgresql/initdb:/docker-entrypoint-initdb.d
      - postgresqldata:/var/lib/postgresql/data
    environment:
      - POSTGRES_USER=chirpstack
      - POSTGRES_PASSWORD=chirpstack
      - POSTGRES_DB=chirpstack
    ports:
      - "5432:5432"
    networks:
      - chirpstack-net

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: redis-server --save 300 1 --save 60 100 --appendonly no
    volumes:
      - redisdata:/data
    networks:
      - chirpstack-net

  mosquitto:
    image: eclipse-mosquitto:2
    restart: unless-stopped
    ports:
      - "1883:1883"
    volumes:
      - ./configuration/mosquitto/config/:/mosquitto/config/
    networks:
      - chirpstack-net

  node-app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "4001:4001"
    environment:
      - MQTT_BROKER=tcp://mosquitto:1883
      - POSTGRES_HOST=#######
      - POSTGRES_USER=#######
      - POSTGRES_PASSWORD=########
      - POSTGRES_DB=#########
      - POSTGRES_PORT=####
    volumes:
      - .:/app
    depends_on:
      - mosquitto
      - postgres
    restart: unless-stopped
    networks:
      - chirpstack-net

volumes:
  postgresqldata:
  redisdata:

networks:
  chirpstack-net:
    driver: bridge

You still need all the Chirpstack API modules, as you can see on the top of my device_service.js, it requires both the chirpstack API modules (or atleast two of them) and the grpc module. Rather then adding the modules from the github page it’s easier to just download them with the npm commands I showed in the previous message. Add the device_service.js wherever you want, it doesn’t matter as long as you reference it properly in the require statement of whatever script uses it. For example my usage script has require("./device_service"); so it is looking for the device_service in the same directory as itself.

The second code example is only meant to show how to retrieve and use the enqueueDownlink function in a separate script from the device_service.js, it’s not a script to be used in itself.

Not sure if I understand this question 100% but yes you would have to create your own application_service.js or tenant_service.js for the rest of the API commands if you want to use that functionality. The script I shared with you only covers some of the API calls relating to devices, but none for applications or tenants.

I can’t tell you how to code your own server and when it should use these calls, it entirely depends on your use case. I can only show you how to use the scripts I shared. Besides, I’m no good at working with HTML :sweat_smile:

Just copy the logic in the example script to wherever you want to trigger a downlink in your own code, and you can use similar logic for all the rest of the functions in my device_service.js if you need to (createDeviceClient, createDevice, deleteDevice, createDeviceKeys, updateDeviceKeys, disableDevice, enqueueDownlink)

Since it seems like you’re going down the application path anyway I also have an application_service.js but it only has the create_application and delete_application. This and the device_service.js is all that we need for the purposes of our own portal.

const grpc = require("@grpc/grpc-js");
const application_grpc = require("@chirpstack/chirpstack-api/api/application_grpc_pb");
const application_pb = require("@chirpstack/chirpstack-api/api/application_pb");

/**
 * Create and return a gRPC client for the ApplicationService.
 * @param {string} server - The gRPC server address.
 * @param {string} apiToken - The API token for authentication.
 * @returns {{client: Object, metadata: Object}} - The gRPC client and metadata.
 */
function createApplicationClient(server, apiToken) {
  const client = new application_grpc.ApplicationServiceClient(
    server,
    grpc.credentials.createSsl()
  );

  const metadata = new grpc.Metadata();
  metadata.set("authorization", "Bearer " + apiToken);

  return { client, metadata };
}

/**
 * Create a new application.
 * @param {Object} client - The gRPC client.
 * @param {Object} metadata - The gRPC metadata.
 * @param {string} name - Name of the application.
 * @param {string} tenantId - Tenant ID (UUID).
 * @param {string} [description=""] - Description of the application.
 * @returns {Promise<string>} - The ID of the newly created application.
 */
async function createApplication(client, metadata, name, tenantId, description = "") {
  return new Promise((resolve, reject) => {
    // Validate inputs
    if (!/^[a-zA-Z0-9\s]+$/.test(name)) {
      return reject(new Error("Invalid name. It must only contain letters, numbers, or spaces."));
    }
    if (!/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(tenantId)) {
      return reject(new Error("Invalid tenantId. It must be a valid UUID."));
    }

    const application = new application_pb.Application();
    application.setName(name);
    application.setTenantId(tenantId);
    application.setDescription(description);

    const createApplicationReq = new application_pb.CreateApplicationRequest();
    createApplicationReq.setApplication(application);

    client.create(createApplicationReq, metadata, (err, resp) => {
      if (err) {
        return reject(err);
      }
      resolve(resp.getId());
    });
  });
}

/**
 * Delete an application.
 * @param {Object} client - The gRPC client.
 * @param {Object} metadata - The gRPC metadata.
 * @param {string} applicationId - ID of the application to delete (UUID).
 * @returns {Promise<void>}
 */
async function deleteApplication(client, metadata, applicationId) {
  return new Promise((resolve, reject) => {
    // Validate inputs
    if (!/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(applicationId)) {
      return reject(new Error("Invalid applicationId. It must be a valid UUID."));
    }

    const deleteApplicationReq = new application_pb.DeleteApplicationRequest();
    deleteApplicationReq.setId(applicationId);

    client.delete(deleteApplicationReq, metadata, (err) => {
      if (err) {
        return reject(err);
      }
      resolve();
    });
  });
}

module.exports = { createApplicationClient, createApplication, deleteApplication };

Then here’s a usage example that creates an application then immediately deletes it:

const { createApplicationClient, createApplication, deleteApplication } = require("./application_service");

(async () => {
  const server = "<your-server>:<your-port>";
  const apiToken = "<your-api-token>";

  // Initialize the gRPC client and metadata
  const { client, metadata } = createApplicationClient(server, apiToken);

  try {
    const tenantId = "52f14cd4-c6f1-4fbd-8f87-4025e1d49242";

    // Create an application
    const applicationId = await createApplication(client, metadata, "MyApp", tenantId, "Test Application");
    console.log("Application created with ID:", applicationId);

    // Delete the application
    await deleteApplication(client, metadata, applicationId);
    console.log("Application deleted successfully.");
  } catch (err) {
    console.error("Error:", err.message);
  } finally {
    // Close the client when done
    client.close();
  }
})();

@Liam_Philipp thanks for the help above I have linked the nodejs to the chirpstack. The thing I a struggling with is i am trying to build a device control that handles the devEui, name, description, battery level, object ( the info from the sensors on the device) and last seen.
gRPC api service

// ChirpStack gRPC client service
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const path = require('path');

// Configuration for ChirpStack connection
const config = {
  // Update these values based on your ChirpStack setup
  host: process.env.CHIRPSTACK_HOST || '',
  port: process.env.CHIRPSTACK_PORT || '',
  apiToken: process.env.CHIRPSTACK_API_TOKEN || ' ', // Your API token
  //tlsEnabled: process.env.CHIRPSTACK_TLS_ENABLED === 'true' || false,
  protoDir: path.resolve(__dirname, '../../proto') // Location where you'll store the .proto files
};

// Create credentials based on configuration
const getCredentials = () => {
  if (config.tlsEnabled) {
    // For production, use secure connection
    return grpc.credentials.createSsl();
  } else {
    // For development, you might use insecure connection
    return grpc.credentials.createInsecure();
  }
};

// Load all the proto files you need
const loadProtoDescriptors = () => {
  // List of services you want to use from ChirpStack
  const services = [
    'device',
    'device_profile',
    'gateway',
    'application',
    'user',
    // Add more services as needed
  ];
  
  const protos = {};
  
  services.forEach(service => {
    const protoPath = path.join(config.protoDir, `${service}.proto`);
    console.log(`Loading proto file: ${protoPath}`);  // Debug logging
    
    try {
        const packageDefinition = protoLoader.loadSync(
            protoPath,  // Use full path
            {
                keepCase: true,
                longs: String,
                enums: String,
                defaults: true,
                oneofs: true,
                includeDirs: [config.protoDir]
            }
        );
        
        protos[service] = grpc.loadPackageDefinition(packageDefinition).api;
        console.log(`Successfully loaded ${service} service`);
    } catch (error) {
        console.error(`Failed to load ${service}.proto:`, error);
        throw error;
    }
});

return protos;
};

// Create service clients
const createClients = (protos) => {
  const clients = {};
  const serverAddress = `${config.host}:${config.port}`;
  const credentials = getCredentials();
  
  // Create a client for each service
  Object.keys(protos).forEach(service => {
    const ServiceClass = protos[service][`${service.charAt(0).toUpperCase() + service.slice(1)}Service`];
    if (ServiceClass) {
      clients[service] = new ServiceClass(serverAddress, credentials);
    }
  });
  
  return clients;
};

// Helper to handle authentication metadata
const getAuthMetadata = () => {
  const metadata = new grpc.Metadata();
  if (config.apiToken) {
    metadata.add('authorization', `Bearer ${config.apiToken}`);
  }
  return metadata;
};

// Initialize all clients
let protos;
let clients;

try {
  protos = loadProtoDescriptors();
  console.log('Loaded proto descriptors:', Object.keys(protos));
  clients = createClients(protos);
  console.log('Initialized gRPC clients:', {
    device: !!clients.device,
    application: !!clients.application,
    deviceQueue: !!clients.deviceQueue
});
} catch (error) {
  console.error('Failed to initialize ChirpStack gRPC clients:', error);
}

// Export functions for interacting with the ChirpStack API
module.exports = {
  // Device operations
  devices: {
    list: (options = {}) => {
        return new Promise((resolve, reject) => {
            if (!clients.device) {
                return reject(new Error('Device service not initialized'));
            }
            
            // Create the proper request structure
            const request = {
                limit: options.limit || 100,
                offset: options.offset || 0,
                application_id: options.application_id,
                search: options.search,
                order_by: options.order_by,
                order_by_desc: options.order_by_desc,
                tags: options.tags,
                device_profile_id: options.device_profile_id
            };

            // Add debug logging
            console.log('Sending ListDevices request:', JSON.stringify(request, null, 2));

            clients.device.List(request, getAuthMetadata(), (err, response) => {
                if (err) {
                    console.error('ListDevices error:', err);
                    return reject(err);
                }
                console.log('ListDevices response:', JSON.stringify(response, null, 2));
                resolve(response);
            });
        });
    },

    get: (id) => {
      return new Promise((resolve, reject) => {
        if (!clients.device) {
          return reject(new Error('Device service not initialized'));
        }
        
        clients.device.Get({ dev_eui: id }, getAuthMetadata(), (err, response) => {
          if (err) return reject(err);
          resolve(response);
        });
      });
    },

    create: (deviceData) => {
      return new Promise((resolve, reject) => {
        if (!clients.device) {
          return reject(new Error('Device service not initialized'));
        }
        
        clients.device.Create(deviceData, getAuthMetadata(), (err, response) => {
          if (err) return reject(err);
          resolve(response);
        });
      });
    },

    update: (deviceData) => {
      return new Promise((resolve, reject) => {
        if (!clients.device) {
          return reject(new Error('Device service not initialized'));
        }
        
        clients.device.Update(deviceData, getAuthMetadata(), (err, response) => {
          if (err) return reject(err);
          resolve(response);
        });
      });
    },

    delete: (id) => {
      return new Promise((resolve, reject) => {
        if (!clients.device) {
          return reject(new Error('Device service not initialized'));
        }
        
        clients.device.Delete({ dev_eui: id }, getAuthMetadata(), (err, response) => {
          if (err) return reject(err);
          resolve(response);
        });
      });
    },

    // ... existing exports ...
    getConnectionStatus: () => {
        return {
            connected: true, // Implement actual check
            services: Object.keys(clients)
        };
    },

    healthCheck: () => {
        return new Promise((resolve) => {
            if (!clients.device) {
                return resolve(false);
            }
            // Simple request to check connectivity
            clients.device.List({ limit: 1 }, getAuthMetadata(), (err) => {
                resolve(!err);
            });
        });
    },



},
  
  // Add similar methods for other services (gateways, applications, etc.)
  applications: {
    // Application related methods
    list: (request = {}) => {
      return new Promise((resolve, reject) => {
        if (!clients.application) {
          return reject(new Error('Application service not initialized'));
        }
        
        clients.application.List(request, getAuthMetadata(), (err, response) => {
          if (err) return reject(err);
          resolve(response);
        });
      });
    },

    deviceQueue: {
        // Enqueue a downlink message
        enqueue: (downlinkData) => {
          return new Promise((resolve, reject) => {
            if (!clients.deviceQueue) {
              return reject(new Error('DeviceQueue service not initialized'));
            }
            
            // Validate required fields
            if (!downlinkData.queue_item || 
                !downlinkData.queue_item.dev_eui || 
                !downlinkData.queue_item.f_port || 
                !downlinkData.queue_item.data) {
              return reject(new Error('Missing required downlink fields'));
            }
            
            clients.deviceQueue.Enqueue(downlinkData, getAuthMetadata(), (err, response) => {
              if (err) return reject(err);
              resolve(response);
            });
          });
        },
        
        // List queued items for a device
        list: (devEui) => {
          return new Promise((resolve, reject) => {
            if (!clients.deviceQueue) {
              return reject(new Error('DeviceQueue service not initialized'));
            }
            
            clients.deviceQueue.List({ dev_eui: devEui }, getAuthMetadata(), (err, response) => {
              if (err) return reject(err);
              resolve(response);
            });
          });
        },
        
        // Flush queue for a device
        flush: (devEui) => {
          return new Promise((resolve, reject) => {
            if (!clients.deviceQueue) {
              return reject(new Error('DeviceQueue service not initialized'));
            }
            
            clients.deviceQueue.Flush({ dev_eui: devEui }, getAuthMetadata(), (err, response) => {
              if (err) return reject(err);
              resolve(response);
            });
          });
        }
      },

    // Add more application methods as needed
  },
  
  // Add more service objects as needed
};

devicecontroller.js

const chirpstack = require('../services/chirpstack');

// Get all devices with pagination
exports.getAllDevices = async (req, res) => {
    try {
        const applicationId = process.env.CHIRPSTACK_USER_APP_ID || '5fb57dfa-7e1b-4d23-858a-ed60389aa741';
        console.log("Using Application ID:", applicationId);

        // Get devices from ChirpStack
        const response = await chirpstack.devices.list({
            limit: 100,
            offset: 0,
            application_id: applicationId
        });

        // Transform the data for your frontend with additional queue status
        const devices = await Promise.all(response.result.map(async (device) => {
            try {
                // Get queue items for each device
                const queueResponse = await chirpstack.applications.deviceQueue.list(device.dev_eui);
                
                // Find the most recent queue item with an object
                const latestObjectItem = queueResponse.result
                    .filter(item => item.object)
                    .sort((a, b) => 
                        (b.expires_at?.seconds || 0) - (a.expires_at?.seconds || 0)
                    )[0];

                return {
                    devEui: device.dev_eui,
                    name: device.name,
                    description: device.description,
                    lastSeen: device.last_seen_at ? 
                        new Date(device.last_seen_at.seconds * 1000).toISOString() : 'Never',
                    batteryLevel: device.device_status?.battery_level,
                    isActive: device.device_status?.external_power_source || 
                            (device.device_status?.battery_level > 0),
                    deviceProfile: device.device_profile_name,
                    signalStrength: device.device_status?.margin,
                    object: latestObjectItem?.object || null,
                    queueStatus: {
                        pendingCount: queueResponse.result.filter(item => item.is_pending).length,
                        totalCount: queueResponse.total_count
                    }
                };
            } catch (error) {
                console.error(`Error getting queue for device ${device.dev_eui}:`, error);
                return {
                    devEui: device.dev_eui,
                    name: device.name,
                    description: device.description,
                    lastSeen: device.last_seen_at ? 
                        new Date(device.last_seen_at.seconds * 1000).toISOString() : 'Never',
                    batteryLevel: device.device_status?.battery_level,
                    isActive: device.device_status?.external_power_source || 
                            (device.device_status?.battery_level > 0),
                    deviceProfile: device.device_profile_name,
                    signalStrength: device.device_status?.margin,
                    object: null,
                    queueStatus: {
                        error: 'Failed to fetch queue'
                    }
                };
            }
        }));

        res.json({
            success: true,
            count: response.total_count,
            devices
        });

    } catch (error) {
        console.error('Device controller error:', error);
        res.status(500).json({
            success: false,
            error: 'Failed to fetch devices',
            details: process.env.NODE_ENV === 'development' ? error.message : undefined
        });
    }
};

// Get detailed device information including queue items
exports.getDeviceDetails = async (req, res) => {
    try {
        const { devEui } = req.params;
        
        if (!devEui || !/^[A-F0-9]{16}$/i.test(devEui)) {
            return res.status(400).json({ error: 'Invalid DevEUI format' });
        }

        // Get device info
        const device = await chirpstack.devices.get(devEui);
        
        // Get activation info (if needed)
        const activation = await chirpstack.devices.getActivation(devEui).catch(() => null);
        
        // Get queue items
        const queue = await chirpstack.applications.deviceQueue.list(devEui);
        
        // Find the most recent queue item with an object
        const latestObjectItem = queue.result
            .filter(item => item.object)
            .sort((a, b) => 
                (b.expires_at?.seconds || 0) - (a.expires_at?.seconds || 0)
            )[0];

        res.json({
            success: true,
            device: {
                ...device,
                activation,
                object: latestObjectItem?.object || null,
                queueItems: queue.result,
                queueStatus: {
                    pendingCount: queue.result.filter(item => item.is_pending).length,
                    totalCount: queue.total_count
                }
            }
        });

    } catch (error) {
        if (error.code === 5) { // NOT_FOUND
            return res.status(404).json({ error: 'Device not found' });
        }
        res.status(500).json({ 
            success: false,
            error: 'Failed to fetch device details' 
        });
    }
};
  

// Get device details with full history
exports.getDeviceById = async (req, res) => {
    try {
        const { devEui } = req.params;
        
        if (!devEui) {
            return res.status(400).json({ error: 'DevEUI is required' });
        }
        
        // Get device info
        const device = await chirpstack.devices.get(devEui);
        
        // Get activation info
        const activation = await chirpstack.devices.getActivation(devEui);
        
        // Get queue items
        const queue = await chirpstack.deviceQueue.list({ dev_eui: devEui });
        
        // Get metrics
        const metrics = await chirpstack.devices.getMetrics({
            dev_eui: devEui,
            start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), // Last 7 days
            end: new Date(),
            aggregation: 'HOUR' // or 'DAY', 'MONTH'
        });

        res.json({
            success: true,
            result: {
                ...device,
                activation,
                queue: queue.result,
                metrics
            }
        });
    } catch (error) {
        console.error(`Error fetching device ${req.params.devEui}:`, error);
        
        if (error.code === 5) { // NOT_FOUND
            return res.status(404).json({ error: 'Device not found' });
        }
        
        res.status(500).json({ 
            success: false,
            error: error.message || 'Failed to fetch device' 
        });
    }
};

// Create a new device
exports.createDevice = async (req, res) => {
  try {
    const deviceData = req.body;
    
    // Validate required fields
    if (!deviceData.device || !deviceData.device.dev_eui || !deviceData.device.application_id) {
      return res.status(400).json({ error: 'Missing required device information' });
    }
    
    const response = await chirpstack.devices.create(deviceData);
    res.status(201).json(response);
  } catch (error) {
    console.error('Error creating device:', error);
    res.status(500).json({ error: error.message || 'Failed to create device' });
  }
};

// Update an existing device
exports.updateDevice = async (req, res) => {
  try {
    const { devEui } = req.params;
    const deviceData = req.body;
    
    // Ensure the device data contains the DevEUI from the URL
    if (!deviceData.device) {
      deviceData.device = {};
    }
    deviceData.device.dev_eui = devEui;
    
    const response = await chirpstack.devices.update(deviceData);
    res.json(response);
  } catch (error) {
    console.error(`Error updating device ${req.params.devEui}:`, error);
    res.status(500).json({ error: error.message || 'Failed to update device' });
  }
};

// Delete a device
exports.deleteDevice = async (req, res) => {
  try {
    const { devEui } = req.params;
    
    if (!devEui) {
      return res.status(400).json({ error: 'DevEUI is required' });
    }
    
    await chirpstack.devices.delete(devEui);
    res.status(204).send(); // No content response for successful deletion
  } catch (error) {
    console.error(`Error deleting device ${req.params.devEui}:`, error);
    res.status(500).json({ error: error.message || 'Failed to delete device' });
  }
}; 

when I run a test script in the terminal I get this back but no object ( sensor info)
Key Device Information:

  • Name: Kitchen Freezer Sensor
  • Description: None
  • Battery Level: 96.8499984741211
  • Last Seen: 2025-05-15T12:49:04.000Z

the info that’s missing is the sensor info and cant figure it out

Hard to just jump into this code without any context, especially when it’s so clearly GPT generated (no shade, I use it too), but I think I have a good enough idea.

Firstly, for your question:

If you want the actual sensor readings through gRPC, you must use a getDeviceMetrics request which your code does not implement.

While I think this will work fine for your purposes, it is the worst way to get sensor readings out of Chirpstack. Chirpstack only stores a certain amount of uplinks by default (I think it’s 10) then will delete them, it will also only store the uplinks for a certain timeframe. So using the API you can only get those most recent uplinks. You can change the number of stored uplinks in your Chirpstack.toml but it’s still not optimal. The preferred way is to use one of the integrations (like MQTT or SQL) which will send all of your data in realtime, then it’s up to you how you store and use it. For our portal we pull the readings via MQTT, store it in SQL then read it however / whenever we want. Although this is likely too large a scope for what you are doing and if you only need the most recent readings, the API should handle that.

Now onto my own questions / observations about to the code:

You have a (service?) called deviceQueue in your applications grouping for the API calls. Firstly this is not a real service in the Chirpstack API, there is no deviceQueue service. I am not entirely sure how your calls pull the clients but as far as I can tell these should all fail in error. Secondly, grouping deviceQueue under ‘applications’ and not ‘devices’ is psychotic to me. If these are the calls you thought would pull the sensor readings then that would be your issue, they are being run on a service that does not exist, they should instead be run on the device service and the call for pulling the sensor readings should be a getMetrics() call.

fully understand that, GPT, deep seek and claude, I need all the help I can get. since I am a one man team. no shade taken i am not a coder by trade.

So in the device.proto file I saw the getMetrics part and then i also saw the getQueue

  // GetMetrics returns the device metrics.
  // Note that this requires a device-profile with codec and measurements
  // configured.
  rpc GetMetrics(GetDeviceMetricsRequest) returns (GetDeviceMetricsResponse) {
    option (google.api.http) = {
      get : "/api/devices/{dev_eui}/metrics"
    };
  }
  // GetQueue returns the downlink device-queue.
  rpc GetQueue(GetDeviceQueueItemsRequest)
      returns (GetDeviceQueueItemsResponse) {
    option (google.api.http) = {
      get : "/api/devices/{dev_eui}/queue"
    };
  }

I am thinking of finding out how to save the data to a SQL so that there will be a way to view histories.

thanks for pointing that out will look into that observation of yours about deviceQueue.

I have gone through my deviceController.js and the gRPC api service and now when I run my test-script.js i get this

Found 5 devices

Testing metrics for device: IO Controller Test Generator (24e124445d354713)
Metrics for IO Controller Test Generator:
State Metrics:
  gpio_out_2: off ()
  gpio_out_1: off ()

Time Series Metrics:

Testing metrics for device: Kitchen Freezer Sensor (24e124136d319204)
Metrics for Kitchen Freezer Sensor:
State Metrics:
  magnet_status: closed ()

Time Series Metrics:
  battery:
    battery: No data points available
  humidity:
    humidity: No data points available
  temperature:
    temperature: No data points available

Testing metrics for device: Kitchen Movement Sensor (24e124538c195163)
Metrics for Kitchen Movement Sensor:
State Metrics:
  daylight: light ()
  pir: movement detected ()

now I just need to figure out how to get the values that are saying no data points available. Then once I have all the data that I need, I have to figure out how I am going to make it visible on the front end.

Sounds like you’re a full modern day dev team then :wink:

Yes, getMetrics and getQueue are calls, but they are calls for the deviceService. Let’s walk through it. Your code generates a client for each service listed in your gRPC API service:

const loadProtoDescriptors = () => {
  // List of services you want to use from ChirpStack
  const services = [
    'device',
    'device_profile',
    'gateway',
    'application',
    'user',
    // Add more services as needed
  ];

Then you use the appropriate call related to a client with clients.<client.type>.<api.call>. For example:

clients.device.List()

Calls the list method of the device client, which is the client using the deviceService. All good there, it will return a list of all your devices.

But then if you look at your methods for deviceQueue. You call them as:
clients.deviceQueue.Enqueue() and clients.deviceQueue.List()

This is attempting to pull deviceQueue from your clients list, but deviceQueue is not in your clients list, it’s not even a chirpstack client to begin with. If you want to pull the downlink queue, or enqueue to it, the calls should be using clients.device.Enqueue().

But again this is the queue for downlinks, not for your history of uplinks. What you really want to add is client.device.getMetrics() to your gRPC API service and then use that in your devicecontroller.js.

If you want to go down this path, just use Chirpstack’s SQL integration. That will store all uplinks into an SQL table for you to use how you want. You will however be responsible for cleanup and parsing from that point so it would add some complexity for sure.

Not sure what the difference between state metrics and time series metrics are here, are these all retrieved through getMetrics and it’s just your device codec that splits them up so? Not sure how to help without seeing your updated code and test function.