A modular, extensible service console for providers on the M3tering protocol. Features a hook-based architecture for backend extensibility and a UI extension system for frontend customization.
- Make sure Public key is set on the M3ter contract
- Make sure the price for energy has been set on the PriceContext contract
- Make sure the Console has been granted publish permission on the Streamr stream
-
Clone/Update
git clone https://github.com/M3tering/Console.git # or cd Console/ && git pull
-
Environment Variables Create
.envfile:PORT=3000 API_TOKEN=... APPLICATION_ID=... CONTRACT_LABEL=M3ters CHIRPSTACK_HOST=localhost MAINNET_RPC=https://sepolia.drpc.org PREFERRED_PROVER_NODE=http://34.244.149.153 ETHEREUM_PRIVATE_KEY="..." -
Docker Build and Run
sudo docker compose down sudo docker compose up -d
sudo docker ps- list running containerssudo docker ps -a- list all containerssudo docker compose down- stop containersudo docker compose logs- view logssudo docker compose logs -tf- follow logs with timestampssudo docker system prune- cleanup unused containers/imagessudo docker system prune -a- cleanup everything unused
npm install
npm run devThe M3tering Console provides two complementary extension systems:
- Backend Hooks - Hook into the console lifecycle (MQTT, database, message processing)
- UI Hooks - Add custom icons, panels, and actions to the web interface
Both systems use a config-driven approach where modules are loaded dynamically from paths specified in console.config.json.
{
"modules": [
"core/arweave",
"core/prover",
"core/streamr",
"core/is_on",
"core/prune_sync"
],
"uiModules": {
"streamr": "core/streamr/ui"
},
"streamr": {
"streamId": ["0x.../m3tering/test"],
"cronSchedule": "0 * * * *"
}
}modules: Array of paths to backend hook modules (relative tosrc/lib/)uiModules: Object mapping module IDs to UI module paths (relative tosrc/lib/)
Backend hooks allow modules to react to console lifecycle events. Each module exports a default class implementing the Hooks interface.
// src/lib/core/my-module/index.ts
import type { Hooks } from "../../../types";
export default class implements Hooks {
onAfterInit() {
console.log("My module initialized!");
}
onTransactionDistribution(tokenId, decodedPayload, pendingTransactions) {
// Process transactions
}
}Add to console.config.json:
{
"modules": ["core/my-module"]
}| Hook | Description | Parameters |
|---|---|---|
onBeforeInit |
Before any initialization begins | None |
onDatabaseSetup |
After SQLite tables/jobs are initialized | None |
onAfterInit |
After all initialization completes successfully | None |
onInitError |
When an error occurs during initialization | error: any |
| Hook | Description | Parameters |
|---|---|---|
onMqttConnect |
MQTT client successfully connects to ChirpStack | client: MqttClient |
onMqttSubscribed |
After subscribing to the device uplink topic | client: MqttClient, topic: string |
onMqttError |
MQTT connection error occurs | error: any, client: MqttClient |
onMqttReconnect |
Attempting to reconnect to MQTT broker | client: MqttClient |
| Hook | Description | Parameters |
|---|---|---|
onMessageReceived |
Raw MQTT message received (before parsing) | blob: Buffer |
onMessageDropped |
Message dropped (e.g., device locked) | reason: string, devEui: string |
onMeterCreated |
New meter saved to database | newMeter: MeterRecord |
onTransactionDistribution |
Before sending to Arweave/prover/Streamr | tokenId: number, decodedPayload: DecodedPayload, pendingTransactions: TransactionRecord[] |
| Hook | Description | Parameters |
|---|---|---|
isOnStateCompute |
Determine device on/off state (returns boolean) |
m3terId: number |
onIsOnStateComputed |
After on/off state computed | m3terId: number, isOn: boolean |
onIsOnStateComputeError |
Error during state computation | m3terId: number, error: any |
onStateEnqueued |
State enqueued to gRPC for device response | state: any, latitude: number, longitude: number |
| Hook | Description | Parameters |
|---|---|---|
onMessageError |
Error during message processing | error: any |
onDeviceUnlocked |
Device lock released (regardless of outcome) | devEui: string |
onMessageProcessingComplete |
Message processing finished | None |
UI Hooks allow modules to extend the web interface at http://localhost:3000. Modules can add desktop icons, app windows/panels, and trigger-able actions.
// src/lib/core/my-module/ui.ts
import type { UIHooks, UIAppIcon, UIAppWindow, UIAction } from "../../../types";
export default class implements UIHooks {
getAppIcon(): UIAppIcon {
return {
id: "my-module",
label: "My Module",
iconHtml: '<i class="nes-icon heart is-medium"></i>',
buttonClass: "is-primary",
};
}
getAppWindow(): UIAppWindow {
return {
id: "my-module",
title: "My Module Panel",
contentHtml: `
<p>Hello from my module!</p>
<button class="nes-btn is-success" onclick="invokeAction('my-module', 'do-something', this)">
Do Something
</button>
`,
};
}
getActions(): UIAction[] {
return [
{
id: "do-something",
label: "Do Something",
handler: async () => {
// Perform action
return { message: "Action completed!" };
},
},
];
}
}Add to console.config.json:
{
"uiModules": {
"my-module": "core/my-module/ui"
}
}| Method | Return Type | Description |
|---|---|---|
getAppIcon() |
UIAppIcon |
Desktop icon displayed in the app grid |
getAppWindow() |
UIAppWindow |
Window/panel shown when icon is clicked |
getActions() |
UIAction[] |
Actions invokable from the frontend |
getStatusData() |
Record<string, any> |
Metadata for display (optional) |
interface UIAppIcon {
id: string; // Unique identifier
label: string; // Display label below icon
iconHtml: string; // HTML content (supports NES.css icons)
buttonClass?: string; // Optional button class (e.g., "is-primary")
}interface UIAppWindow {
id: string; // Must match icon id
title: string; // Window title bar text
contentHtml: string; // HTML content for window body
containerClass?: string; // Optional container class
}interface UIAction {
id: string; // Action identifier
label: string; // Button label
buttonClass?: string; // Optional button class
handler: () => void | Promise<{ message?: string; data?: any }>;
}From your panel HTML, use the global invokeAction() function:
// invokeAction(moduleId, actionId, buttonElement?)
invokeAction('my-module', 'do-something', this);The function:
- Shows loading state on the button (if provided)
- Calls
POST /api/actions/:moduleId/:actionId - Displays success/error notification using NES.css styling
POST /api/actions/:moduleId/:actionId
Response: { success: boolean, message?: string, data?: any }
| Module | Description |
|---|---|
core/arweave |
Uploads transaction data to Arweave permanent storage |
core/prover |
Sends batched transactions to the prover node |
core/streamr |
Publishes transactions to Streamr streams on a cron schedule |
core/is_on |
Computes device on/off state based on balance |
core/prune_sync |
Cleans up old synchronized transactions |
| Module | Description |
|---|---|
streamr |
Panel showing stream config, pending count, and "Publish Now" action |