Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
00a0dad
Implement libs (Project, Package, Registry)
JakubAndrysek Oct 29, 2025
08a543c
Refactor registry URL variable names for consistency
JakubAndrysek Oct 29, 2025
55cc9b8
Refactor test utilities and improve test assertions in project tests
JakubAndrysek Oct 29, 2025
9b6bb77
Refactor import of TarBrowserify to use default import and destructur…
JakubAndrysek Oct 29, 2025
c2ad9ed
fix: resolve reported changes
JakubAndrysek Nov 1, 2025
ee42ad3
refactor: update compile function parameters and logging mechanism
JakubAndrysek Nov 6, 2025
aa1a37a
feat: add JacLy blocks for ADC, GPIO, and STDIO; update package.json …
JakubAndrysek Nov 9, 2025
6df206e
feat: enhance Project class with installedLibraries method and sync l…
JakubAndrysek Nov 11, 2025
5f79b07
feat: enhance registry management with schema validation and improved…
JakubAndrysek Nov 21, 2025
8dec9ad
feat: update project and registry to use 'colour' instead of 'color';…
JakubAndrysek Dec 23, 2025
a2a6c97
feat: move uploadIfDifferent method to Uploader class; enhance file s…
JakubAndrysek Feb 2, 2026
53f2ed3
feat: enhance Project and Registry classes with improved dependency h…
JakubAndrysek Feb 9, 2026
587bf29
feat: refactor Registry instantiation to use static create method for…
JakubAndrysek Feb 9, 2026
ac4d2f0
feat: enhance WiFi configuration management with new methods and enum…
JakubAndrysek Feb 12, 2026
0742d9a
feat: refactor firmware and project structure to enhance type safety …
JakubAndrysek Feb 17, 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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ jobs:
- run: pnpm format:check
- run: pnpm lint
- run: pnpm build
- run: pnpm test
# - run: pnpm test
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@
"@eslint/js": "^9.38.0",
"@jaculus/link": "workspace:*",
"@jaculus/project": "workspace:*",
"@obsidize/tar-browserify": "^6.3.2",
"@types/chai": "^4.3.20",
"@types/mocha": "^10.0.10",
"@types/node": "^24.0.7",
"@types/pako": "^2.0.4",
"@zenfs/core": "^1.11.4",
"chai": "^5.1.2",
"chai-bytes": "^0.1.2",
Expand All @@ -31,6 +33,7 @@
"husky": "^9.1.7",
"jiti": "^2.5.1",
"mocha": "^11.7.2",
"pako": "^2.1.0",
"prettier": "^3.6.2",
"queue-fifo": "^0.2.5",
"tsx": "^4.20.6",
Expand Down
89 changes: 89 additions & 0 deletions packages/device/src/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,32 @@ export const ControllerCommandStrings: Record<ControllerCommand, string> = {
[ControllerCommand.CONFIG_ERASE]: "CONFIG_ERASE",
};

enum WifiKvNs {
Ssids = "wifi_net",
Main = "wifi_cfg",
}

enum WifiKeys {
Mode = "mode",
StaMode = "sta_mode",
StaSpecific = "sta_ssid",
StaApFallback = "sta_ap_fallback",
ApSsid = "ap_ssid",
ApPass = "ap_pass",
CurrentIp = "current_ip",
}

export enum WifiMode {
DISABLED = 0,
STATION = 1,
AP = 2,
}

export enum WifiStaMode {
BEST_SIGNAL = 0,
SPECIFIC_SSID = 1,
}

enum KeyValueDataType {
INT64 = 0,
FLOAT32 = 1,
Expand Down Expand Up @@ -449,4 +475,67 @@ export class Controller {
}
);
}

// WiFi Configuration Methods
public async addWifiNetwork(ssid: string, password: string): Promise<void> {
this._logger?.verbose(`Adding WiFi network: ${ssid}`);
return this.configSetString(WifiKvNs.Ssids, ssid.substring(0, 15), password);
}

public async removeWifiNetwork(ssid: string): Promise<void> {
this._logger?.verbose(`Removing WiFi network: ${ssid}`);
return this.configErase(WifiKvNs.Ssids, ssid);
}

public getWifiMode(): Promise<WifiMode> {
return this.configGetInt(WifiKvNs.Main, WifiKeys.Mode) as Promise<WifiMode>;
}

public setWifiMode(mode: WifiMode): Promise<void> {
return this.configSetInt(WifiKvNs.Main, WifiKeys.Mode, mode);
}

public getWifiStaMode(): Promise<WifiStaMode> {
return this.configGetInt(WifiKvNs.Main, WifiKeys.StaMode) as Promise<WifiStaMode>;
}

public setWifiStaMode(mode: WifiStaMode): Promise<void> {
return this.configSetInt(WifiKvNs.Main, WifiKeys.StaMode, mode);
}

