Skip to content

Repository files navigation

M3tering Console

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.

Pre-setup

  • 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

Quick Setup

  1. Clone/Update

    git clone https://github.com/M3tering/Console.git
    # orcd Console/ && git pull
  2. Environment Variables Create .env file:

    # Server Configuration
    PORT=3000
    # Module Configuration
    BACKEND_MODULES="core/arweave,core/is_on,core/prover,core/streamr"
    UI_MODULES="streamr:core/streamr/ui"
    # ChirpStack Configuration
    API_TOKEN=...
    APPLICATION_ID=...
    CHIRPSTACK_HOST=localhost
    # Contract & Network Configuration
    CONTRACT_LABEL=M3ters
    MAINNET_RPC=https://sepolia.drpc.org
    ETHEREUM_PRIVATE_KEY="..."
    # Streamr Configuration
    STREAMR_STREAM_ID="0x567853282663b601bfdb9203819b1fbb3fe18926/m3tering/test"
    STREAMR_CRONSCHEDULE="0 * * * *" # Every hour
    # Optional: Prover Node (defaults to automatic selection)
    # PREFERRED_PROVER_NODE="https://prover.m3ter.ing"
    # PROVER_CRONSCHEDULE="0 0 * * *" # Every 24 hours
    
  3. Docker Build and Run

    sudo docker compose down
    sudo docker compose up -d

Other Docker Commands

  • sudo docker ps - list running containers
  • sudo docker ps -a - list all containers
  • sudo docker compose down - stop container
  • sudo docker compose logs - view logs
  • sudo docker compose logs -tf - follow logs with timestamps
  • sudo docker system prune - cleanup unused containers/images
  • sudo docker system prune -a - cleanup everything unused

Development

npm install
npm run dev

Extension System

The M3tering Console provides two complementary extension systems:

  1. Backend Hooks - Hook into the console lifecycle (MQTT, database, message processing)
  2. UI Hooks - Add custom icons, panels, and actions to the web interface

Both systems use an environment-driven approach where modules are loaded dynamically from paths specified in your .env file.

Configuration

Modules are configured via environment variables. Modules are automatically pulled from GitHub repositories when the container starts up.

# Backend Modules (comma-separated GitHub repositories)
BACKEND_MODULES="core/arweave,core/is_on,core/prover,core/streamr,username/my-custom-module"# UI Modules (colon-separated format: moduleId:github_repo)
UI_MODULES="streamr:core/streamr/ui,my-module:username/my-custom-module"# Module-specific configuration
STREAMR_STREAM_ID="0x567853282663b601bfdb9203819b1fbb3fe18926/m3tering/test"
STREAMR_CRONSCHEDULE="0 * * * *"# Every hour
  • BACKEND_MODULES: Comma-separated list of GitHub repositories in the format <github_username>/<repo_name> or built-in paths like core/arweave
  • UI_MODULES: Comma-separated list of moduleId:<github_username>/<repo_name> pairs
  • Module-specific variables: Each module can have its own configuration variables (e.g., STREAMR_STREAM_ID)

Publishing Custom Modules

To use your own extensions:

  1. Publish your module code to a GitHub repository
  2. Reference it in your .env file using the format <github_username>/<repo_name>
  3. The extension code is automatically cloned from GitHub when the Docker container starts up
  4. For specific versions, append #<tag> or #<branch> (e.g., username/my-module#v1.0.0)

Backend Hooks

Backend hooks allow modules to react to console lifecycle events. Each module exports a default class implementing the Hooks interface.

Creating a Backend Module

  1. Create your module repository with the following structure:
// index.tsimporttype{Hooks}from"../../../types";exportdefaultclassimplementsHooks{onAfterInit(){console.log("My module initialized!");}onTransactionDistribution(tokenId,decodedPayload,pendingTransactions){// Process transactions}}
  1. Publish to GitHub: Push your module code to a GitHub repository (e.g., github.com/yourusername/my-m3tering-module)

  2. Configure in .env:

BACKEND_MODULES="core/arweave,core/prover,yourusername/my-m3tering-module"
  1. Restart container: The module will be automatically cloned from GitHub and loaded when the container starts

Hook Lifecycle Reference

Initialization Phase

HookDescriptionParameters
onBeforeInitBefore any initialization beginsNone
onDatabaseSetupAfter SQLite tables/jobs are initializedNone
onAfterInitAfter all initialization completes successfullyNone
onInitErrorWhen an error occurs during initializationerror: any

MQTT Connection Phase

HookDescriptionParameters
onMqttConnectMQTT client successfully connects to ChirpStackclient: MqttClient
onMqttSubscribedAfter subscribing to the device uplink topicclient: MqttClient, topic: string
onMqttErrorMQTT connection error occurserror: any, client: MqttClient
onMqttReconnectAttempting to reconnect to MQTT brokerclient: MqttClient

Message Processing Phase

HookDescriptionParameters
onMessageReceivedRaw MQTT message received (before parsing)blob: Buffer
onMessageDroppedMessage dropped (e.g., device locked)reason: string, devEui: string
onMeterCreatedNew meter saved to databasenewMeter: MeterRecord
onTransactionDistributionBefore sending to Arweave/prover/StreamrtokenId: number, decodedPayload: DecodedPayload, pendingTransactions: TransactionRecord[]

State Computation Phase

