This Repository contains the code used to build my MeterThing website. It includes both the backend written in python using FastAPI and the frontend built with svelte. This README is used to explain how everything fits together and how to run the project.
This tutorial will guide you through the process of setting up your API credentials file for ChirpStack, which is necessary for interacting with the ChirpStack API programmatically.
- Access to a ChirpStack server instance
- Administrative permissions to create API keys
- Basic knowledge of JSON file format
- Must have Node JS version 22.14.0 or higher installed
- Log in to your ChirpStack web interface
- Navigate to API Keys section (usually found under your user profile or in the administration panel)
- Click on Add API Key
- Enter a name for your API key (e.g., "My Application Integration")
- Select the appropriate access rights for your needs
- Click Create API Key
- Important: Copy the generated API token immediately, as it will only be shown once
- Create a new file named
chirpstack_config.jsonin your project directory - Open the file in a text editor
- Add the following JSON structure:
{
"server_address": "[SERVER_IP_OR_HOSTNAME]:[PORT]",
"api_token": "[YOUR_API_TOKEN]",
"tenant_id": "[YOUR_TENANT_ID]",
"application_id": "[YOUR_APPLICATION_ID]",
"device_eui": "[YOUR_DEVICE_EUI]"
}Replace the placeholders with your actual values:
- server_address: The IP address or hostname of your ChirpStack server, including the port number
- api_token: The API token generated in Step 1
- tenant_id: Your tenant ID (found in the ChirpStack web interface under Tenants)
- application_id: The ID of the application you want to work with (found under Applications)
- device_eui: The EUI of your LoRaWAN device
cd backend
pip install -r requirements.txt
pip install fastapi uvicorn
cd backend
uvicorn app:app --reload --port 8000
cd meterthing
npm run dev
I'll help you understand how your code works by breaking down the flow from backend to frontend. Let me explain the entire system architecture and how data moves through it.
The backend system starts with two main Python files: app.py and chirpstack_client.py. The app.py file creates a FastAPI endpoint that serves as a bridge between your frontend and the ChirpStack server.
When a request comes to /api/devices/list, your FastAPI server:
- Loads configuration from
chirpstack_config.json - Creates a ChirpStack client instance
- Retrieves devices using gRPC communication
- Transforms the device data into a simplified format
- Returns this data as a JSON response
The ChirpStackClient class in chirpstack_client.py handles the low-level communication with the ChirpStack server using gRPC. It uses your API token and server address to authenticate and retrieve device information.
Your frontend uses Svelte's store system to manage device data. Here's how the data flows:
- The
deviceStore.tsfile creates a custom store using Svelte'swritablestore:
function createDeviceStore() {
const store = writable<DeviceTypes>({});
// ... initialization logic
return {
subscribe: store.subscribe,
initialize
};
}- The store's
initializefunction fetches data from your backend API and organizes it by device type:
const deviceTypes = data.result.reduce((acc: DeviceTypes, device: Device) => {
const type = device.device_profile_name;
if (!acc[type]) {
acc[type] = {
label: type,
devices: [],
selected: ''
};
}
acc[type].devices.push(device.name);
return acc;
}, {});- Your Svelte component connects to this store using:
import { deviceTypes } from "../../stores/deviceStore";Your Svelte component uses reactive statements and the store to render a dynamic form:
onMount(async () => {
await deviceTypes.initialize();
});When the component mounts, it initializes the store, which triggers the API call. The $ prefix in $deviceTypes is Svelte's auto-subscription syntax - it automatically handles subscribing and unsubscribing to the store.
The template section creates a form that:
- Checks if device types exist (
{#if $deviceTypes}) - Iterates through each device type (
{#each Object.entries($deviceTypes) as [type, config]}) - Creates a select dropdown for each type, populated with that type's devices
- Uses two-way binding with
bind:valueto keep the UI and store in sync
Your vite.config.ts sets up a development proxy that forwards API requests to your FastAPI backend:
proxy: {
'/api': {
target: 'http://127.0.0.1:8000',
changeOrigin: true
}
}This means when your frontend makes a request to /api/devices/list, it actually goes to http://127.0.0.1:8000/api/devices/list during development.
- User loads the page
- Component mounts and calls
deviceTypes.initialize() - Initialize function makes an API request to
/api/devices/list - Request is proxied to your FastAPI backend
- Backend creates ChirpStack client and retrieves devices
- Data flows back to frontend
- Store processes and organizes the data
- Svelte reactively updates the UI with the new data
- User sees dropdowns populated with their devices, organized by type
This architecture creates a clean separation of concerns while maintaining reactive data flow throughout your application.
When building modern web applications, particularly those involving IoT devices and real-time data, one of the most important concepts to understand is asynchronous programming using async/await. This guide will explain what these keywords mean, why they're essential, and how they work in practice.
To understand async/await, imagine you're at a busy restaurant. When you order food, the waiter follows an efficient process:
- Takes your order
- Submits it to the kitchen
- Serves other tables while your food is being prepared
- Returns when the food is ready
This is exactly how async/await works in programming. It allows your code to handle other tasks while waiting for slow operations (like getting data from a server) to complete. Without this capability, your application would be like a waiter who can only serve one table at a time, standing idle while waiting for each order to be prepared.
Let's look at a real-world example from a Svelte component that loads IoT device data:
onMount(async () => {
// First action when the component loads
console.log('Component mounted');
// Wait for device data to load
await deviceTypes.initialize();
// Once data is loaded, we can use it
console.log('Store initialized:', $deviceTypes);
});In this code, the async keyword tells JavaScript that this function will perform operations that take time to complete. The await keyword marks specific points where we need to wait for something to finish before moving on.
Here's how async operations work when fetching data from an API:
const initialize = async () => {
try {
// Wait for the API response
const response = await fetch('/api/devices/list');
// Wait for the JSON data to be extracted
const data = await response.json();
// Update the application with the new data
store.set(deviceTypes);
} catch (error) {
console.error('Failed to fetch devices:', error);
}
};Each await keyword marks a point where the code needs to pause and wait for an operation to complete. However, this pause doesn't block the rest of your application - other code can continue running, just like our waiter can serve other tables while waiting for one order to be prepared.
The concept of async operations extends to backend development as well. Here's an example using FastAPI:
@app.get("/api/devices/list")
async def list_devices():
try:
# Create a client to communicate with IoT devices
client = ChirpStackClient(config['server_address'],
config['api_token'],
config['application_id'])
# Get the device data
devices = client.get_devices()
# Process the data into a useful format
device_list = []
for device in devices.result:
device_list.append({
"name": device.name,
"device_profile_name": device.device_profile_name,
"dev_eui": device.dev_eui
})
return {"result": device_list}
except Exception as e:
print(f"Error: {str(e)}")
raiseAsync programming is crucial in modern web development for several reasons:
-
Responsiveness: Your application stays interactive even while waiting for slow operations to complete.
-
Efficiency: Multiple operations can happen simultaneously, like a waiter handling multiple tables.
-
Better User Experience: Users don't experience freezes or delays while waiting for data to load.
-
Resource Management: Your application can make better use of system resources by not blocking while waiting for operations to complete.
This is particularly important in IoT applications where you're dealing with:
- Network communications that may have varying response times
- Multiple devices sending data simultaneously
- Real-time data updates that shouldn't freeze the interface
- Complex operations that take time to complete
When working with async/await, keep these guidelines in mind:
- Always wrap async operations in try/catch blocks to handle errors gracefully
- Use async/await consistently throughout your application
- Remember that async functions always return a Promise
- Consider the user experience when designing async operations
- Use loading indicators to show users when operations are in progress
Understanding async/await is fundamental to building modern web applications, especially those involving IoT devices and real-time data. By allowing your code to handle multiple operations efficiently without blocking, async/await enables you to create responsive, user-friendly applications that can handle complex operations smoothly.
Remember: just as a good restaurant needs efficient waiters who can handle multiple tables simultaneously, a good web application needs efficient async operations to handle multiple tasks concurrently. This approach ensures your application stays responsive and provides a smooth user experience, even when dealing with complex device communications and data processing.