Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 9de9d3e

Browse files
committedNov 18, 2024·
Initial release
0 parents  commit 9de9d3e

12 files changed

+2682
-0
lines changed
 

‎.changeset/README.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Changesets
2+
3+
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4+
with multi-package repos, or single-package repos to help you version and publish your code. You can
5+
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6+
7+
We have a quick list of common questions to get you started engaging with this project in
8+
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)

‎.changeset/config.json

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"$schema": "https://unpkg.com/@changesets/config@3.0.3/schema.json",
3+
"changelog": "@changesets/cli/changelog",
4+
"commit": false,
5+
"fixed": [],
6+
"linked": [],
7+
"access": "public",
8+
"baseBranch": "main",
9+
"updateInternalDependencies": "patch",
10+
"ignore": []
11+
}

‎.github/workflows/ci.yml

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- main
8+
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.ref }}
11+
cancel-in-progress: true
12+
13+
jobs:
14+
ci:
15+
runs-on: ubuntu-latest
16+
strategy:
17+
matrix:
18+
node-version: [20]
19+
pnpm-version: [9]
20+
steps:
21+
- uses: actions/checkout@v4
22+
23+
- name: Install pnpm
24+
uses: pnpm/action-setup@v4
25+
with:
26+
version: ${{ matrix.pnpm-version }}
27+
28+
- name: Use Node.js ${{ matrix.node-version }}
29+
uses: actions/setup-node@v4
30+
with:
31+
node-version: ${{ matrix.node-version }}
32+
cache: "pnpm"
33+
34+
- name: Install dependencies
35+
run: pnpm install
36+
37+
- name: Run CI
38+
run: npm run ci

