@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