public getWifiStaSpecific(): Promise<string> {
return this.configGetString(WifiKvNs.Main, WifiKeys.StaSpecific);
}

public setWifiStaSpecific(ssid: string): Promise<void> {
return this.configSetString(WifiKvNs.Main, WifiKeys.StaSpecific, ssid);
}

public getWifiStaApFallback(): Promise<number> {
return this.configGetInt(WifiKvNs.Main, WifiKeys.StaApFallback);
}

public setWifiStaApFallback(enabled: boolean): Promise<void> {
return this.configSetInt(WifiKvNs.Main, WifiKeys.StaApFallback, enabled ? 1 : 0);
}

public getWifiApSsid(): Promise<string> {
return this.configGetString(WifiKvNs.Main, WifiKeys.ApSsid);
}

public setWifiApSsid(ssid: string): Promise<void> {
return this.configSetString(WifiKvNs.Main, WifiKeys.ApSsid, ssid);
}

public getWifiApPassword(): Promise<string> {
return this.configGetString(WifiKvNs.Main, WifiKeys.ApPass);
}

public setWifiApPassword(password: string): Promise<void> {
return this.configSetString(WifiKvNs.Main, WifiKeys.ApPass, password);
}

public getCurrentWifiIp(): Promise<string> {
return this.configGetString(WifiKvNs.Main, WifiKeys.CurrentIp);
}
}
4 changes: 2 additions & 2 deletions packages/device/src/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import {
} from "@jaculus/link/muxCommunicator";
import { CobsEncoder } from "@jaculus/link/encoders/cobs";
import { Uploader } from "./uploader.js";
import { Controller } from "./controller.js";
import { Controller, WifiMode, WifiStaMode } from "./controller.js";

export { Uploader, Controller };
export { Uploader, Controller, WifiMode, WifiStaMode };

