From cf5274bfdcc0e0143ae9632492be4fbded4af283 Mon Sep 17 00:00:00 2001 From: Kevin Py Date: Thu, 2 Jul 2026 21:46:31 +0200 Subject: [PATCH 1/3] fix: improve security and docs --- app/src/lib/config.ts | 13 +- docs/plugins.md | 236 +++++++++++++++++++-- firmware/include/TimeService.h | 1 + firmware/src/PluginRegistry.cpp | 7 +- firmware/src/TimeService.cpp | 14 +- firmware/src/main.cpp | 2 +- plugins/weather/firmware/WeatherPlugin.cpp | 6 +- 7 files changed, 250 insertions(+), 29 deletions(-) diff --git a/app/src/lib/config.ts b/app/src/lib/config.ts index e393c2f..c6c9ce9 100644 --- a/app/src/lib/config.ts +++ b/app/src/lib/config.ts @@ -327,12 +327,15 @@ export function buildBudpyConfig(input: BudpyConfigInput): BudpyConfig { const fittedCells = fitCellsToOrientation(input.cells, input.orientation); const pageCount = getLayoutPageCount(fittedCells); const backgroundColor = normalizeColorValue(input.backgroundColor); - const firstConfig = fittedCells[0]?.config ?? {}; - const locale = (firstConfig.locale as SupportedLocale) ?? "fr-FR"; + const localeValue = fittedCells.find( + (cell) => typeof cell.config.locale === "string", + )?.config.locale; + const locale = (localeValue as SupportedLocale) ?? "fr-FR"; + const timezoneValue = fittedCells.find( + (cell) => typeof cell.config.timezone === "string", + )?.config.timezone; const timezone = - typeof firstConfig.timezone === "string" - ? firstConfig.timezone - : "Europe/Paris"; + typeof timezoneValue === "string" ? timezoneValue : "Europe/Paris"; const cells = fittedCells.map( ({ instanceId: _instanceId, diff --git a/docs/plugins.md b/docs/plugins.md index d382700..227ab1e 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -5,6 +5,14 @@ Budpy plugins are part of this repository. To add one, create a new directory in plugins dynamically from external repositories at runtime; the plugin must be compiled into the firmware before the ESP32 is flashed. +A plugin has two halves that share one `manifest.json`: + +- **Web app**: the manifest describes the widget (name, default size, config + fields) so the layout editor can render its settings form automatically. No + JavaScript is required. +- **Firmware**: a small PlatformIO library with a C++ render function that + draws the widget on the ESP32 screen. + ## Repository Layout Start from the template when creating a plugin: @@ -19,13 +27,27 @@ complete real plugin: ```txt plugins// - manifest.json - library.json + manifest.json # Plugin description for the web app + firmware registry + library.json # PlatformIO library manifest firmware/ - .cpp - .h + .cpp # Render (and optional touch/tick) functions + .h # Declarations, includes PluginRuntime.h ``` +## Step By Step + +1. Copy `templates/plugin` to `plugins/`. +2. Edit `manifest.json`: set `id`, `displayName`, `description`, + `defaultSize`, `capabilities`, `configFields`, and the `firmware` entry + points. +3. Edit `library.json`: rename the library and adjust dependencies. +4. Rename and implement the firmware sources in `firmware/`. +5. Run `pnpm generate:plugins` to refresh the generated registries. +6. Run `pnpm --filter @budpy/app test` and `pnpm firmware:build`. +7. Open a pull request with the plugin directory and the generated files. + +## manifest.json + `manifest.json` describes the plugin for the web app and declares the firmware entry points used by the generated firmware registry: @@ -51,9 +73,107 @@ entry points used by the generated firmware registry: } ``` -The plugin id must be unique and match `/^[a-z][a-z0-9-]*$/`. -`firmware.path` points to the PlatformIO library root inside the plugin -directory and defaults to `.`. +Field reference: + +- `id`: unique, must match `/^[a-z][a-z0-9-]*$/`. +- `version`, `displayName`, `description`: non-empty strings shown in the web + app plugin list. +- `defaultSize.colSpan` / `defaultSize.rowSpan`: integers from 1 to 4. The + layout grid is 3×4 in portrait and 4×3 in landscape; sizes are clamped to + the active grid. +- `capabilities`: array of `time`, `network`, `ha`, `touch`. Informational + tags describing what the plugin uses. +- `firmware.type`: always `platformio-library`. +- `firmware.path`: PlatformIO library root inside the plugin directory, + defaults to `.`. Must stay inside the plugin directory. +- `firmware.include`: header included by the generated registry. +- `firmware.renderFunction`: C++ identifier of the render function. +- `firmware.needsSecondTicksFunction` (optional): C++ identifier of the + seconds-tick predicate (see below). +- `firmware.handleTouchFunction` (optional): C++ identifier of the touch + handler (see below). + +The manifest schema lives in `packages/plugin-sdk/src/manifest.ts` and the +manifests are validated again by `scripts/generate-plugin-registry.mjs`. + +### Config Fields + +`configFields` drives the settings form in the web app. Each entry produces +one input, and the resulting values are stored in the cell config that the +firmware receives as JSON: + +```json +{ + "key": "label", + "label": "Label", + "type": "text", + "defaultValue": "Hello Budpy", + "placeholder": "Hello Budpy" +} +``` + +- `key`: config key, also the JSON key the firmware reads. +- `label`: input label in the web app. +- `type`: one of `text`, `number`, `boolean`, `select`, `color`. +- `defaultValue` (optional): string, number, or boolean applied when the + widget is added to the layout. +- `options` (for `select`): array of `{ "label": ..., "value": ... }` string + pairs. +- `min`, `max`, `step` (for `number`): input constraints. +- `maxLength` (for `text`): maximum input length (defaults to 64 in the UI). +- `placeholder`, `description` (optional): input hints. +- `disabledWhen` (optional): `{ "field": "otherKey", "equals": value }` greys + the field out when another field has a given value. + +Tip: keep `select` option values (and their `defaultValue`) as strings — the +firmware side typically parses them with `doc["key"].as()`. + +`text` and `color` fields can also reference global variables (`$var:KEY`) +managed in the web app's Variables dialog. References are resolved by the web +app before the config is sent, so the firmware always receives literal values. + +### Setting Groups + +For plugins with many fields, `settingGroups` organises the form into +collapsible sections (see `plugins/ha-sensor/manifest.json`): + +```json +"settingGroups": [ + { "title": "Connection", "fieldKeys": ["haUrl", "haToken"] }, + { "title": "Style", "fieldKeys": ["titleFont", "titleColor"] } +] +``` + +Each group lists the `configFields` keys it contains, in display order. An +optional `tab` string splits groups across tabs. Fields not referenced by any +group land in an automatic "More" section. + +## library.json + +`library.json` is a standard PlatformIO library manifest. The build points at +the `firmware/` directory: + +```json +{ + "name": "BudpyExamplePlugin", + "version": "0.1.0", + "description": "Budpy example plugin firmware library", + "frameworks": "arduino", + "platforms": "espressif32", + "dependencies": { + "bblanchon/ArduinoJson": "^7.4.3", + "bodmer/TFT_eSPI": "^2.5.43" + }, + "build": { + "srcDir": "firmware", + "includeDir": "firmware" + } +} +``` + +Give each plugin a unique library `name`. Declare only the dependencies your +plugin actually uses (`HTTPClient` and `WiFiClient*` come with the Arduino +ESP32 core and need no entry). ## Firmware Contract @@ -68,12 +188,71 @@ void renderExamplePlugin(PluginRenderContext& context); `PluginRenderContext` gives the plugin access to: - `context.renderer` for drawing screen content. -- `context.config` for device-level config. -- `context.cell` for the widget position, size, plugin id, and raw `configJson`. +- `context.config` for device-level config (grid size, timezone, locale, + background color, the full cell list). +- `context.cell` for the widget position, size, plugin id, and raw + `configJson`. +- `context.cellIndex` for the cell's index (useful to key per-cell state, see + Redraws below). - `context.timeInfo` for the current local time. +- `context.forceClear` — `true` when the whole screen was just cleared and the + plugin must repaint everything. -Plugin-specific settings are available as JSON in `context.cell.configJson`. -Keep this parsing aligned with the fields declared in `manifest.json`. +### Reading the cell config + +Plugin-specific settings are available as a JSON string in +`context.cell.configJson`. Parse it with ArduinoJson and keep the keys aligned +with the fields declared in `manifest.json`: + +```cpp +JsonDocument doc; +if (deserializeJson(doc, context.cell.configJson) == DeserializationError::Ok) { + const char* label = doc["label"] | "Hello Budpy"; +} +``` + +Always provide fallbacks — every field is optional in the stored config. + +### Cell geometry + +The screen is divided into a `config.cols` × `config.rows` grid. Compute the +cell rectangle from the cell's grid coordinates: + +```cpp +int16_t gridCoordinate(int16_t size, uint8_t position, uint8_t divisions) { + if (divisions == 0) return 0; + return static_cast((static_cast(size) * position) / divisions); +} + +const int16_t cellLeft = gridCoordinate(renderer.width(), cell.col, config.cols); +const int16_t cellRight = gridCoordinate(renderer.width(), cell.col + cell.colSpan, config.cols); +const int16_t cellTop = gridCoordinate(renderer.height(), cell.row, config.rows); +const int16_t cellBottom = gridCoordinate(renderer.height(), cell.row + cell.rowSpan, config.rows); +``` + +Draw only inside this rectangle; other cells share the same screen. + +### Rendering rules + +- The runtime calls the render function about once per minute, or once per + second when any plugin's `needsSecondTicks` predicate returns `true` — and + immediately after a touch interaction. Expect frequent calls and make + repeated renders cheap. +- The available fonts are `1` (small), `2` (medium), and `4` (large); only + those are compiled into the firmware. Built-in fonts render ASCII + 0x20–0x7E only, so sanitise strings from the network (see `toAscii` in + `plugins/ha-sensor/firmware/HaSensorPlugin.cpp`). +- When `context.forceClear` is `false`, the screen still shows your previous + frame. Either repaint the full cell (`renderer.fillRect(cellLeft, cellTop, + ...)` with the background color) or skip drawing when nothing changed. The + common pattern is a small per-cell render cache keyed by `context.cellIndex` + storing the last drawn value plus the cell rectangle — see + `plugins/weather/firmware/WeatherPlugin.cpp`. +- Blocking work (HTTP requests) runs on the render path, so cache results and + refresh them on an interval; see the `findOrCreateCache` pattern in the + `weather`, `get-data`, and `ha-sensor` plugins. + +### Seconds ticks If the plugin needs to refresh every second, expose a function like: @@ -81,7 +260,24 @@ If the plugin needs to refresh every second, expose a function like: bool examplePluginNeedsSecondTicks(const AppConfig& config); ``` -Return `false` for static plugins. +Return `true` only when one of the plugin's cells actually needs per-second +rendering (for example, the clock returns `true` only when a clock cell shows +seconds). Return `false` for static plugins, or omit the function entirely. + +### Touch + +Declare `firmware.handleTouchFunction` in the manifest (and add the `touch` +capability) to receive taps on the plugin's cell: + +```cpp +void examplePluginHandleTouch(PluginTouchContext& context); +``` + +`PluginTouchContext` provides `config`, `cell`, `cellIndex`, and a +`showNextPage(config)` callback that switches the display to the next layout +page (used by the `next` plugin). After the handler runs, the runtime forces a +full re-render. See `plugins/ha-cover` for a handler that calls a Home +Assistant service, including debounce handling. ## Generation Script @@ -98,17 +294,19 @@ This scans `plugins/*/manifest.json`, validates each manifest, and generates: - `firmware/src/generated/PluginRegistrations.cpp`. - `firmware/generated/plugins.ini` for PlatformIO `lib_deps`. +Commit the regenerated files together with the plugin. + `pnpm firmware:build` runs `pnpm generate:plugins` before compiling, so firmware builds also refresh the generated plugin registry and dependencies. -## Adding A Plugin +## Testing A Plugin -1. Copy `templates/plugin` to `plugins/`. -2. Update `manifest.json`, `library.json`, and firmware sources. -3. Run `pnpm generate:plugins`. -4. Run `pnpm --filter @budpy/app test` and `pnpm firmware:build`. -5. Open a pull request with the plugin directory and generated files. +1. `pnpm generate:plugins` — must pass manifest validation. +2. `pnpm --filter @budpy/app test` and `pnpm typecheck` — web app checks. +3. `pnpm firmware:build` — compiles the firmware including your plugin. +4. Flash the device, add the widget in the web app, and send the + configuration over USB serial. The ESP32 will reject a layout with `Unknown pluginId` if the plugin is present in the web app but the device was flashed with an older firmware that does not -include that plugin. Rebuild and flash the firmware after new plugins are merged. \ No newline at end of file +include that plugin. Rebuild and flash the firmware after new plugins are merged. diff --git a/firmware/include/TimeService.h b/firmware/include/TimeService.h index 3316969..d4339a8 100644 --- a/firmware/include/TimeService.h +++ b/firmware/include/TimeService.h @@ -6,6 +6,7 @@ bool connectWifiAndSyncTime(const AppConfig& config, String& error); bool getLocalTimeParts(struct tm& timeInfo); +bool isNativelySupportedTimezone(const String& timezone); bool getTimePartsForTimezone(const String& timezone, struct tm& timeInfo); bool getTimePartsForUtcOffsetMinutes(int16_t offsetMinutes, struct tm& timeInfo); \ No newline at end of file diff --git a/firmware/src/PluginRegistry.cpp b/firmware/src/PluginRegistry.cpp index 3255fc0..41817c8 100644 --- a/firmware/src/PluginRegistry.cpp +++ b/firmware/src/PluginRegistry.cpp @@ -144,8 +144,13 @@ void renderPlugins(Renderer& renderer, const AppConfig& config, } struct tm cellTimeInfo; + // Named timezones keep DST rules, so only fall back to the fixed UTC + // offset captured by the web app when the timezone is not known natively. + const bool useUtcOffset = cell.pluginId == "clock" && + cell.clock.hasTimezoneOffsetMinutes && + !isNativelySupportedTimezone(cell.clock.timezone); const bool hasCellTime = - cell.pluginId == "clock" && cell.clock.hasTimezoneOffsetMinutes + useUtcOffset ? getTimePartsForUtcOffsetMinutes(cell.clock.timezoneOffsetMinutes, cellTimeInfo) : getTimePartsForTimezone(cell.pluginId == "clock" diff --git a/firmware/src/TimeService.cpp b/firmware/src/TimeService.cpp index f4c2e9b..7db0fd5 100644 --- a/firmware/src/TimeService.cpp +++ b/firmware/src/TimeService.cpp @@ -9,7 +9,8 @@ namespace { constexpr uint32_t WIFI_CONNECT_TIMEOUT_MS = 20000; constexpr uint32_t NTP_SYNC_TIMEOUT_MS = 15000; -const char* timezoneToPosix(const String& timezone) { +// Returns the POSIX TZ string for natively supported timezones, or nullptr. +const char* findPosixTimezone(const String& timezone) { if (timezone == "Europe/Paris") { return "CET-1CEST,M3.5.0/2,M10.5.0/3"; } @@ -39,7 +40,12 @@ const char* timezoneToPosix(const String& timezone) { return "AEST-10AEDT,M10.1.0/2,M4.1.0/3"; } - return "UTC0"; + return nullptr; +} + +const char* timezoneToPosix(const String& timezone) { + const char* posixTimezone = findPosixTimezone(timezone); + return posixTimezone != nullptr ? posixTimezone : "UTC0"; } bool readTimePartsForPosixTimezone(const char* posixTimezone, @@ -102,6 +108,10 @@ bool getLocalTimeParts(struct tm& timeInfo) { return getLocalTime(&timeInfo, 250); } +bool isNativelySupportedTimezone(const String& timezone) { + return findPosixTimezone(timezone) != nullptr; +} + bool getTimePartsForTimezone(const String& timezone, struct tm& timeInfo) { return readTimePartsForPosixTimezone(timezoneToPosix(timezone), timeInfo); } diff --git a/firmware/src/main.cpp b/firmware/src/main.cpp index 536c5f5..7ef7c5f 100644 --- a/firmware/src/main.cpp +++ b/firmware/src/main.cpp @@ -278,7 +278,7 @@ void loop() { const uint32_t now = millis(); if (runtimeReady && renderer != nullptr && - now - lastRenderMs >= RENDER_INTERVAL_MS) { + (renderDirty || now - lastRenderMs >= RENDER_INTERVAL_MS)) { lastRenderMs = now; renderPlugins(*renderer, appConfig, renderDirty); renderDirty = false; diff --git a/plugins/weather/firmware/WeatherPlugin.cpp b/plugins/weather/firmware/WeatherPlugin.cpp index 1814c43..2d96adb 100644 --- a/plugins/weather/firmware/WeatherPlugin.cpp +++ b/plugins/weather/firmware/WeatherPlugin.cpp @@ -550,9 +550,13 @@ String dayLabel(uint8_t dayOffset) { String weatherDayDisplayKey(const WeatherDay& day) { return String(static_cast(day.minTemp)) + ":" + String(static_cast(day.maxTemp)) + ":" + + String(static_cast(day.minFeelsLike)) + ":" + + String(static_cast(day.maxFeelsLike)) + ":" + String(static_cast(day.humidity)) + ":" + + String(static_cast(day.pressure)) + ":" + String(day.maxWindSpeed, 0) + ":" + String(day.windDeg) + ":" + - String(day.totalRain, 1) + ":" + String(static_cast(day.maxPop)); + String(day.totalRain, 1) + ":" + String(static_cast(day.maxPop)) + + ":" + String(static_cast(day.avgClouds)); } const char* windDirectionStr(uint16_t deg) { From da1dc7bf5884ab0a96c8cbdaa76ced2fb44aefb3 Mon Sep 17 00:00:00 2001 From: Kevin Py Date: Thu, 2 Jul 2026 21:55:17 +0200 Subject: [PATCH 2/3] chore: add open source community files and CI workflow --- .github/ISSUE_TEMPLATE/bug_report.yml | 72 ++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 5 ++ .github/ISSUE_TEMPLATE/feature_request.yml | 29 +++++++ .github/ISSUE_TEMPLATE/plugin_request.yml | 37 ++++++++ .github/PULL_REQUEST_TEMPLATE.md | 30 +++++++ .github/workflows/ci.yml | 60 +++++++++++++ CODE_OF_CONDUCT.md | 73 ++++++++++++++++ CONTRIBUTING.md | 98 ++++++++++++++++++++++ LICENSE | 21 +++++ README.md | 17 +++- SECURITY.md | 31 +++++++ 11 files changed, 472 insertions(+), 1 deletion(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/ISSUE_TEMPLATE/plugin_request.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/ci.yml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 SECURITY.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..2d06d9f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,72 @@ +name: Bug report +description: Report something that does not work as expected. +title: "[Bug]: " +labels: ["bug"] +body: + - type: dropdown + id: area + attributes: + label: Area + description: Which part of Budpy is affected? + options: + - Web app (layout editor / provisioning) + - Firmware (ESP32 device) + - A specific plugin + - Documentation + - Other / not sure + validations: + required: true + - type: input + id: plugin + attributes: + label: Plugin + description: If a specific plugin is affected, which one? + placeholder: e.g. clock, weather, ha-sensor + - type: textarea + id: what-happened + attributes: + label: What happened? + description: A clear description of the bug. + placeholder: What did you see? + validations: + required: true + - type: textarea + id: expected + attributes: + label: What did you expect to happen? + validations: + required: true + - type: textarea + id: steps + attributes: + label: Steps to reproduce + placeholder: | + 1. Open the web app + 2. Add a plugin ... + 3. Send the config to the ESP32 ... + validations: + required: true + - type: input + id: version + attributes: + label: Budpy version + description: Version shown in the web app footer, or firmware version flashed on the device. + placeholder: e.g. 1.2.0 + - type: input + id: device + attributes: + label: Device + description: For firmware issues, which board are you using? + placeholder: e.g. ESP32-2432S028R (CYD) + - type: input + id: browser + attributes: + label: Browser + description: For web app issues (Web Serial requires Chrome or Edge). + placeholder: e.g. Chrome 130 on macOS + - type: textarea + id: logs + attributes: + label: Logs / screenshots + description: Serial monitor output, browser console errors, or screenshots. + render: shell diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..c11c265 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Question or discussion + url: https://github.com/pyxel-dev/budpy/discussions + about: Ask questions and discuss ideas with the community. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..d898982 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,29 @@ +name: Feature request +description: Suggest an improvement or new capability for Budpy. +title: "[Feature]: " +labels: ["enhancement"] +body: + - type: textarea + id: problem + attributes: + label: What problem does this solve? + description: Describe the need or frustration behind the request. + validations: + required: true + - type: textarea + id: solution + attributes: + label: Proposed solution + description: What would you like to happen? + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Other approaches or workarounds you have thought about. + - type: textarea + id: context + attributes: + label: Additional context + description: Mockups, links, or examples from other projects. diff --git a/.github/ISSUE_TEMPLATE/plugin_request.yml b/.github/ISSUE_TEMPLATE/plugin_request.yml new file mode 100644 index 0000000..7df70ed --- /dev/null +++ b/.github/ISSUE_TEMPLATE/plugin_request.yml @@ -0,0 +1,37 @@ +name: Plugin request +description: Suggest a new plugin (widget) for the Budpy dashboard. +title: "[Plugin]: " +labels: ["enhancement", "plugin"] +body: + - type: input + id: name + attributes: + label: Plugin name + placeholder: e.g. Calendar, Stock ticker, Air quality + validations: + required: true + - type: textarea + id: description + attributes: + label: What should it display? + description: Describe the information shown on the screen and how it should look. + validations: + required: true + - type: textarea + id: data-source + attributes: + label: Data source + description: Where does the data come from? (public API, Home Assistant entity, device-local, ...) Include API links if relevant. + validations: + required: true + - type: dropdown + id: contribution + attributes: + label: Are you willing to implement it? + description: Plugins are self-contained — see docs/plugins.md for the guide. + options: + - Yes, I plan to open a PR + - Maybe, with some guidance + - No, just suggesting the idea + validations: + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..bcffbad --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,30 @@ +# Description + + + +## Related issue + + + +## Type of change + + + +- [ ] Bug fix +- [ ] New feature +- [ ] New plugin +- [ ] Documentation +- [ ] Refactoring / maintenance + +## How was this tested? + + + +## Checklist + +- [ ] `pnpm test` passes +- [ ] `pnpm typecheck` passes +- [ ] `pnpm generate:plugins` was run and the generated files are committed (if plugins/manifests changed) +- [ ] `pnpm firmware:build` compiles (if firmware or plugins changed) +- [ ] Tested on a physical device (if firmware behavior changed) +- [ ] Documentation updated (if needed, e.g. `docs/plugins.md`) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1f818b2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,60 @@ +name: CI + +on: + pull_request: + push: + branches: + - develop + +permissions: + contents: read + +jobs: + web: + name: Web app & SDK + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + run_install: false + + - uses: actions/setup-node@v4 + with: + node-version: 24 + + - run: pnpm install --frozen-lockfile + + - name: Check generated plugin registry is up to date + run: | + pnpm generate:plugins + git diff --exit-code plugins/generated firmware/include/generated firmware/src/generated firmware/generated + + - run: pnpm typecheck + - run: pnpm test + - run: pnpm build + + firmware: + name: Firmware + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + run_install: false + + - uses: actions/setup-node@v4 + with: + node-version: 24 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install PlatformIO CLI + run: python -m pip install --upgrade pip platformio + + - run: pnpm install --frozen-lockfile + - run: pnpm firmware:build diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..def2c6b --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,73 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances + of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, + without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for +moderation decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the project maintainer through +[pyxel.dev](https://pyxel.dev) or by opening a private report on GitHub. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..4b24093 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,98 @@ +# Contributing to Budpy + +Thanks for your interest in contributing! This document explains how to set up +the project, the workflow for changes, and what is expected in a pull request. + +## Ways to contribute + +- **Report a bug** — open a [bug report](https://github.com/pyxel-dev/budpy/issues/new/choose). +- **Suggest a feature or plugin** — open a feature or plugin request issue. +- **Improve documentation** — typos, clarifications, and examples are welcome. +- **Write code** — fix a bug, improve the web app or firmware, or add a plugin. + +For anything non-trivial, please open an issue first so the approach can be +discussed before you invest time in an implementation. + +## Project structure + +```txt +app/ Web app (React + Vite): layout editor, flashing, provisioning +packages/plugin-sdk/ Shared TypeScript schemas (manifest + device config) +firmware/ ESP32 firmware (PlatformIO, Arduino framework) +plugins/ Plugins: one directory per plugin (manifest + firmware) +templates/plugin/ Starter template for new plugins +scripts/ Repo tooling (plugin registry generation, publishing) +docs/ Documentation +``` + +## Prerequisites + +- [Node.js](https://nodejs.org/) >= 25 +- [pnpm](https://pnpm.io/) 11.5.2 (`corepack enable` will pick the right version) +- [PlatformIO](https://platformio.org/) CLI — only needed for firmware builds + +## Development setup + +```sh +git clone https://github.com/pyxel-dev/budpy.git +cd budpy +pnpm install + +# Web app dev server +pnpm --filter @budpy/app dev + +# Tests and type checking (all workspaces) +pnpm test +pnpm typecheck + +# Regenerate plugin registries after touching plugins/*/manifest.json +pnpm generate:plugins + +# Build the firmware (runs generate:plugins first) +pnpm firmware:build +``` + +## Creating a plugin + +Plugins are the most self-contained way to contribute. Follow the guide in +[docs/plugins.md](docs/plugins.md) — it covers the manifest, the firmware +contract, and the testing checklist. + +## Pull request workflow + +1. Fork the repository and create a branch from `develop` + (e.g. `feat/my-plugin` or `fix/clock-dst`). +2. Make your changes. Keep PRs focused — one bug fix or feature per PR. +3. Make sure the checks pass locally: + - `pnpm test` + - `pnpm typecheck` + - `pnpm firmware:build` (if firmware or plugins changed) + - Commit the regenerated files from `pnpm generate:plugins` (if manifests changed) +4. For firmware changes, test on a real ESP32-2432S028R (CYD) when possible and + say so in the PR description. +5. Open a pull request against `develop` and fill in the template. + +## Commit messages + +The project follows [Conventional Commits](https://www.conventionalcommits.org/): + +```txt +feat: add air quality plugin +fix: prevent stale weather render cache +docs: clarify plugin touch contract +``` + +## Coding style + +- **TypeScript/React**: match the existing code style; keep modules small and + covered by Vitest tests where logic is involved (`*.test.ts`). +- **C++ firmware**: the repo has a `.clang-format`; keep functions in unnamed + namespaces when they are file-local, and prefer the patterns used by + existing plugins (config parsing with fallbacks, per-cell render caches). +- Text rendered on the TFT display must be ASCII (0x20–0x7E); sanitise + network strings. + +## License + +By contributing, you agree that your contributions will be licensed under the +[MIT License](LICENSE). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..adda4d8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Pyxel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 2266f22..b96b003 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,21 @@ You are the possibility to create multiple pages, and to switch between them. Yo See [docs/plugins.md](docs/plugins.md) for plugin development instructions. +## Contributing + +Contributions are welcome! Read the [contributing guide](CONTRIBUTING.md) to +get started — it covers the development setup, the pull request workflow, and +how to create a plugin. Please also review the +[code of conduct](CODE_OF_CONDUCT.md). + +- Found a bug? [Open a bug report](https://github.com/pyxel-dev/budpy/issues/new/choose) +- Have an idea? Open a feature or plugin request +- Security issue? See the [security policy](SECURITY.md) + ## 3D print -You can find a lot of 3D printable cases for the ESP32 CYD on [Thingiverse](https://www.thingiverse.com/search?q=esp32+cyd&type=things&sort=relevant). I recommend the [Aura case](https://makerworld.com/fr/models/1382304-aura-smart-weather-forecast-display). \ No newline at end of file +You can find a lot of 3D printable cases for the ESP32 CYD on [Thingiverse](https://www.thingiverse.com/search?q=esp32+cyd&type=things&sort=relevant). I recommend the [Aura case](https://makerworld.com/fr/models/1382304-aura-smart-weather-forecast-display). + +## License + +Budpy is released under the [MIT License](LICENSE). \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..8d7a163 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,31 @@ +# Security Policy + +## Supported versions + +Only the latest release of Budpy (web app and firmware) receives security +fixes. Please update to the most recent version before reporting. + +## Reporting a vulnerability + +Please **do not open a public issue** for security vulnerabilities. + +Instead, report it privately via +[GitHub Security Advisories](https://github.com/pyxel-dev/budpy/security/advisories/new). + +Include: + +- A description of the vulnerability and its impact +- Steps to reproduce or a proof of concept +- The affected area (web app, firmware, a specific plugin) + +You should receive an acknowledgement within a few days. Once the issue is +confirmed and fixed, the fix will be released and the advisory published. + +## Scope notes + +- Budpy stores WiFi credentials and API tokens (e.g. Home Assistant) in plain + text on the device filesystem and in the browser's localStorage. This is a + known design constraint of the platform, not a reportable vulnerability — + only flash devices and use tokens you control. +- Firmware HTTPS requests currently skip certificate validation + (`setInsecure()`), which is a known limitation of the ESP32 TLS setup. From d9124256fe55cf46c45ef866ceb0d4bc34625ea6 Mon Sep 17 00:00:00 2001 From: Kevin Py Date: Thu, 2 Jul 2026 21:59:02 +0200 Subject: [PATCH 3/3] 1.2.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ce78f7d..f356a76 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "budpy", - "version": "1.2.0", + "version": "1.2.1", "description": "Budpy is an autonomous ESP32 CYD dashboard.", "private": true, "packageManager": "pnpm@11.5.2",