HookDescriptionParameters
isOnStateComputeDetermine device on/off state (returns boolean)m3terId: number
onIsOnStateComputedAfter on/off state computedm3terId: number, isOn: boolean
onIsOnStateComputeErrorError during state computationm3terId: number, error: any
onStateEnqueuedState enqueued to gRPC for device responsestate: any, latitude: number, longitude: number

Error & Cleanup Phase

HookDescriptionParameters
onMessageErrorError during message processingerror: any
onDeviceUnlockedDevice lock released (regardless of outcome)devEui: string
onMessageProcessingCompleteMessage processing finishedNone

UI Hooks

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.

Creating a UI Module

  1. Create your module repository with the following structure:
// ui.ts (or index.ts)importtype{UIHooks,UIAppIcon,UIAppWindow,UIAction}from"../../../types";exportdefaultclassimplementsUIHooks{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 actionreturn{message: "Action completed!"};},},];}}
  1. Publish to GitHub: Push your module code to a GitHub repository (e.g., github.com/yourusername/my-ui-module)

  2. Configure in .env:

UI_MODULES="streamr:core/streamr/ui,my-module:yourusername/my-ui-module"
  1. Restart container: The module will be automatically cloned from GitHub and loaded when the container starts

UIHooks Interface

MethodReturn TypeDescription
getAppIcon()UIAppIconDesktop icon displayed in the app grid
getAppWindow()UIAppWindowWindow/panel shown when icon is clicked
getActions()UIAction[]Actions invokable from the frontend
getStatusData()Record<string, any>Metadata for display (optional)

Type Definitions

UIAppIcon

interfaceUIAppIcon{id: string;// Unique identifierlabel: string;// Display label below iconiconHtml: string;// HTML content (supports NES.css icons)buttonClass?: string;// Optional button class (e.g., "is-primary")}

UIAppWindow

interfaceUIAppWindow{id: string;// Must match icon idtitle: string;// Window title bar textcontentHtml: string;// HTML content for window bodycontainerClass?: string;// Optional container class}

UIAction

interfaceUIAction{id: string;// Action identifierlabel: string;// Button labelbuttonClass?: string;// Optional button classhandler: ()=>void|Promise<{message?: string;data?: any}>;}

Frontend API

Invoking Actions

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

REST Endpoint

POST /api/actions/:moduleId/:actionId
Response: { success: boolean, message?: string, data?: any }

Built-in Modules

Backend Modules

ModuleDescription
core/arweaveUploads transaction data to Arweave permanent storage
core/proverSends batched transactions to the prover node
core/streamrPublishes transactions to Streamr streams on a cron schedule
core/is_onComputes device on/off state based on balance

UI Modules

ModuleDescription
streamrPanel showing stream config, pending count, and "Publish Now" action

Guides

Extracting Transaction data from running console docker container

1) Find the running container name/ID

docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}"

Look for ghcr.io/m3tering/console:main.

2) Copy the SQLite file OUT of the container (backup)

docker cp <container_name_or_id>:/opt/app/.data/m3tering.db ./m3tering.db

Now you must have the sqlite database in the directory you are working from. if you want to get the transactions in json or csv format, go ahead with the following steps

3) Extract in readable format

Option 1: Use sqlite3 + .mode json (fastest)

If you have sqlite3 installed:

  1. Export transactions table to JSON:
sqlite3 m3tering.db ".mode json"".once transactions.json""SELECT * FROM transactions;"

Option 2: Export to CSV then convert to JSON (works everywhere)

Export a table to CSV:

sqlite3 m3tering.db ".headers on"".mode csv"".once transactions.csv""SELECT * FROM transactions;"

Option 3: Export the whole DB (all tables) into one JSON file (best)

This produces a single JSON like:

{ "table1": [...], "table2": [...] }
python3 - <<'PY'import sqlite3, jsondb_path = "m3tering.db"out_path = "db_export.json"con = sqlite3.connect(db_path)con.row_factory = sqlite3.Rowcur = con.cursor()tables = [r[0] for r in cur.execute(""" SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name""")]export = {}for t in tables: rows = cur.execute(f'SELECT * FROM "{t}"').fetchall() export[t] = [dict(r) for r in rows]with open(out_path, "w", encoding="utf-8") as f: json.dump(export, f, indent=2, ensure_ascii=False)print(f"Exported {len(tables)} tables to {out_path}")PY

Updating code for Running Console Code (Docker)

“updating code” means pulling a newer image tag (or switching to a build: config) or updating the console's environment variables.


A) Update only .env (no image change)

  1. Edit .env in the Console directory.

  2. Recreate the container so it reloads env_file:

docker compose up -d --force-recreate console
  1. Confirm env actually changed:
docker compose exec console sh -lc 'env | sort | head'

B) Update the app “code” (i.e., get latest ghcr.io/m3tering/console:main)

Because you’re using an image, you update by pulling and recreating.

  1. Pull the latest image:
docker compose pull console
  1. Recreate using the new image:
docker compose up -d --force-recreate console
  1. Verify the container is on the new image:
docker compose images
docker compose ps
docker image ls | grep ghcr.io/m3tering/console

Optional (clean up old images):

docker image prune -f

C) If you changed BOTH .env and want latest image

Do it in one go:

docker compose pull console
docker compose up -d --force-recreate console

About

M3tering Console

Resources

Stars

0 stars

Watchers

1 watching

Forks

Packages

Used by

Contributors

Languages