Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
98bec22
feat: refactor application structure to modularize components and imp…
Emmo00 Jan 8, 2026
cb0e5c1
feat: remove deprecated logic files and move grpc and mqtt services
Emmo00 Jan 8, 2026
da21690
feat: add modular decode and encode functions, initialize verifiers c…
Emmo00 Jan 8, 2026
04047bc
docs: add complete hook lifecycle documentation for M3tering Console
Emmo00 Jan 8, 2026
70cdaab
refactor: modularize Streamr client initialization, destroy client in…
Emmo00 Jan 8, 2026
684a5aa
refactor: update getCachedVerifiers to return a promise and adjust re…
Emmo00 Jan 8, 2026
337750a
refactor: add node-cron for scheduled transaction publishing and modu…
Emmo00 Jan 16, 2026
569fc8a
refactor: update .gitignore to include console configuration file
Emmo00 Jan 16, 2026
be73e48
refactor: remove STREAMR_STREAM_ID from .env.example and README, dele…
Emmo00 Jan 16, 2026
606f4cc
refactor: modularize Streamr transaction handling and configuration l…
Emmo00 Jan 16, 2026
dc166c9
refactor: add prune_sync module and update configurations in example …
Emmo00 Jan 16, 2026
51c247a
refactor: implement prune_sync module for scheduled transaction pruni…
Emmo00 Jan 16, 2026
0600852
refactor: remove unused BatchTransactionPayload import and update cro…
Emmo00 Jan 16, 2026
a1ddba4
refactor: enhance MQTT initialization error handling and improve Stre…
Emmo00 Jan 16, 2026
80f2a9b
refactor: add logging for cron job registration in prune_sync and Str…
Emmo00 Jan 16, 2026
ff9ed94
refactor: simplify error logging in loadConfigurations function
Emmo00 Jan 16, 2026
fac32fc
refactor: remove unused nonce synchronization phase and improve MQTT …
Emmo00 Jan 16, 2026
d0192f9
refactor: add check for prover URL before sending pending transactions
Emmo00 Jan 16, 2026
4d826c3
refactor: remove unnecessary whitespace and logging of request payloa…
Emmo00 Jan 16, 2026
9d72834
refactor: implement UI extension system and integrate dynamic module …
Emmo00 Jan 19, 2026
61ed826
refactor: update README for clarity and enhance extension system docu…
Emmo00 Jan 19, 2026
6d26c62
refactor: enable MQTT error throwing during application initialization
Emmo00 Jan 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,4 @@ CONTRACT_LABEL="M3ters"
CHIRPSTACK_HOST="localhost"
MAINNET_RPC="https://sepolia.drpc.org"
PREFERRED_PROVER_NODE="http://prover.m3ter.ing"
STREAMR_STREAM_ID="0x567853282663b601bfdb9203819b1fbb3fe18926/m3tering/test"
ETHEREUM_PRIVATE_KEY="..."
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# console configuration file
console.config.json

#test scripts
emulate.ts
test-database.ts
Expand Down
10 changes: 10 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"cSpell.words": [
"ardrive",
"arweave",
"m3ters",
"Emmo00",
"ccip",
"Mauchly",
]
}
267 changes: 264 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# M3tering Console Setup
# 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 evergy as been set on the PriceContext 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
Expand All @@ -27,7 +29,6 @@
CHIRPSTACK_HOST=localhost
MAINNET_RPC=https://sepolia.drpc.org
PREFERRED_PROVER_NODE=http://34.244.149.153
STREAMR_STREAM_ID="0x567853282663b601bfdb9203819b1fbb3fe18926/m3tering/test"
ETHEREUM_PRIVATE_KEY="..."
```

Expand All @@ -54,3 +55,263 @@
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 a config-driven approach where modules are loaded dynamically from paths specified in `console.config.json`.

## Configuration

```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 to `src/lib/`)
- **`uiModules`**: Object mapping module IDs to UI module paths (relative to `src/lib/`)

---

# 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

```typescript
// 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`:
```json
{
"modules": ["core/my-module"]
}
```

## Hook Lifecycle Reference

### Initialization Phase

| 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` |

### MQTT Connection Phase

| 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` |

### Message Processing Phase

| 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[]` |

### State Computation Phase

| 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` |

### Error & Cleanup Phase

| 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

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

```typescript
// 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`:
```json
{
"uiModules": {
"my-module": "core/my-module/ui"
}
}
```

## UIHooks Interface

| 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) |

## Type Definitions

### UIAppIcon

```typescript
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")
}
```

### UIAppWindow

```typescript
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
}
```

### UIAction

```typescript
interface UIAction {
id: string; // Action identifier
label: string; // Button label
buttonClass?: string; // Optional button class
handler: () => void | Promise<{ message?: string; data?: any }>;
}
```

## Frontend API

### Invoking Actions

From your panel HTML, use the global `invokeAction()` function:

```javascript
// 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

| 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 |

## UI Modules

| Module | Description |
|--------|-------------|
| `streamr` | Panel showing stream config, pending count, and "Publish Now" action |

---
21 changes: 21 additions & 0 deletions console.example.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"modules": [
"core/arweave",
"core/prover",
"core/streamr",
"core/is_on",
"core/prune_sync"
],
"uiModules": {
"streamr": "core/streamr/ui"
},
"streamr": {
"streamId": [
"0x567853282663b601bfdb9203819b1fbb3fe18926/m3tering/test"
],
"cronSchedule": "0 * * * *"
},
"prune_sync": {
"cronSchedule": "0 * * * *"
}
}
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"express-handlebars": "^7.1.2",
"handlebars": "^4.7.8",
"mqtt": "^5.5.0",
"node-cron": "^4.2.1",
"ssh2": "^1.17.0",
"ws": "^8.18.3"
},
Expand Down
Loading