‎.gitignore

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Node.js
2+
node_modules/
3+
npm-debug.log*
4+
yarn-debug.log*
5+
yarn-error.log*
6+
7+
# TypeScript
8+
*.tsbuildinfo
9+
10+
# Logs
11+
logs
12+
*.log
13+
logs/*.log
14+
15+
# Dependency directories
16+
jspm_packages/
17+
18+
# Optional npm cache directory
19+
.npm
20+
21+
# Optional eslint cache
22+
.eslintcache
23+
24+
# Optional REPL history
25+
.node_repl_history
26+
27+
# Output directories
28+
dist/
29+
build/
30+
out/
31+
32+
# IDEs and editors
33+
.vscode/
34+
.idea/
35+
*.suo
36+
*.ntvs*
37+
*.njsproj
38+
*.sln
39+
*.sw?
40+
41+
# Environment variables
42+
.env
43+
.env.test
44+
45+
# macOS
46+
.DS_Store

‎.npmignore

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
!dist
2+
src
3+
.changeset
4+
node_modules

‎README.md

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Embedded : WiFi connection manager
2+
3+
> [!IMPORTANT]
4+
> This module is ESM only.
5+
6+
A robust solution to manage Wi-Fi connections in embedded systems using the Moddable SDK. `WiFiConnectionManager` simplifies the process of maintaining an active connection to a Wi-Fi access point.
7+
8+
## Features
9+
10+
- **Auto-reconnect:** Automatically retries connection if unavailable at startup or dropped during runtime.
11+
- **Connection lifecycle management:** Allows clean disconnection using `close`.
12+
- **Redundant message suppression:** Filters out repetitive `WiFi.disconnect` messages.
13+
- **Convenient readiness check:** Use the `ready` getter to quickly determine if Wi-Fi is connected.
14+
- **Connection timeout handling:** Forces a disconnect and retries if no IP address is assigned within a configurable timeout.
15+
- **Graceful recovery:** Waits a configurable duration after an unforced disconnect for a clean reconnect.
16+
- **Multiple access points:** Attempts connection to access points in order from a provided list.
17+
18+
## Installation
19+
20+
```sh
21+
npm install @embedded-js/wifi-connection-manager
22+
```
23+
24+
```ts
25+
import Net from "net"; // From moddable SDK
26+
import { WiFiConnectionManager } from "@embedded-js/wifi-connection-manager";
27+
28+
const manager = new WiFiConnectionManager(
29+
[
30+
// First access point to attempt
31+
{
32+
ssid: "Freebox-ScreamZ",
33+
password: "invalid!", // Invalid password for testing in a simulator
34+
},
35+
// Second access point to attempt
36+
{
37+
ssid: "Freebox-ScreamZ",
38+
password: "good_password",
39+
},
40+
],
41+
(message) => {
42+
switch (msg) {
43+
case WiFiConnectionManager.gotIP.connected:
44+
break; // still waiting for IP address
45+
case WiFiConnectionManager.gotIP.gotIP:
46+
trace(`IP address ${Net.get("IP")}\n`);
47+
break;
48+
case WiFiConnectionManager.gotIP.disconnected:
49+
break; // connection lost
50+
}
51+
}
52+
);
53+
54+
// Example: Checking if connected (procedural way)
55+
if (manager.ready) {
56+
console.log("Wi-Fi is connected!");
57+
}
58+
59+
// Close connection when needed
60+
manager.close();
61+
```

‎biome.json

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
3+
"vcs": {
4+
"enabled": false,
5+
"clientKind": "git",
6+
"useIgnoreFile": false
7+
},
8+
"files": {
9+
"ignoreUnknown": false,
10+
"ignore": ["dist"]
11+
},
12+
"formatter": {
13+
"enabled": true,
14+
"indentStyle": "tab"
15+
},
16+
"organizeImports": {
17+
"enabled": true
18+
},
19+
"linter": {
20+
"enabled": true,
21+
"rules": {
22+
"recommended": true
23+
}
24+
},
25+
"javascript": {
26+
"formatter": {
27+
"quoteStyle": "double"
28+
}
29+
}
30+
}

‎package.json

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "@embedded-js/wifi-connection-manager",
3+
"description": "A wifi connection manager for Embedded JS that supports multiple networks and auto-reconnect.",
4+
"version": "1.0.0",
5+
"type": "module",
6+
"main": "dist/index.js",
7+
"scripts": {
8+
"build": "tsup",
9+
"ci": "tsc && pnpm run build && biome check && pnpm run check-exports",
10+
"format": "biome check --write",
11+
"check-exports": "attw --pack . --ignore-rules=cjs-resolves-to-esm",
12+
"prepublishOnly": "pnpm run ci"
13+
},
14+
"devDependencies": {
15+
"@arethetypeswrong/cli": "^0.17.0",
16+
"@biomejs/biome": "1.9.4",
17+
"@changesets/cli": "^2.27.9",
18+
"@moddable/typings": "^5.2.0",
19+
"tsup": "^8.3.5",
20+
"type-fest": "^4.27.0"
21+
}
22+
}

‎pnpm-lock.yaml

+2,246
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/index.ts

+182
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/// <reference types="../node_modules/@moddable/typings/xs.d.ts" />
2+
/// <reference types="../node_modules/@moddable/typings/wifi.d.ts" />
3+
/// <reference types="../node_modules/@moddable/typings/timer.d.ts" />
4+
5+
import WiFi, { type WiFiCallback, type WiFiOptions } from "wifi";
6+
import Timer from "timer";
7+
8+
type WiFiMessage =
9+
| "gotIP"
10+
| "lostIP"
11+
| "connect"
12+
| "disconnect"
13+
| "station_connect"
14+
| "station_disconnect";
15+
16+
/**
17+
* WiFiConnectionManager is designed to be used in place of the "wifi" module in project, that need a continuously available connection to a Wi-Fi access point.
18+
*
19+
* - If no connection available at start, retries until one is.
20+
* - Automatically attempts to reconnect when connection dropped.
21+
* - Disconnects when calling close.
22+
* - Suppresses redundant WiFi.disconnect messages.
23+
* - Callback uses same message constants as "wifi" from the Moddable SDK.
24+
* - Getter on "ready" is convenient way to check if Wi-Fi is connected.
25+
* - If connection attempt does not succeed with an IP address in X (arg) seconds, forces disconnect and retries.
26+
* - Wait X (configurable) seconds after (unforced) disconnect to ensure clean reconnect.
27+
* - Try a list of access point in order from array structure
28+
*
29+
* ```ts
30+
* import { WiFiConnectionManager } from "@embedded-js/wifi-connection-manager";
31+
*
32+
* const manager = new WiFiConnectionManager(
33+
* [
34+
* // First access point tested
35+
* {
36+
* ssid: "Freebox-ScreamZ",
37+
* password: "invalid!", // For usage in simulator, this is the checked invalid password
38+
* },
39+
* // Second access point tested
40+
* {
41+
* ssid: "Freebox-ScreamZ",
42+
* password: "good_password",
43+
* },
44+
* ],
45+
* (m) => {
46+
* trace(m, "\n");
47+
* }
48+
* );
49+
* ```
50+
*/
51+
export class WiFiConnectionManager extends WiFi {
52+
private reconnectTimer?: Timer;
53+
private connectionTimer?: Timer;
54+
private currentState: WiFiMessage = "disconnect";
55+
private APIndex = 0;
56+
57+
constructor(
58+
private readonly accessPointConfig: Array<WiFiOptions>,
59+
private onNetworkChange?: WiFiCallback,
60+
private readonly reconnectInterval: number = 5000, // Default reconnect interval in ms
61+
private readonly connectionTimeout: number = 30000 // Default connection timeout in ms
62+
) {
63+
// Initialize parent WiFi class with a custom callback handler
64+
super(accessPointConfig[0], (message) => this.handleWiFiEvent(message));
65+
66+
// Start the initial connection attempt with a timeout
67+
this.startConnectionTimer();
68+
}
69+
70+
/**
71+
* Handles WiFi events and manages state transitions, reconnection, and user callbacks.
72+
*/
73+
private handleWiFiEvent(message: WiFiMessage): void {
74+
if (message === "disconnect") {
75+
this.handleDisconnect();
76+
} else if (message === "gotIP") {
77+
this.clearTimers();
78+
this.APIndex = 0; // Reset access point index on successful connection
79+
} else if (message === "connect") {
80+
this.clearReconnectTimer();
81+
}
82+
83+
this.currentState = message;
84+
this.onNetworkChange?.(message);
85+
}
86+
87+
/**
88+
* Handles WiFi disconnection events, including retries and reconnection.
89+
*/
90+
private handleDisconnect(): void {
91+
this.clearConnectionTimer();
92+
93+
this.APIndex = (this.APIndex + 1) % this.accessPointConfig.length;
94+
95+
this.clearReconnectTimer();
96+
this.reconnectTimer = Timer.set(() => {
97+
this.reconnectTimer = undefined;
98+
99+
try {
100+
WiFi.connect(this.accessPointConfig[this.APIndex]);
101+
this.startConnectionTimer();
102+
} catch (error) {
103+
trace("Error during reconnect:", error as string);
104+
this.handleDisconnect();
105+
}
106+
}, this.reconnectInterval); // Wait before reconnecting, accounting for spurious disconnects
107+
108+
if (this.currentState !== "disconnect") {
109+
this.currentState = "disconnect";
110+
this.onNetworkChange?.("disconnect");
111+
}
112+
}
113+
114+
/**
115+
* Starts a timer for connection attempts, forcing a disconnect on timeout.
116+
*/
117+
private startConnectionTimer(): void {
118+
this.connectionTimer = Timer.set(() => {
119+
this.clearConnectionTimer();
120+
try {
121+
WiFi.disconnect(); // Force disconnect on timeout
122+
} catch (error) {
123+
trace("Error during forced disconnect:", error as string);
124+
}
125+
}, this.connectionTimeout);
126+
}
127+
128+
/**
129+
* Clears the connection attempt timer.
130+
*/
131+
private clearConnectionTimer(): void {
132+
if (this.connectionTimer) {
133+
Timer.clear(this.connectionTimer);
134+
this.connectionTimer = undefined;
135+
}
136+
}
137+
138+
/**
139+
* Clears the reconnect timer.
140+
*/
141+
private clearReconnectTimer(): void {
142+
if (this.reconnectTimer) {
143+
Timer.clear(this.reconnectTimer);
144+
this.reconnectTimer = undefined;
145+
}
146+
}
147+
148+
/**
149+
* Clears all timers and resets connection state.
150+
*/
151+
private clearTimers(): void {
152+
this.clearConnectionTimer();
153+
this.clearReconnectTimer();
154+
}
155+
156+
/**
157+
* Closes the connection, cleans up resources, and resets the state.
158+
* This method should be called when the manager is no longer needed
159+
* to ensure proper cleanup of timers and resources.
160+
*/
161+
override close(): void {
162+
this.clearTimers();
163+
try {
164+
WiFi.disconnect();
165+
} catch (error) {
166+
trace("Error during WiFi.disconnect:", error as string);
167+
}
168+
169+
try {
170+
super.close();
171+
} catch (error) {
172+
trace("Error during super.close:", error as string);
173+
}
174+
}
175+
176+
/**
177+
* Returns true if the connection has obtained an IP address.
178+
*/
179+
get ready(): boolean {
180+
return this.currentState === "gotIP";
181+
}
182+
}

‎tsconfig.json

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"compilerOptions": {
3+
/* Base Options: */
4+
"esModuleInterop": true,
5+
"skipLibCheck": true,
6+
"target": "es2022",
7+
"lib": ["ES2022"],
8+
"allowJs": true,
9+
"resolveJsonModule": true,
10+
"moduleDetection": "force",
11+
"isolatedModules": true,
12+
"verbatimModuleSyntax": true,
13+
14+
/* Strictness */
15+
"strict": true,
16+
"noUncheckedIndexedAccess": true,
17+
"noImplicitOverride": true,
18+
19+
"noEmit": true,
20+
"module": "NodeNext"
21+
},
22+
"include": ["src"],
23+
"exclude": ["dist", "node_modules"]
24+
}

‎tsup.config.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { defineConfig } from "tsup";
2+
3+
export default defineConfig({
4+
entryPoints: ["src/index.ts"],
5+
format: ["esm"],
6+
dts: true,
7+
outDir: "dist",
8+
minify: true,
9+
clean: true,
10+
});

0 commit comments

Comments
 (0)
Please sign in to comment.