IoT Library for the Firmngin Platform. Enables ESP8266/ESP32 devices to accept payments, receive commands, and communicate securely with mTLS support.
Check out firmngin.dev for more information.
- ESP8266 & ESP32 support
- Event-driven API using
on()callbacks: raw or typed - Typed objects for common flows:
Verifications,Payments,Usages,DeviceStates,Inits,EntityCommand - mTLS authentication via
keys.h(optional but recommended)
Add to your platformio.ini:
lib_deps =
bblanchon/ArduinoJson @ ^7.4.3
knolleary/PubSubClient @ ^2.8.0We use these excellent 3rd party libraries to power Firmngin Arduino Library. Make sure they are installed in your project.
Copy src/firmngin.h and src/firmngin.cpp to your project.
- Install dependencies via Library Manager:
- ArduinoJson by Benoit Blanchon
- PubSubClient by Nick O'Leary
- Install the library, then add your optional
keys.hfile next to your sketch if you use mTLS.
#include <Arduino.h>
#include <firmngin.h>
#if defined(ESP8266)
#include <ESP8266WiFi.h>
#elif defined(ESP32)
#include <WiFi.h>
#endif
#define DEVICE_ID "YOUR_DEVICE_ID"
#define DEVICE_KEY "YOUR_DEVICE_SECRET_KEY"
const char *ssid = "YOUR_SSID";
const char *password = "YOUR_PASSWORD";
Firmngin fngin(DEVICE_ID, DEVICE_KEY);
Entity relay1("gpio_1");
Entity temperature("temperature");
ON_ENTITY(relay1, [](EntityCommand &cmd) {
digitalWrite(2, cmd.value() == "1" ? HIGH : LOW);
});
ON_ENTITY_S("status", [](EntityCommand &cmd) {
Serial.println(cmd.value());
});
void setup() {
Serial.begin(115200);
// Connect to WiFi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi connected");
pinMode(2, OUTPUT);
fngin.setDebug(true);
fngin.begin();
}
void loop() {
fngin.loop();
// Push entity state every 10 seconds
static unsigned long lastPush = 0;
if (millis() - lastPush > 10000) {
lastPush = millis();
fngin.pushEntity(temperature, "25.5");
}
}Create a keys.h file next to your sketch (do not commit this file).
You can download your device's keys.h directly from the Firmngin Dashboard → Devices → your device → Download keys.h.
Choose how the device validates the MQTT broker's identity:
| Mode | Define | Best For | Pros | Cons |
|---|---|---|---|---|
| CA Certificate | USE_CA_CERT |
ESP32, Production | Full chain validation, auto-renew safe | Uses more memory |
| Fingerprint | USE_FINGERPRINT |
ESP8266, Prototypes | Lightweight, memory efficient | Must update if server cert changes |
If you want the server to renew freely without touching the client, use USE_CA_CERT and keep the Root CA stable. Only fingerprint mode requires client regeneration when the server certificate changes.
#ifndef KEYS_H
#define KEYS_H
// Choose ONE validation mode:
// #define USE_CA_CERT // Recommended for ESP32
// #define USE_FINGERPRINT // Recommended for ESP8266
// Optional. If omitted, the library uses asia-jkt1.firmngin.dev:8883.
// #define FIRMNGIN_SERVER_ADDR "asia-jkt1.firmngin.dev"
// #define FIRMNGIN_SERVER_PORT 8883
static const char CA_CERT[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
[Your CA Certificate]
-----END CERTIFICATE-----
)EOF";
static const char CLIENT_CERT[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
[Your Client Certificate]
-----END CERTIFICATE-----
)EOF";
static const char PRIVATE_KEY[] PROGMEM = R"EOF(
-----BEGIN PRIVATE KEY-----
[Your Private Key]
-----END PRIVATE KEY-----
)EOF";
static const uint8_t SERVER_FINGERPRINT_BYTES[20] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
#endifA template is available in src/keys.h.template.
Register handlers using the enum constants. Use on(ENUM, callback) for raw callbacks or typed objects for automatic parsing.
| Enum | Description |
|---|---|
PAYMENT |
Payment successfully settled |
DEVICE_STATUS |
Device state changed |
PENDING_PAYMENT |
Invoice created, waiting for payment |
METADATA_ON_PENDING |
Custom metadata for pending payments (raw JSON) |
METADATA_ON_EXPIRED |
Custom metadata for expired payments (raw JSON) |
METADATA_ON_SUCCESS |
Custom metadata for successful payments (raw JSON) |
INIT |
Initial configuration after connection |
DISPLAY_PIN |
Display PIN on screen for guest verification |
VERIFICATION_RESULT |
PIN or precondition check result |
VERIFICATIONS |
Typed callback: handles PIN display and verification result |
PAYMENTS |
Typed callback: handles pending and success payments |
USAGES |
Typed callback: handles usage, limit exceeded, and near limit |
USAGE_RESPONSE |
Current MQTT usage and quota |
LIMIT_EXCEEDED |
Quota limit reached |
NEAR_LIMIT |
Approaching quota limit |
ENTITIES |
Received commands to specific entities (e.g. gpio_1) |
Register one callback to receive commands sent to specific entities (e.g. gpio_1, relay_2). Use the provided key to route commands to the correct GPIO or component in your code:
fngin.on(ENTITIES, [](EntityCommand &cmd) {
Serial.print("Key: "); Serial.println(cmd.key());
Serial.print("Value: "); Serial.println(cmd.value());
// Control GPIO based on entity key
if (cmd.key() == "gpio_1") {
digitalWrite(LED_BUILTIN, cmd.value() == "1" ? HIGH : LOW);
}
});Register callbacks for individual entities using onEntity(). The library will route commands to the correct handler automatically:
fngin.onEntity("gpio_1", [](EntityCommand &cmd) {
digitalWrite(LED_BUILTIN, cmd.value() == "1" ? HIGH : LOW);
});
fngin.onEntity("relay_2", [](EntityCommand &cmd) {
digitalWrite(RELAY_PIN, cmd.value() == "1" ? HIGH : LOW);
});Use the Entity class as a typed key reference for push and receive operations:
// Define entities (key only, no GPIO logic)
Entity relay1("gpio_1"); // entity "gpio_1"
Entity relay2("relay_2"); // entity "relay_2"
// Register callbacks for entity objects
ON_ENTITY(relay1, [](EntityCommand &cmd) {
digitalWrite(2, cmd.value() == "1" ? HIGH : LOW);
});
ON_ENTITY(relay2, [](EntityCommand &cmd) {
digitalWrite(4, cmd.value() == "1" ? HIGH : LOW);
});
// Or register by string key directly
ON_ENTITY_S("status", [](EntityCommand &cmd) {
Serial.print("Status received: ");
Serial.println(cmd.value());
});Send entity state updates from your device using pushEntity() or updateEntities(). Use string keys or Entity objects as keys:
// Send a single state update (string key)
fngin.pushEntity("gpio_1", "1");
fngin.pushEntity("temperature", "25.5");
// Or use Entity object as key
Entity relay1("gpio_1");
fngin.pushEntity(relay1, "1");
Entity sensor("temperature");
fngin.pushEntity(sensor, "25.5");
// Send batch state updates with builder pattern
bool sent = fngin.pushBatchEntities()
.add(10, String(temperature)) // int key, string value
.add(20, String(humidity)) // int key, string value
.add("pressure", String(pressure)) // string key, string value
.add("light", String(light)) // string key, string value
.add("status", status) // string key, string value
.send();
if (sent) {
Serial.println("Batch state sent successfully!");
}
// Or send raw JSON array directly
fngin.updateEntities("[{\"key\":\"gpio_1\",\"value\":\"1\"},{\"key\":\"gpio_2\",\"value\":\"0\"}]");Manually request initial configuration after connection:
fngin.requestInit();The following flows are used when your device is integrated with Firmngin's payment and verification system.
Register one callback for the entire verification flow. The same Verifications object handles both PIN display and verification result:
fngin.on(VERIFICATIONS, [](Verifications &v) {
if (v.hasPinNumber()) {
Serial.println(v.pin()); // "123456"
Serial.println(v.sessionId()); // "abc..."
Serial.println(v.ttl()); // 60
}
if (v.hasResult()) {
Serial.println(v.pinMet()); // true / false
Serial.println(v.preconditionMet()); // true / false
}
});Register one callback for the entire payment flow. The same Payments object handles both pending and success:
fngin.on(PAYMENTS, [](Payments &p) {
if (p.isPending()) {
Serial.println("Payment pending...");
}
if (p.isSuccess()) {
Serial.println("Payment received!");
}
Serial.println(p.itemTitle()); // "Cappuccino"
Serial.println(p.price()); // "45000"
Serial.println(p.orderId()); // "ODR-260506-12345678"
Serial.println(p.metadata()); // raw JSON string
});Register one callback for the entire usage flow. The same Usages object handles usage response, limit exceeded, and near limit:
fngin.on(USAGES, [](Usages &u) {
if (u.isNormal()) {
Serial.println("Usage normal");
}
if (u.isNearLimit()) {
Serial.println("WARNING: Near limit!");
}
if (u.isLimitExceeded()) {
Serial.println("ERROR: Limit exceeded!");
}
Serial.print("Used: "); Serial.println(u.used());
Serial.print("Limit: "); Serial.println(u.limit());
Serial.print("Remaining: "); Serial.println(u.remaining());
Serial.print("Percentage: "); Serial.println(u.percentage());
Serial.print("Reset at: "); Serial.println(u.resetAt());
Serial.print("Granularity:"); Serial.println(u.granularity());
});Register one callback for device state changes. The DeviceStates object provides boolean helpers for each state:
fngin.on(DEVICE_STATUS, [](DeviceStates &ds) {
if (ds.isIdle()) {
Serial.println("Device is idle");
}
if (ds.isPendingPayment()) {
Serial.println("Device is pending payment");
}
if (ds.isExpiredPayment()) {
Serial.println("Device payment expired");
}
if (ds.isSuccessPayment()) {
Serial.println("Device payment success");
}
if (ds.isMaintenance()) {
Serial.println("Device is under maintenance");
}
if (ds.isOnActiveService()) {
Serial.println("Device is on active service");
}
Serial.print("State: "); Serial.println(ds.state());
});The
DeviceStatesobject is automatically populated when adsmessage arrives. Use the boolean helpers to check the current state.
Register one callback for device initialization. The Inits object provides access to entities, merchant status, and verification flags:
fngin.on(INIT, [](Inits &i) {
Serial.print("Entities: "); Serial.println(i.entities()); // raw JSON array
if (i.isIdle()) {
Serial.println("Merchant status: idle");
}
if (i.isPendingPayment()) {
Serial.println("Merchant status: pending payment");
}
Serial.print("Verification flag: "); Serial.println(i.verificationFlag());
if (i.isPinEnabled()) {
Serial.println("PIN verification enabled");
}
if (i.isPreconditionEnabled()) {
Serial.println("Precondition verification enabled");
}
if (i.isVerificationRequired()) {
Serial.println("Some form of verification is required");
}
});The
Initsobject is automatically populated when aninitmessage arrives. Useentities()to get the raw JSON array of GPIO entities. Use the boolean helpers to check merchant status and verification configuration.
# Edit library in src/
# Sync to example
python3 sync_lib.py
# Build example
pio run -e esp32dev| Function | Description | Return Type | Possible Values |
|---|---|---|---|
begin() |
Initialize library, sync time, setup mTLS, and connect to server | void |
N/A |
loop() |
Maintain connection and process incoming messages | void |
N/A |
setDebug(bool) |
Enable/disable debug output to Serial | void |
true, false |
setTimezone(int) |
Set timezone offset from GMT | void |
-12 to 12 |
setDaylightOffsetSec(int) |
Set daylight saving offset in seconds | void |
Any integer |
setNtpServer(const char*) |
Set NTP server address | void |
e.g. "pool.ntp.org" |
setClient(Client&) |
Use an external network client | void |
EthernetClient, other Client impl |
isPlatformSupported() |
Check if current board is supported | bool |
true (ESP8266/ESP32), false |
on(STATE, callback) |
Register a raw callback for any state | void |
See enum constants |
on(VERIFICATIONS, callback) |
Register typed callback for dpin + vr | void |
Verifications & |
on(PAYMENTS, callback) |
Register typed callback for pp + pm | void |
Payments & |
on(USAGES, callback) |
Register typed callback for ur + le + nl | void |
Usages & |
on(DEVICE_STATUS, callback) |
Register typed callback for ds | void |
DeviceStates & |
on(INIT, callback) |
Register typed callback for init | void |
Inits & |
on(ENTITIES, callback) |
Register typed callback for entity commands | void |
EntityCommand & |
onEntity(key, callback) |
Register callback for a specific entity key (string or object) | void |
EntityCommand & |
pushEntity(key, value) |
Send a single entity state update | bool |
true = sent, false = fail |
pushEntity(entity, value) |
Send entity state using Entity object as key | bool |
true = sent, false = fail |
updateEntities(json) |
Send multiple entity states as JSON array | bool |
true = sent, false = fail |
pushBatchEntities() |
Start batch state builder (chain .add().send()) |
BatchState |
Builder object |
requestInit() |
Request initial configuration | bool |
true = sent, false = fail |
| Function | Description | Return Type | Possible Values |
|---|---|---|---|
isValid() |
Check if payload was parsed successfully | bool |
true, false |
hasPinNumber() |
True if this is a dpin message | bool |
true, false |
hasResult() |
True if this is a vr message | bool |
true, false |
pin() |
6-digit PIN code | String |
e.g. "123456" |
sessionId() |
Masked session ID | String |
e.g. "abc..." |
ttl() |
PIN validity in seconds | int |
e.g. 60 |
pinMet() |
PIN verification passed | bool |
true, false |
preconditionMet() |
Precondition check passed | bool |
true, false |
metadata() |
Raw JSON payload | String |
Full JSON string |
| Function | Description | Return Type | Possible Values |
|---|---|---|---|
isValid() |
Check if payload was parsed successfully | bool |
true, false |
isPending() |
True if this is a pp (pending payment) message | bool |
true, false |
isSuccess() |
True if this is a pm (payment success) message | bool |
true, false |
itemTitle() |
Menu item title | String |
e.g. "Cappuccino" |
price() |
Price as string | String |
e.g. "45000" |
orderId() |
Human-readable order ID | String |
e.g. "ODR-260506-12345678" |
metadata() |
Raw JSON payload | String |
Full JSON string |
| Function | Description | Return Type | Possible Values |
|---|---|---|---|
isValid() |
Check if payload was parsed successfully | bool |
true, false |
isNormal() |
Usage is within normal range | bool |
true, false |
isNearLimit() |
Quota is nearly exceeded (>80%) | bool |
true, false |
isLimitExceeded() |
Quota limit reached | bool |
true, false |
used() |
Messages used in current window | int |
e.g. 1250 |
limit() |
Plan limit for current window | int |
e.g. 30000 |
remaining() |
Messages remaining | int |
e.g. 28750 |
percentage() |
Usage percentage (0-100) | int |
e.g. 4 |
resetAt() |
ISO-8601 window reset time | String |
e.g. "2026-05-06T00:00:00Z" |
granularity() |
Window granularity | String |
"minute", "hour", "day", "month" |
metadata() |
Raw JSON payload | String |
Full JSON string |
| Function | Description | Return Type | Possible Values |
|---|---|---|---|
isValid() |
Check if payload was parsed successfully | bool |
true, false |
state() |
Raw state string | String |
"idle", "pending_payment", "expired_payment", "success_payment", "maintenance", "on_active_service" |
isIdle() |
Device is idle | bool |
true, false |
isPendingPayment() |
Device is pending payment | bool |
true, false |
isExpiredPayment() |
Device payment expired | bool |
true, false |
isSuccessPayment() |
Device payment success | bool |
true, false |
isMaintenance() |
Device is under maintenance | bool |
true, false |
isOnActiveService() |
Device is on active service | bool |
true, false |
metadata() |
Raw JSON payload | String |
Full JSON string |
| Function | Description | Return Type | Possible Values |
|---|---|---|---|
isValid() |
Check if payload was parsed successfully | bool |
true, false |
entities() |
Raw JSON array of GPIO entities | String |
e.g. [{"p":1,"v":"0"}] |
merchantStatus() |
Current merchant status | String |
"idle", "pending_payment", "expired_payment", "success_payment", "maintenance", "on_active_service" |
verificationFlag() |
Verification mode flag | int |
0, 1, 2, 3 |
isIdle() |
Merchant status is idle | bool |
true, false |
isPendingPayment() |
Merchant status is pending payment | bool |
true, false |
isExpiredPayment() |
Merchant status is expired payment | bool |
true, false |
isSuccessPayment() |
Merchant status is success payment | bool |
true, false |
isMaintenance() |
Merchant status is maintenance | bool |
true, false |
isOnActiveService() |
Merchant status is on active service | bool |
true, false |
isPinEnabled() |
PIN verification is enabled | bool |
true, false |
isPreconditionEnabled() |
Precondition verification is enabled | bool |
true, false |
isVerificationRequired() |
Some form of verification is required | bool |
true, false |
metadata() |
Raw JSON payload | String |
Full JSON string |
| Function | Description | Return Type | Possible Values |
|---|---|---|---|
isValid() |
Check if payload was parsed successfully | bool |
true, false |
key() |
Entity key (e.g. gpio_1, relay_2) | String |
e.g. "gpio_1" |
value() |
Raw command value | String |
e.g. "1", "ON", "255" |
metadata() |
Raw payload string | String |
Full payload string |
| Function | Description | Return Type | Example |
|---|---|---|---|
Entity(key) |
Constructor with key | N/A | Entity("temperature") |
key() |
Get entity key | String |
e.g. "gpio_1" |
gpio() |
Get GPIO pin (optional) | int |
e.g. 2 |
hasGpio() |
Check if GPIO is set | bool |
true, false |
| Macro | Description | Example |
|---|---|---|
ON_ENTITY(entityObject, callback) |
Register custom callback for entity object | ON_ENTITY(relay1, [](EntityCommand &cmd){ ... }); |
ON_ENTITY_S(key, callback) |
Register custom callback by string key | ON_ENTITY_S("led_1", [](EntityCommand &cmd){ ... }); |
fngin.onEntity(key, callback) |
Register callback by string key (manual) | fngin.onEntity("status", [](EntityCommand &cmd){ ... }); |
MIT License
