fostrom 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +289 -0
- package/package.json +5 -2
- package/readme.md +42 -0
package/index.js
CHANGED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { execSync } from "child_process"
|
|
2
|
+
import { fileURLToPath } from "url"
|
|
3
|
+
import path from "path"
|
|
4
|
+
|
|
5
|
+
function agent_path() {
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
7
|
+
return path.join(__dirname, ".agent", "fostrom-device-agent")
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
class FostromError extends Error {
|
|
11
|
+
constructor(atom, msg) {
|
|
12
|
+
const formattedMessage = `\x1b[31m\x1b[1mFostrom Error\x1b[0m \x1b[34m[${atom}]\x1b[0m ${msg}`
|
|
13
|
+
super(formattedMessage)
|
|
14
|
+
this.name = ""
|
|
15
|
+
this.stack = null
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class Mail {
|
|
20
|
+
id
|
|
21
|
+
name
|
|
22
|
+
payload = undefined
|
|
23
|
+
mailbox_size
|
|
24
|
+
#instance
|
|
25
|
+
|
|
26
|
+
constructor(fostrom_instance, id, name, payload, mailbox_size) {
|
|
27
|
+
this.#instance = fostrom_instance
|
|
28
|
+
this.id = id
|
|
29
|
+
this.name = name
|
|
30
|
+
this.payload = payload
|
|
31
|
+
this.mailbox_size = mailbox_size
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async ack() { return await this.#instance.__mail_op("ack", this.id) }
|
|
35
|
+
async reject() { return await this.#instance.__mail_op("reject", this.id) }
|
|
36
|
+
async requeue() { return await this.#instance.__mail_op("requeue", this.id) }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default class Fostrom {
|
|
40
|
+
#log = true
|
|
41
|
+
#creds = {}
|
|
42
|
+
|
|
43
|
+
onMail = async mail => {
|
|
44
|
+
console.log(`[Fostrom] Received Mail (Mailbox Size: ${mail.mailbox_size}): ${mail.name} -> ID ${mail.id}`)
|
|
45
|
+
console.warn(" Auto-Acknowledging Mail. Define Mail Handler to handle incoming mail.\n `fostrom.on_mail = async (mail) => { ...; await mail.ack(); }`\n")
|
|
46
|
+
await mail.ack()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
connected = () => {
|
|
50
|
+
if (this.#log) console.log("[Fostrom] Connected")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
unauthorized = (reason, after) => {
|
|
54
|
+
if (this.#log) {
|
|
55
|
+
const after_s = Math.floor(after / 1000);
|
|
56
|
+
console.log(`[Fostrom] Unauthorized: ${reason}. Reconnecting in ${after_s} seconds...`)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
reconnecting = (reason, after) => {
|
|
61
|
+
if (this.#log) {
|
|
62
|
+
const after_s = Math.floor(after / 1000);
|
|
63
|
+
console.log(`[Fostrom] Failed to connect: ${reason}. Reconnecting in ${after_s} seconds...`)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
constructor(config = {}) {
|
|
68
|
+
if (!config.fleet_id) throw "[Fostrom] Fleet ID required."
|
|
69
|
+
if (!config.device_id) throw "[Fostrom] Device ID required."
|
|
70
|
+
if (!config.device_secret) throw "[Fostrom] Device Secret required."
|
|
71
|
+
this.#creds.fleet_id = config.fleet_id
|
|
72
|
+
this.#creds.device_id = config.device_id
|
|
73
|
+
this.#creds.device_secret = config.device_secret
|
|
74
|
+
if (config.log == false) this.#log = false
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
#start_agent() {
|
|
78
|
+
const { fleet_id, device_id, device_secret } = this.#creds
|
|
79
|
+
|
|
80
|
+
let args = [
|
|
81
|
+
"start",
|
|
82
|
+
"--fleet-id", fleet_id,
|
|
83
|
+
"--device-id", device_id,
|
|
84
|
+
"--device-secret", device_secret
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
if (process.env.FOSTROM_AGENT_MODE === 'dev') args.push("--dev")
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const output = execSync(`${agent_path()} ${args.join(" ")}`, { encoding: "utf8" })
|
|
91
|
+
|
|
92
|
+
if (output.trim() == "starting") {
|
|
93
|
+
execSync(`sleep 0.25`)
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
let [err, msg] = error.stdout.trim().split(":", 2)
|
|
98
|
+
throw new FostromError(err, msg)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async #req(path = "/", method = "GET", payload = null, additional_options = {}) {
|
|
103
|
+
if (method != "GET" && method != "PUT" && method != "POST" && method != "DELETE") {
|
|
104
|
+
throw new Error(`Unsupported ${method} Request for path ${path}`)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const headers = new Headers()
|
|
108
|
+
headers.set("Content-Type", "application/json")
|
|
109
|
+
headers.set("Accept", "application/json")
|
|
110
|
+
|
|
111
|
+
const base_options = {
|
|
112
|
+
method,
|
|
113
|
+
headers,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if ((method == "POST" || method == "PUT") && payload) {
|
|
117
|
+
base_options.body = JSON.stringify(payload)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const options = {
|
|
121
|
+
...base_options,
|
|
122
|
+
...additional_options
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let resp
|
|
126
|
+
let body
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
resp = await fetch(`http://localhost:8585${path}`, options)
|
|
130
|
+
} catch (e) {
|
|
131
|
+
throw new FostromError("req_failed", "Communicating with the device agent is failing.")
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const fleet_id = resp.headers.get('x-fleet-id')
|
|
135
|
+
const device_id = resp.headers.get('x-device-id')
|
|
136
|
+
|
|
137
|
+
if (this.#creds.fleet_id != fleet_id || this.#creds.device_id != device_id) {
|
|
138
|
+
console.warn("Restarting Agent due to credential mismatch.")
|
|
139
|
+
this.stop_agent()
|
|
140
|
+
this.#start_agent()
|
|
141
|
+
return await this.#req(path, options)
|
|
142
|
+
} else {
|
|
143
|
+
try {
|
|
144
|
+
body = await resp.json()
|
|
145
|
+
}
|
|
146
|
+
catch (e) {
|
|
147
|
+
console.error(e)
|
|
148
|
+
throw new FostromError("req_failed", "Communicating with the device agent is failing.")
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (body.error && body.msg) {
|
|
153
|
+
throw new FostromError(body.error, body.msg)
|
|
154
|
+
} else {
|
|
155
|
+
return body
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
static stopAgent() {
|
|
160
|
+
try {
|
|
161
|
+
execSync(`${agent_path()} stop`, { encoding: "utf8" })
|
|
162
|
+
} catch (e) {
|
|
163
|
+
console.error("[Fostrom] Failed to stop the Fostrom Device Agent")
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async connect() {
|
|
168
|
+
this.#start_agent()
|
|
169
|
+
const resp = await this.#req()
|
|
170
|
+
const status = resp.connect_status
|
|
171
|
+
|
|
172
|
+
if (!status.connected) {
|
|
173
|
+
console.error(new FostromError(status.error, status.msg))
|
|
174
|
+
} else {
|
|
175
|
+
this.#open_event_stream()
|
|
176
|
+
return true
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async sendDatapoint(name, payload) {
|
|
181
|
+
return await this.#req(`/pulse/datapoint/${name}`, "POST", payload)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async sendMsg(name, payload) {
|
|
185
|
+
return await this.#req(`/pulse/msg/${name}`, "POST", payload)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async mailboxStatus() {
|
|
189
|
+
const resp = await this.#req(`/mailbox/status`)
|
|
190
|
+
|
|
191
|
+
if (resp.mailbox_empty == true) {
|
|
192
|
+
return {
|
|
193
|
+
mailbox_size: 0,
|
|
194
|
+
next_mail_id: null,
|
|
195
|
+
next_mail_name: null
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
return {
|
|
199
|
+
mailbox_size: resp.mailbox_size,
|
|
200
|
+
next_mail_id: resp.pulse_id,
|
|
201
|
+
next_mail_name: resp.name,
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async nextMail() {
|
|
207
|
+
const resp = await this.#req(`/mailbox/next`)
|
|
208
|
+
|
|
209
|
+
if (resp.mailbox_empty == true) {
|
|
210
|
+
return null
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return new Mail(this, resp.pulse_id, resp.name, resp.payload, resp.mailbox_size)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async __mail_op(op, mail_id) {
|
|
217
|
+
if (op != "ack" && op != "reject" && op != "requeue") {
|
|
218
|
+
throw new Error("Invalid Mailbox Operation")
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return await this.#req(`/mailbox/${op}/${mail_id}`, "PUT")
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async #event_handler(event_object) {
|
|
225
|
+
const { event, _timestamp } = event_object
|
|
226
|
+
const data = event_object.data
|
|
227
|
+
|
|
228
|
+
switch (event) {
|
|
229
|
+
case 'connected': this.connected(); break;
|
|
230
|
+
case 'unauthorized': this.unauthorized(data.reason, data.reconnecting_in); break;
|
|
231
|
+
case 'connect_failed': this.reconnecting(data.reason, data.reconnecting_in); break;
|
|
232
|
+
case 'next_mail': this.#dispatch_next_mail(data); break;
|
|
233
|
+
default: null
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async #dispatch_next_mail(data) {
|
|
238
|
+
const mail = new Mail(this, data.pulse_id, data.name, data.payload, data.mailbox_size)
|
|
239
|
+
this.onMail(mail)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async #open_event_stream() {
|
|
243
|
+
const td = new TextDecoder()
|
|
244
|
+
|
|
245
|
+
const resp = await fetch(`http://localhost:8585/events`, {
|
|
246
|
+
headers: { 'Accept': 'text/event-stream' }
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
const reader = resp.body.getReader()
|
|
250
|
+
let buffer = ''
|
|
251
|
+
|
|
252
|
+
while (true) {
|
|
253
|
+
const { done, value } = await reader.read()
|
|
254
|
+
if (done) { this.connect() }
|
|
255
|
+
const decoded = td.decode(value)
|
|
256
|
+
parse_events(buffer, decoded, this.#event_handler.bind(this))
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function parse_events(buffer, chunk, event_handler) {
|
|
262
|
+
buffer += chunk
|
|
263
|
+
const lines = buffer.split('\n')
|
|
264
|
+
|
|
265
|
+
// Keep the last incomplete line in buffer
|
|
266
|
+
buffer = lines.pop() || ''
|
|
267
|
+
|
|
268
|
+
let event = {}
|
|
269
|
+
|
|
270
|
+
for (const line of lines) {
|
|
271
|
+
// Empty line indicates end of event
|
|
272
|
+
if (line === '') {
|
|
273
|
+
if (event.data && event.data != '') {
|
|
274
|
+
event.data = JSON.parse(event.data)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
event_handler(event)
|
|
278
|
+
event = {}
|
|
279
|
+
} else if (line.startsWith('data: ')) {
|
|
280
|
+
event.data = (event.data || '') + line.slice(6)
|
|
281
|
+
} else if (line.startsWith('event: ')) {
|
|
282
|
+
event.event = line.slice(7)
|
|
283
|
+
} else if (line.startsWith('id: ')) {
|
|
284
|
+
event.timestamp = new Date(parseInt(line.slice(4)))
|
|
285
|
+
} else if (line.startsWith('retry: ')) {
|
|
286
|
+
event.retry = parseInt(line.slice(7))
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fostrom",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "Fostrom's Official Device SDK for JS. Fostrom (https://fostrom.io) is an IoT Cloud Platform.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fostrom",
|
|
@@ -22,8 +22,11 @@
|
|
|
22
22
|
"author": "Fostrom <support@fostrom.io> (https://fostrom.io)",
|
|
23
23
|
"files": [
|
|
24
24
|
"index.js",
|
|
25
|
-
"dl-agent.sh"
|
|
25
|
+
"dl-agent.sh",
|
|
26
|
+
"readme.md"
|
|
26
27
|
],
|
|
28
|
+
"main": "index.js",
|
|
29
|
+
"type": "module",
|
|
27
30
|
"scripts": {
|
|
28
31
|
"postinstall": "sh dl-agent.sh .agent"
|
|
29
32
|
},
|
package/readme.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Fostrom Device SDK
|
|
2
|
+
|
|
3
|
+
[Fostrom](https://fostrom.io) is an IoT Cloud Platform built for developers. Monitor and control your fleet of devices, from microcontrollers to industrial IoT. Designed to be simple, secure, and fast. Experience first-class tooling with Device SDKs, type-safe schemas, programmable actions, and more.
|
|
4
|
+
|
|
5
|
+
The Fostrom Device SDK for JavaScript works in Node.js and Bun, on Linux and macOS, and helps you quickly integrate, start monitoring, and controlling your IoT devices in just a few lines of code.
|
|
6
|
+
|
|
7
|
+
## Example
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import Fostrom from 'fostrom';
|
|
11
|
+
|
|
12
|
+
const fostrom = new Fostrom({
|
|
13
|
+
fleet_id: "<fleet-id>",
|
|
14
|
+
device_id: "<device-id>",
|
|
15
|
+
device_secret: "<device-secret>",
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
// Setup the on_mail handler, to process incoming mail.
|
|
19
|
+
fostrom.onMail = async (mail) => {
|
|
20
|
+
{id, name, payload, mailbox_size} = mail
|
|
21
|
+
console.debug(`Received Mail (${mailbox_size}): ${name} ${id}`)
|
|
22
|
+
await mail.ack()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function main() {
|
|
26
|
+
await fostrom.connect()
|
|
27
|
+
|
|
28
|
+
// To send a message to Fostrom
|
|
29
|
+
await fostrom.sendMsg("<packet-schema-name>", { ...payload })
|
|
30
|
+
|
|
31
|
+
// To send a datapoint to Fostrom
|
|
32
|
+
await fostrom.sendDatapoint("<packet-schema-name>", { ...payload })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
main()
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## A Note on the Device Agent
|
|
39
|
+
|
|
40
|
+
The Fostrom Device SDK downloads and runs the Fostrom Device Agent in the background. The Agent is downloaded when the package is installed through `npm`. The Device Agent is started when `fostrom.connect()` is called, and it remains running in the background forever.
|
|
41
|
+
|
|
42
|
+
We recommend you allow the Device Agent to run continously, even if your program has exited or crashed, so that when your program is automatically restarted by a process manager, the reconnection to Fostrom is nearly instant. However, if you wish to stop the Device Agent, you can call `Fostrom.stopAgent()` in your code before terminating your program.
|