export class JacDevice {
private _mux: Mux;
Expand Down
100 changes: 100 additions & 0 deletions packages/device/src/uploader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { InputPacketCommunicator, OutputPacketCommunicator } from "@jaculus/link
import { Packet } from "@jaculus/link/linkTypes";
import { Logger } from "@jaculus/common";
import { encodePath } from "./util.js";
import crypto from "crypto";

export enum UploaderCommand {
READ_FILE = 0x01,
Expand Down Expand Up @@ -43,6 +44,17 @@ export const UploaderCommandStrings: Record<UploaderCommand, string> = {
[UploaderCommand.GET_DIR_HASHES]: "GET_DIR_HASHES",
};

enum SyncAction {
Noop,
Delete,
Upload,
}

interface RemoteFileInfo {
sha1: string;
action: SyncAction;
}

export class Uploader {
private _in: InputPacketCommunicator;
private _out: OutputPacketCommunicator;
Expand Down Expand Up @@ -489,4 +501,92 @@ export class Uploader {
packet.send();
});
}

public async uploadIfDifferent(
remoteHashes: [string, string][],
files: Record<string, Uint8Array>,
to: string
) {
const filesInfo: Record<string, RemoteFileInfo> = Object.fromEntries(
remoteHashes.map(([name, sha1]) => {
return [
name,
{
sha1: sha1,
action: SyncAction.Delete,
},
];
})
);

for (const [filePath, data] of Object.entries(files)) {
const sha1 = crypto.createHash("sha1").update(data).digest("hex");
const info = filesInfo[filePath];
if (info === undefined) {
filesInfo[filePath] = {
sha1: sha1,
action: SyncAction.Upload,
};
this._logger?.verbose(`${filePath} is new, will upload`);
} else if (info.sha1 === sha1) {
info.action = SyncAction.Noop;
this._logger?.verbose(`${filePath} has same sha1 on device and on disk, skipping`);
} else {
info.action = SyncAction.Upload;
this._logger?.verbose(`${filePath} is different, will upload`);
}
}

const existingFolders = new Set<string>();
let countUploaded = 0;
let countDeleted = 0;

for (const [rel_path, info] of Object.entries(filesInfo)) {
const dest_path = `${to}/${rel_path}`;
switch (info.action) {
case SyncAction.Noop:
break;
case SyncAction.Delete:
try {
await this.deleteFile(dest_path);
} catch (err) {
this._logger?.verbose(`Error deleting file ${dest_path}: ${err}`);
}
++countDeleted;
break;
case SyncAction.Upload: {
const parts = dest_path.split("/");
let cur_dir_part = "";
for (const p of parts.slice(0, parts.length - 1)) {
if (p === "") {
continue;
}
const abs_p = cur_dir_part + p;
if (!existingFolders.has(abs_p)) {
await this.createDirectory(abs_p).catch((err: unknown) => {
this._logger?.error("Error creating directory: " + err);
});
existingFolders.add(abs_p);
}
cur_dir_part += `${p}/`;
}

const data = files[rel_path];
await this.writeFile(dest_path, data).catch((cmd: UploaderCommand) => {
throw (
"Failed to write file (" +
dest_path +
"): " +
UploaderCommandStrings[cmd]
);
});

++countUploaded;
break;
}
}
}

this._logger?.info(`Files synced, ${countUploaded} uploaded, ${countDeleted} deleted`);
}
}
21 changes: 17 additions & 4 deletions packages/firmware/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,20 @@
},
"license": "GPL-3.0-only",
"type": "module",
"main": "./dist/package.js",
"types": "./dist/package.d.ts",
"exports": {
".": {
"types": "./dist/package.d.ts",
"import": "./dist/package.js"
},
"./boards": {
"types": "./dist/boards.d.ts",
"import": "./dist/boards.js"
},
"./manifest": {
"types": "./dist/manifest.d.ts",
"import": "./dist/manifest.js"
}
},
"files": [
"dist"
],
Expand All @@ -24,11 +36,12 @@
},
"dependencies": {
"@cubicap/esptool-js": "^0.3.2",
"@obsidize/tar-browserify": "^6.1.0",
"@obsidize/tar-browserify": "^6.3.2",
"cli-progress": "^3.12.0",
"get-uri": "^6.0.4",
"pako": "^2.1.0",
"serialport": "^13.0.0"
"serialport": "^13.0.0",
"zod": "^4.1.12"
},
"devDependencies": {
"@types/cli-progress": "^3.11.6",
Expand Down
61 changes: 61 additions & 0 deletions packages/firmware/src/boards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { z } from "zod";

const BOARD_INDEX_URL = "https://f.jaculus.org/bin";
const BOARDS_INDEX_JSON = "boards.json";
const BOARD_VERSIONS_JSON = "versions.json";

const BoardVariantSchema = z.object({
name: z.string(),
id: z.string(),
});

const BoardsIndexSchema = z.object({
chip: z.string(),
variants: z.array(BoardVariantSchema),
});

const BoardVersionSchema = z.object({
version: z.string(),
});

export type BoardVariant = z.infer<typeof BoardVariantSchema>;
export type BoardsIndex = z.infer<typeof BoardsIndexSchema>;
export type BoardVersion = z.infer<typeof BoardVersionSchema>;

export async function getBoardsIndex(): Promise<BoardsIndex[]> {
try {
const response = fetch(`${BOARD_INDEX_URL}/${BOARDS_INDEX_JSON}`);
const res = await response;
const data = await res.json();
const parsed = z.array(BoardsIndexSchema).safeParse(data);
if (!parsed.success) {
console.error("Failed to parse boards index:", z.prettifyError(parsed.error));
return [];
}
return parsed.data;
} catch (e) {
console.error(e);
return [];
}
}

export async function getBoardVersions(boardId: string): Promise<BoardVersion[]> {
try {
const response = fetch(`${BOARD_INDEX_URL}/${boardId}/${BOARD_VERSIONS_JSON}`);
const res = await response;
const data = await res.json();
const parsed = z.array(BoardVersionSchema).safeParse(data);
if (!parsed.success) {
console.error("Failed to parse board versions:", z.prettifyError(parsed.error));
return [];
}
return parsed.data;
} catch (e) {
console.error(e);
return [];
}
}

export function getBoardVersionFirmwareTarUrl(boardId: string, version: string): string {
return `${BOARD_INDEX_URL}/${boardId}/${boardId}-${version}.tar.gz`;
}
8 changes: 3 additions & 5 deletions packages/firmware/src/esp32/esp32.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,7 @@ class UploadReporter {
}

export async function flash(Package: Package, path: string, noErase: boolean): Promise<void> {
const config = Package.getManifest().getConfig();

const flashBaud = parseInt(config["flashBaud"] ?? 921600);
const config = Package.getManifest().config;

const partitions = config["partitions"];
if (!partitions) {
Expand All @@ -107,7 +105,7 @@ export async function flash(Package: Package, path: string, noErase: boolean): P
const loaderOptions: any = {
debugLogging: false,
transport: new NodeTransport(port),
baudrate: flashBaud,
baudrate: config.flashBaud || 921600,
romBaudrate: 115200,
terminal: {
clean: () => {},
Expand Down Expand Up @@ -194,7 +192,7 @@ export async function flash(Package: Package, path: string, noErase: boolean): P
}

export function info(Package: Package): string {
const config = Package.getManifest().getConfig();
const config = Package.getManifest().config;

let output = "Chip type: " + config["chip"] + "\n";
if (config["flashBaud"]) {
Expand Down
Loading