143 Commits
v2.6.5 ... main

Author SHA1 Message Date
Wessel
0108494bd8 Channel bandwidth is kHz, not MHz (#983) 2025-12-08 23:03:41 -05:00
rj-xy
3ecea59da8 Update README with Buf CLI installation instructions (#981)
Added instructions for installing the Buf CLI.
2025-12-04 22:59:37 -05:00
Dan Ditomaso
0b2fdb6439 Revert "feat(ui): add SNR, RSSI, hops info for messages (#963)" (#974)
This reverts commit 020e9d6b63.
2025-12-02 10:10:19 -05:00
Dan Ditomaso
94220e729b feat(map): add heatmap layer (#969)
* feat: add heatmap layer

* Update packages/web/src/components/PageComponents/Map/Layers/HeatmapLayer.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update packages/web/src/components/PageComponents/Map/Tools/MapLayerTool.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update packages/web/src/pages/Map/index.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update packages/web/src/pages/Map/index.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update packages/web/src/components/PageComponents/Map/Layers/HeatmapLayer.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* lint/formatting fixes

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-28 19:58:05 -05:00
Alexey Stepanov
020e9d6b63 feat(ui): add SNR, RSSI, hops info for messages (#963)
* feat(ui): add SNR, RSSI, hops, MQTT info for messages

* review fixes

* zeros for new fields

* Move label under the message

---------

Co-authored-by: Pmmlabs <meshtastic@pmmlabs.ru>
2025-11-27 15:42:11 -05:00
Kamil Dzieniszewski
6a7be99a6a feat: add fixed position coordinate picker (#909)
* chore: remove unused logo SVG files

* feat: add interactive fixed position picker with map interface

- Created new FixedPositionPicker component with clickable map for setting device coordinates
- Added form field type for fixed position picker that appears when fixedPosition toggle is enabled
- Implemented position request functionality to retrieve current device location

* feat: display altitude unit based on user's display settings

- Added dynamic altitude unit (Meters/Feet) that respects the user's imperial/metric display preference
- Updated altitude field description to show the appropriate unit instead of hardcoded "Meters"

* refactor: replace any type with MapLayerMouseEvent in map click handler

* refactor: improve accessibility and code quality in FixedPositionPicker

- Replace hardcoded IDs with useId() hook for proper accessibility
- Use Number.isNaN() instead of isNaN() for more reliable type checking
- Add radix parameter to parseInt() and remove unnecessary fragment wrapper

* refactor: simplify fixed position picker integration

- Removed dedicated FixedPositionPicker form field type in favor of toggle's additionalContent prop
- Moved FixedPositionPicker to render conditionally within toggle field instead of as separate dynamic field
- Streamlined form field types by eliminating FixedPositionPickerFieldProps

* style: format code with consistent line breaks and import ordering

* refactor: simplify fixed position picker container styling

* feat: disable fixed position toggle when GPS is enabled

* refactor: use ComponentRef instead of ElementRef in Switch component

* refactor: replace interactive map picker with inline coordinate fields for fixed position

- Removed FixedPositionPicker component with map interface
- Added latitude, longitude, and altitude fields directly to position form
- Moved coordinate validation into PositionValidationSchema with proper min/max bounds
- Updated translation strings to include coordinate ranges and improved altitude description
- Coordinates now sent via setFixedPosition admin message on form submit when fixedPosition is enabled

* refactor: simplify toggle field by removing additionalContent prop and unused field spreading

- Removed additionalContent prop and its JSDoc documentation from ToggleFieldProps
- Removed rendering of additionalContent below toggle switch
- Cleaned up Controller render function by removing unused rest spread operator
- Renamed field destructuring to controllerField for clarity

* refactor: improve fixed position handling and add position broadcast request

- Restructure onSubmit to save config before sending admin message
- Add position broadcast request after setting fixed position to immediately update display
- Add comprehensive debug logging throughout submission flow
- Extract coordinate exclusion logic earlier in submission process for clarity
- Add 1 second delay before requesting position broadcast to allow fixed position processing

* feat: add max length constraint to latitude and longitude fields

- Set fieldLength.max to 10 for both latitude and longitude inputs
- Prevents excessive decimal precision while maintaining 7 decimal places (±1.1cm accuracy)
2025-11-27 15:40:57 -05:00
Alexey Stepanov
5debdb4689 fix(ui): add "never" i18n string, fix "Favorite" tooltip (#965)
* fix(ui): add "never" i18n string, fix "Favorite" tooltip

* format

* format

---------

Co-authored-by: Pmmlabs <meshtastic@pmmlabs.ru>
2025-11-27 15:40:41 -05:00
dependabot[bot]
057043864f chore(deps-dev): bump happy-dom from 20.0.0 to 20.0.2 (#968)
Bumps [happy-dom](https://github.com/capricorn86/happy-dom) from 20.0.0 to 20.0.2.
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v20.0.0...v20.0.2)

---
updated-dependencies:
- dependency-name: happy-dom
  dependency-version: 20.0.2
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-27 14:59:28 -05:00
Kamil Dzieniszewski
8918603f96 fix(ui): correct typo in languagePicker key and adjust description width (#961)
Fixed typo 'languagePickeer' to 'languagePicker' in DeviceInfoPanel and corrected responsive width class from 'md:w-6' to 'md:w-5/6' in Connections page description.
2025-11-24 21:02:28 -05:00
github-actions[bot]
68cea2e858 chore(i18n): New Crowdin Translations by GitHub Action (#962)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-11-22 20:54:06 -05:00
Dan Ditomaso
06cc266acd fix(ui): removed internet hosted fonts from app (#955)
* fix(ui): removed internet hosted fonts from app

* Update packages/web/src/components/generic/Mono.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update packages/web/src/components/UI/Typography/Code.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* removed unsupported font extention

* formatter fix

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-19 17:52:26 -05:00
zeo
295755ec4c fix: interpolate longName and shortName in PKI backup download (#959)
This caused {{shortName}} and {{longName}} to appear unformatted in the
exported key files:

``` === MESHTASTIC KEYS FOR {{longName}} ({{shortName}}) ===

Private Key: <censored>

Public Key: <censored>

=== END OF KEYS === ```

The fix simply replicates the behaviour used elsewhere in
PKIIBackupDialog.
2025-11-18 18:04:40 -05:00
github-actions[bot]
b99057df69 chore(i18n): New Crowdin Translations by GitHub Action (#958)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-11-17 21:28:28 -05:00
Dan Ditomaso
390b46f026 fix(ui): add language switcher to connections page (#954)
* fix(ui): add language switcher to connections page

* desciption length fix

* Update packages/web/src/pages/Connections/index.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* add new language picker key

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-13 13:06:20 -05:00
jsacrist
94d51912e6 feat: add devcontainer (#953) 2025-11-13 12:37:11 -05:00
Dan Ditomaso
ac28fbc53d fix: removed duplicate images (#951) 2025-11-12 11:20:53 -05:00
Dan Ditomaso
648a9c3640 refactor: device connection logic, added nonce to get config only (#946)
* refactor: device connection logic, added nonce to get config only on connect.

* Update packages/web/src/core/services/MeshService.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update packages/web/src/pages/Connections/useConnections.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* code review fixes

* fixes from code review

* ui fixes

* refactored meshService, moved code into deviceStore. Fixed some connnection issues

* formatting fixes

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-11 20:56:22 -05:00
Dan Ditomaso
7f21b3b531 Add 'packages/web' to excluded directories (#947) 2025-11-09 20:45:24 -05:00
github-actions[bot]
d1597ce00f chore(i18n): New Crowdin Translations by GitHub Action (#941)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-11-09 20:38:31 -05:00
Jeremy Gallant
0c8fcec23d fix(i18n): Correct 'disconnected' typo in connections.json (#943)
* fix(i18n): Correct 'disconnected' typo in connections.json

* fix(i18n): Correct typo 'checkConnetion' to 'checkConnection'

* Fix(i18n): Correct typo in connection test description

---------

Co-authored-by: philon- <philon-@users.noreply.github.com>
2025-11-09 20:38:18 -05:00
Dan Ditomaso
2e03b4a413 fix(ui): fix add connection dialog typo (#938) 2025-11-06 12:41:04 -05:00
Dan Ditomaso
a00cb2098b feat(ui): match avatar color other platforms (#933)
* feat(ui): match avatar color other platforms

* Update packages/web/src/components/UI/Avatar.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update packages/web/src/components/DeviceInfoPanel.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-05 21:53:55 -05:00
Dan Ditomaso
6aeaed988e feat(docker): add arm v7 support (#934) 2025-11-05 21:53:41 -05:00
Dan Ditomaso
7c9013a217 fix(connection): support port on HTTP connection (#935) 2025-11-05 21:53:26 -05:00
Dan Ditomaso
ab0308701c fix(connections): ensure connections reflect actual status. (#930) 2025-11-04 16:09:30 -05:00
github-actions[bot]
e13d543e73 chore(i18n): New Crowdin Translations by GitHub Action (#924)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-11-04 15:23:20 -05:00
Dan Ditomaso
d8ad0efcdf fix(config): update change registry channel value (#929)
* fix(config): update change registry channel value

* format/linting
2025-11-04 15:12:35 -05:00
Dan Ditomaso
2e60af1e29 fix(ci): add ui library to excluded list (#928) 2025-11-03 21:59:40 -05:00
Dan Ditomaso
1af1295b8f feat(connections): Add connections page (replaces new device dialog) (#919)
* feat(conn): add connection screen and logic

* fixes from code review

* force https

* code review fixes

* add http for self testing

* enable deviceStore persistance

* added translations

* disabled feature flag

* i18n updates

* chore: add new folders to biome config (#910)

* chore(i18n): New Crowdin Translations by GitHub Action (#908)

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>

* fix: use correct deprecated GPS coordinate format enum (#917)

The Config_DisplayConfig_GpsCoordinateFormat export doesn't exist in the protobufs package. The correct export is Config_DisplayConfig_DeprecatedGpsCoordinateFormat, which matches what's used in the validation schema.

Added TODO comment explaining that this field is deprecated since protobufs 2.7.4 and should be migrated to DeviceUIConfig.gps_format when DeviceUI settings are implemented.

* style: fix line wrapping for GPS coordinate format enum (#918)

- Split long enum reference across multiple lines to improve code readability
- Maintains consistent code formatting standards without changing functionality

* fix(core): ensure core package works in browser (#923)

* fix(core): ensure core package works in browser

* style(core): revert new line removal

* fix: add @serialport/bindings-cpp to onlyBuiltDependencies (#914)

* feat(ui): Add UI library (#900)

* feat: scaffold UI library

* Update packages/ui/src/components/theme-provider.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* add lock file

* lint/formatting fixes

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* formatting/linting fixes

* fixed some paring logic

* fixed connection issue with serial

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Kamil Dzieniszewski <kamil.dzieniszewski@gmail.com>
Co-authored-by: Azarattum <43073346+Azarattum@users.noreply.github.com>
Co-authored-by: Ben Allfree <ben@benallfree.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-03 20:24:24 -05:00
Dan Ditomaso
a1a646983e fix: added skelton loader for message items (#927) 2025-11-03 19:08:53 -05:00
Dan Ditomaso
4386854e9d feat(ui): Add UI library (#900)
* feat: scaffold UI library

* Update packages/ui/src/components/theme-provider.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* add lock file

* lint/formatting fixes

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-31 14:37:34 -04:00
Ben Allfree
e821a65dd4 fix: add @serialport/bindings-cpp to onlyBuiltDependencies (#914) 2025-10-31 14:37:11 -04:00
Azarattum
3392c9db08 fix(core): ensure core package works in browser (#923)
* fix(core): ensure core package works in browser

* style(core): revert new line removal
2025-10-31 11:58:42 -04:00
Dan Ditomaso
e53edfb00f feat(state): enable deviceStore persistance (#922) 2025-10-31 08:26:21 -04:00
Kamil Dzieniszewski
1df63bb84b style: fix line wrapping for GPS coordinate format enum (#918)
- Split long enum reference across multiple lines to improve code readability
- Maintains consistent code formatting standards without changing functionality
2025-10-29 10:17:38 -04:00
Kamil Dzieniszewski
679f7986cc fix: use correct deprecated GPS coordinate format enum (#917)
The Config_DisplayConfig_GpsCoordinateFormat export doesn't exist in the protobufs package. The correct export is Config_DisplayConfig_DeprecatedGpsCoordinateFormat, which matches what's used in the validation schema.

Added TODO comment explaining that this field is deprecated since protobufs 2.7.4 and should be migrated to DeviceUIConfig.gps_format when DeviceUI settings are implemented.
2025-10-29 10:12:04 -04:00
github-actions[bot]
f375911c4a chore(i18n): New Crowdin Translations by GitHub Action (#908)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-10-28 11:19:36 -04:00
Dan Ditomaso
68ad535259 chore: add new folders to biome config (#910) 2025-10-28 01:54:06 -04:00
Dan Ditomaso
0cf677c8d5 Feat(config): Align settings menu to match android/ios (#906)
* feat: aligned settings menu to match android/ios

* updated sidebar text size.

* Update packages/web/public/i18n/locales/en/config.json

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update packages/web/public/i18n/locales/en/config.json

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update packages/web/src/components/PageComponents/Settings/User.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update packages/web/src/components/PageComponents/ModuleConfig/Telemetry.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* linting/formatting fixes

* fixed formatting issue

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-24 13:31:22 -04:00
Wessel
ff02b1455d Fix description for historyReturnWindow (#907)
It's not number of records but the time window...
2025-10-24 13:31:07 -04:00
Jeremy Gallant
cdad811295 Persists device and app stores across sessions (#860)
* Persistence for device and app data

* esphemeral -> ephemeral

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* devices -> app

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Additional waypoint methods, update mock, update tests

---------

Co-authored-by: philon- <philon-@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-23 17:27:41 -04:00
Dan Ditomaso
fc1e327b74 Update inactive issue workflow schedule and settings (#905)
* Update inactive issue workflow schedule and settings

* Update .github/workflows/inactive-issue.yml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-23 15:52:36 -04:00
dependabot[bot]
af2fac1465 chore(deps): bump vite from 7.1.9 to 7.1.11 (#903)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.1.9 to 7.1.11.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.1.11/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.1.11
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-23 12:29:39 -04:00
Dan Ditomaso
15daa0270c feat(ci): add CI workflow to automatically close issue after 60 days (#897)
* feat(ci): add CI workflow to automatically close issue after 60 days

* run once pe github issue to run once per day
2025-10-22 19:39:10 -04:00
Dan Ditomaso
80c9306db3 feat(i18n): add fr localization support (#902)
* feat: add fr support

* fix(i18n): ensure langs are sorted before being displayed.
2025-10-21 19:42:56 -04:00
Dan Ditomaso
c3f073a380 Update readme with new widgets (#901)
* chore: add new widgets to readme

* add docs url

* Update README.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-21 14:31:28 -04:00
github-actions[bot]
fe35376450 chore(i18n): New Crowdin Translations by GitHub Action (#899)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-10-21 08:39:30 -04:00
Dan Ditomaso
64d1f0f7aa fix(ui): logic on waypoint layer component caused 0 to be shown in UI (#896)
* fix(ui): logic on waypoint layer component caused 0 to be shown in UI

* Update packages/web/src/components/PageComponents/Map/Popups/WaypointDetail.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-17 21:28:17 -04:00
Dan Ditomaso
a61bb2dfb6 fix(actions): improve main to stable release workflow (#895)
* fix(actions): improve main to stable release workflow

* Update .github/workflows/update-stable-from-master.yml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update .github/workflows/update-stable-from-master.yml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-17 10:06:17 -04:00
Dan Ditomaso
acfa7d2269 Add badge indicator on layers icon. (#894)
* feat: indicate number of layers enabled.

* Update packages/web/src/components/PageComponents/Map/Tools/MapLayerTool.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: reduced code duplication in layers component

* fixed unread message bubble

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-16 09:56:32 -04:00
Dan Ditomaso
8f62fb7877 Show nodes on right sidebar on app load (#892)
* fix: show nodes on right sidebar on app load

* chore: update protobuf version to latest (#890)

* chore(deps-dev): bump happy-dom from 19.0.2 to 20.0.0 (#891)

Bumps [happy-dom](https://github.com/capricorn86/happy-dom) from 19.0.2 to 20.0.0.
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v19.0.2...v20.0.0)

---
updated-dependencies:
- dependency-name: happy-dom
  dependency-version: 20.0.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Update packages/web/src/core/stores/nodeDBStore/index.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-15 22:02:45 -04:00
Dan Ditomaso
94409dd8e1 Refactor CI workflow to remove Deno and add exclusions (#893)
* Refactor CI workflow to remove Deno and add exclusions

Removed Deno setup and caching from CI workflow. Added exclusion logic for specific package directories during the build process.

* Update excluded directories in CI workflow

Added 'packages/transport-deno' to the excluded directories.

* Refactor CI workflow for pnpm and build process
2025-10-15 21:57:44 -04:00
dependabot[bot]
ac25326d24 chore(deps-dev): bump happy-dom from 19.0.2 to 20.0.0 (#891)
Bumps [happy-dom](https://github.com/capricorn86/happy-dom) from 19.0.2 to 20.0.0.
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v19.0.2...v20.0.0)

---
updated-dependencies:
- dependency-name: happy-dom
  dependency-version: 20.0.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-15 16:14:02 -04:00
Dan Ditomaso
34bdbe4709 chore: update protobuf version to latest (#890) 2025-10-15 10:23:08 -04:00
Dan Ditomaso
8d98aca496 fix: updated module paths (#889) 2025-10-14 17:14:49 -04:00
Dan Ditomaso
ec9ad1d309 fix: remove buf schema registry from workflow (#888) 2025-10-14 15:24:41 -04:00
Dan Ditomaso
a97195c57e moar fixes (#887) 2025-10-14 14:29:43 -04:00
Dan Ditomaso
22a1d92191 Fix/more protobuf fixes (#886)
* fix: add pnpm to protobuf workflow

* fix: fixing pnpm issue
2025-10-14 13:43:34 -04:00
Dan Ditomaso
35596249b4 fix: add pnpm to protobuf workflow (#885) 2025-10-14 13:00:54 -04:00
Dan Ditomaso
566c588377 Fix protobuf github workflow (#884)
* fix: protobuf workflow

* update workflow name

* fixes
2025-10-14 10:44:33 -04:00
Dan Ditomaso
9ab49d1431 Add protobufs repo as git submodule (#882)
* feat: add protobufs git submodule

* fixes

* fixed publishing issue

* fixed lockfile
2025-10-14 08:54:55 -04:00
Kamil Dzieniszewski
f5a7132421 Update README.md (#881)
* Update README.md

* chore: rename CONTRIBUTIONS.md to CONTRIBUTING.md

- Rename packages/web/CONTRIBUTIONS.md to CONTRIBUTING.md to follow standard open source naming convention
- Update reference in packages/web/README.md to point to the renamed file
2025-10-14 07:12:03 -04:00
Dan Ditomaso
c66b315623 Restored user's node to ui (messages/node list) (#878)
* fix: restored own node to ui

* Update packages/web/src/pages/Messages.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update packages/web/src/core/stores/nodeDBStore/index.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-11 10:07:56 -04:00
Dan Ditomaso
76ba4e917e fix: removed en-GB from fallback (#879) 2025-10-11 10:07:40 -04:00
github-actions[bot]
d0da79bf56 chore(i18n): New Crowdin Translations by GitHub Action (#873)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-10-06 22:45:04 -04:00
Dan Ditomaso
66bf3f1578 Update web dependencies (#876)
* chore: update web depdencies

* update lock file

* chore: add lock file
2025-10-06 22:44:46 -04:00
github-actions[bot]
6a358883f9 chore(i18n): New Crowdin Translations by GitHub Action (#868)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-09-29 22:19:38 -04:00
Dan Ditomaso
86622f83d0 Add minimumReleaseAge to pnpm-workspace.yaml (#865)
* Add minimumReleaseAge to pnpm-workspace.yaml

This is an important addition to protect our software from any sort of supply chain attack. This feature was recently released in pnpm 10.17.x

* Add minimumReleaseAgeExclude to pnpm-workspace.yaml

Allow our own packages to be installed without any freshness check.
2025-09-26 18:34:20 -04:00
github-actions[bot]
66c3945bc4 chore(i18n): New Crowdin Translations by GitHub Action (#863)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-09-22 08:59:35 -04:00
Nullvoid3771
38cb002ce8 Update nightly.yml (#864)
+echo "all_tags=nightly ${IMMUTABLE}" >> "$GITHUB_OUTPUT"
2025-09-21 19:18:22 -04:00
Jeremy Gallant
36443fd838 Map improvements - node and neighbor display (#850)
* Improves map node and neighbor display

Updates the map page to enhance the visualization of nodes, waypoints, and neighbor connections.

Adds a map layer tool for toggling the visibility of direct/remote neighbors and position precision indicators.

Introduces clustering to improve map readability when multiple nodes share the exact same location.

Adds SNR lines to visually represent network connections between nodes

Co-Authored-By: jamon <jamon@users.noreply.github.com>

* Clean up

* Update packages/web/src/components/generic/TimeAgo.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update packages/web/src/components/PageComponents/Map/Layers/SNRLayer.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Improve dark mode and expires field

* Review fixes

Co-Authored-By: Dan Ditomaso <dan.ditomaso@gmail.com>

---------

Co-authored-by: philon- <philon-@users.noreply.github.com>
Co-authored-by: jamon <jamon@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Dan Ditomaso <dan.ditomaso@gmail.com>
2025-09-18 08:13:34 -04:00
Jeremy Gallant
8cc451546d Command Menu improvements (#857)
* Add dialogs for reset and clear actions + clear stores

* Catch failures

* Improve tests, don't reset store on failure

* Remove unnecessary i18n string

---------

Co-authored-by: philon- <philon-@users.noreply.github.com>
2025-09-18 08:13:19 -04:00
Jeremy Gallant
1bdd923365 Do not unregister form (#856)
Co-authored-by: philon- <philon-@users.noreply.github.com>
2025-09-17 18:34:19 +02:00
Jeremy Gallant
d5610de826 Update @bufbuild/protobuf to version 2.8.0 for web as well (#852)
Bumped @bufbuild/protobuf dependency from 2.6.0 to 2.8.0 in packages/web. Updated pnpm-lock.yaml accordingly to use the new version.

Co-authored-by: philon- <philon-@users.noreply.github.com>
2025-09-14 21:32:00 -04:00
Jeremy Gallant
aaf85943a8 Address new linting rules (#851)
Co-authored-by: philon- <philon-@users.noreply.github.com>
2025-09-14 21:31:47 -04:00
Dan Ditomaso
37ad0ac9eb removed all_tags from workflow (#854) 2025-09-14 21:27:05 -04:00
Dan Ditomaso
eb2791e206 Fix for nightly release (#853)
* fix: removed commas in tag name

* removed `all_tags` from workflow
2025-09-14 15:10:56 -04:00
github-actions[bot]
119cde65c7 chore(i18n): New Crowdin Translations by GitHub Action (#849)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-09-14 14:51:12 -04:00
Dan Ditomaso
cb66c22974 Fixes to github actions for latest tag (#848)
* fix: ensure latest tag works

* update release workflow as well

* Update .github/workflows/nightly.yml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-13 16:07:13 -04:00
Henri Bergius
96c5d0d86f Create PR instead of trying to push to protected branch (#842) 2025-09-13 15:56:01 -04:00
Dan Ditomaso
2d98f57b65 fix: update-lock-file (#847) 2025-09-13 11:54:21 -04:00
Jeremy Gallant
3e2fe721d3 Improvements to node filtering and storage (#839)
* Improves node filtering and voltage display

Ensures voltage values are always displayed as positive.

Enhances node filtering logic to handle unknown or undefined values, preventing nodes from being unexpectedly hidden.

Updates UI labels for clarity.

Improves Nodes page responsiveness by debouncing node updates and optimizing selector usage, preventing unnecessary re-renders.

Adds validation warnings for key conflicts between nodes.

* Update packages/web/src/core/stores/nodeDBStore/nodeDBStore.test.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update packages/web/src/core/stores/nodeDBStore/nodeValidation.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Revert copilot suggestion

* Review changes

---------

Co-authored-by: philon- <philon-@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-12 17:21:13 +02:00
Dan Ditomaso
52fd2f712c chore: update web deps (#845)
* chore: update web deps

* Bumped library package version (#843)

* chore: bumped package versions

* removed jsr file
2025-09-11 22:41:56 -04:00
Dan Ditomaso
ad6b9f470f Bumped library package version (#843)
* chore: bumped package versions

* removed jsr file
2025-09-11 22:14:32 -04:00
Dan Ditomaso
765f672bf5 Improve the nightly and release workflow (#836)
* fix: improve the nightly and release workflow

* Update .github/workflows/nightly.yml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix from code review

* add type module to node-serial

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-11 15:34:04 -04:00
dependabot[bot]
67a5d75283 chore(deps): bump vite from 7.1.1 to 7.1.5 (#841)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.1.1 to 7.1.5.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.1.5/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.1.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-11 14:05:55 -04:00
Henri Bergius
4ff010e14b Get protobufs directly from NPM (#840)
* Get protobufs directly from NPM

* Bump dep

* Update lockfile too
2025-09-11 11:13:45 -04:00
Jeremy Gallant
1214b6ecf6 Allow default overrideFrequency (#838)
* Allow overrideFrequency == 0
* Fix typo

---------

Co-authored-by: philon- <philon-@users.noreply.github.com>
2025-09-10 20:33:32 +02:00
Dan Ditomaso
aababb8075 Dialog abstraction using DialogWrapper (#830)
* feat: add dialog abstraction system

* removed unneeded file

* fix formatting

* linting fixes
2025-09-10 13:56:45 -04:00
dependabot[bot]
3b0406c5af chore(deps): bump vite from 7.1.1 to 7.1.5 (#837)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.1.1 to 7.1.5.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.1.5/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.1.5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-10 13:56:12 -04:00
github-actions[bot]
6772f066ee chore(i18n): New Crowdin Translations by GitHub Action (#835)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-09-07 21:10:35 -04:00
Dan Ditomaso
d8df40816f fix: added directive to remove service worker (#834) 2025-09-07 13:14:24 -04:00
Brad
f9bd61af49 Fixed overrideFrequency Input Constraints (#801)
* Fixed overrideFrequency to properly allow for floats with a minimum of 410 MHz and maximum of 930 MHz. Added 3 decimal point check.

* Removed duplicate error message in FormInput.tsx

* Added error message for overrideFrequency in several locales

* Simplified check and reverted translated languages to English besides German. Also fixed a typo in src/components/UI/Input.tsx.

* Let i18n handle fallback + linting

---------

Co-authored-by: philon- <jeremy@gallant.se>
2025-09-04 18:22:06 -04:00
Henri Bergius
bcdda8b751 Connection robustness improvements (#813)
* Clear heartbeat and queue when disconnected

* Give clearer error in case configure fails due to a lost connection. Used to throw 'Packet does not exist'

* If the queue processing error is due to a lost connection, throw it instead of looping endlessly

* In case we send a disconnection event we don't need to also throw

* Catch heartbeat errors

* Also handle invalid state errors

* Handle socket timeouts

* Log heartbeat failures

* Make linter happy

* Transform stream being a singleton prevented reconnection attempts

* Adapt tests to not using singleton

* Aborting already ends the connection
2025-09-04 13:08:05 -04:00
Jeremy Gallant
dcb44d27fe Persistent message store (#814)
* Fix default filter behaviour

* Persist message store

* messageStore tests, node PKI validation

Implement node validation and improve merging logic

- Added `validateIncomingNode` function to validate new nodes against existing nodes, checking for public key conflicts and ensuring proper handling of node updates.
- Updated `nodeDBFactory` to utilize the new validation function when adding nodes.
- Enhanced `getNodes` method to optionally include the current node in the results.
- Removed the `mergeNodeInfo` utility as its functionality is now integrated into the validation and merging process.
- Updated tests to cover new validation logic and ensure correct behavior during node addition and merging.
- Cleaned up unused utility functions related to key comparison.

* refactor: reuse eviction logic for message and node stores

* Update format, move hooks

* Improve test performance

* Update imports

---------

Co-authored-by: philon- <philon-@users.noreply.github.com>
Co-authored-by: Dan Ditomaso <dan.ditomaso@gmail.com>
2025-09-03 21:00:48 -04:00
Dan Ditomaso
799283fd46 fix: added latest tag, added conditionals if prerelease (#832) 2025-09-03 17:09:54 -04:00
Dan Ditomaso
d0f4939a4d Added lint rule to enforce importing with extensions (#831)
* fix: added lint rule to enforce importing with extentions

* fix test import path

* added import file extentions
2025-09-03 17:07:00 -04:00
Jeremy Gallant
d66f10e715 Fix rename seperator again (#828)
* Reapply "Seperator -> Separator (#823)" (#827)

This reverts commit e421eb4244.

* Rename new seperator

* Update packages/web/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update packages/web/src/components/Dialog/RebootDialog.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update packages/web/src/components/PageComponents/Map/NodeDetail.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update packages/web/src/pages/Dashboard/index.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: philon- <philon-@users.noreply.github.com>
Co-authored-by: Dan Ditomaso <dan.ditomaso@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-02 22:02:36 -04:00
Dan Ditomaso
e421eb4244 Revert "Seperator -> Separator (#823)" (#827)
This reverts commit 3582499a3c.
2025-09-02 09:41:48 -04:00
Jeremy Gallant
ce16bf5aa4 Chat message date display (#824)
* Date handling

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: philon- <philon-@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-01 23:00:05 -04:00
Jeremy Gallant
3582499a3c Seperator -> Separator (#823)
Co-authored-by: philon- <philon-@users.noreply.github.com>
2025-09-01 21:57:51 -04:00
Jeremy Gallant
1946000d14 New channel config (#807)
* Channel config rework

Add staged channel config with tabbed UI, import/export workflow, and global form state refactor

* Improve import dialog config comparison and UI labels

* Review fixes

* Improve state handling

* Fix default filter behaviour

---------

Co-authored-by: philon- <philon-@users.noreply.github.com>
2025-09-01 09:07:34 -04:00
Jeremy Gallant
01fa030ef9 Fix default filter behaviour (#820)
Co-authored-by: philon- <philon-@users.noreply.github.com>
2025-08-31 21:44:22 -04:00
Jeremy Gallant
90cf136b8c Update test format (#821)
* Lint tests, format JSON

* Update test formatting

---------

Co-authored-by: philon- <philon-@users.noreply.github.com>
2025-08-31 21:30:11 -04:00
Dan Ditomaso
ee131c3681 fix: added working directory to build step (#819) 2025-08-31 16:22:43 -04:00
github-actions[bot]
85accf7c25 chore(i18n): New Crowdin Translations by GitHub Action (#815)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-08-31 16:05:23 -04:00
Jeremy Gallant
91426a89e5 Lint tests, format JSON (#818) 2025-08-31 16:04:34 -04:00
Dan Ditomaso
d7e492cdc9 fix: removed comma from tag name (#817) 2025-08-31 13:09:50 -04:00
Dan Ditomaso
c5cad1dca6 fix incorrect usage of tags (#811) 2025-08-28 20:56:54 -04:00
Henri Bergius
85aa7a37b9 Methods for setting/removing fixed position (#802)
* Method for setting fixed position

* Implement removeFixedPosition

* Fix formatting
2025-08-27 18:46:14 -04:00
Henri Bergius
b87fad23d8 Catch packet decoding errors (#809)
* Catch decoding errors, refs #808

* Catch packet handling errors

* Add missing type
2025-08-25 16:59:35 -04:00
Jeremy Gallant
68ec7ee5d8 Persistent nodedb (#780)
* Refactor  and consolitdate store imports

- Created a new index file in the core stores directory to export all stores from a single module.
- Updated imports to use consolidated store exports.

* Remove unnecessary import

* Update imports

* First steps to persist nodeDB

* Use named exports

* Change store import after merge

* Persistent nodeDB initial work

* Key mishmatch warning, new serialization handler

* Minor copilot changes

* Add NODEDB_RETENTION_NUM

* Updated tests

* Refactor PKI mismatch logic

* Clear persisted db on reset

* Only persist on featureFlag

* Mock featureFlag in tests

---------

Co-authored-by: philon- <philon-@users.noreply.github.com>
2025-08-24 09:23:20 -04:00
github-actions[bot]
e33463b371 chore(i18n): New Crowdin Translations by GitHub Action (#806)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-08-23 20:45:31 -04:00
Jeremy Gallant
d4b80cf90b Re-add toaster component (#804) 2025-08-23 20:45:22 -04:00
Jeremy Gallant
d7e32e9b03 Add transport status events (#790)
* Transport status events

Add symbol docs
Emit transport status events
Transport test suite

* Review fixes

* Remove core dependency

* HTTP transport use AbortSignal, error handling in TransportNode

* Improve stream handling

* Update packages/transport-web-serial/src/transport.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix linting

---------

Co-authored-by: philon- <philon-@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-23 20:44:03 -04:00
Dan Ditomaso
449fb3ac36 Added feature flags system (#803)
* added feature flag system

* Update packages/web/src/core/services/featureFlags.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* remove process.env

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-22 13:47:24 -04:00
Dan Ditomaso
1de92cd2e9 Replace workspace formatter/linting tool (#799)
* replaced workspace formatter/linting tool

* format / linting fixes
2025-08-22 12:30:05 -04:00
Dan Ditomaso
fdd3e7f971 re-order package steps (#798) 2025-08-20 12:19:26 -04:00
Dan Ditomaso
6eece7172a bumped web-serial version (#797) 2025-08-20 11:53:12 -04:00
Ben Meadors
5b2c25d8ee Try-fix webserial disconnect not actually disconnecting (#796)
* Try-fix webserial disconnects

* Instantiate abort controller on new connection
2025-08-20 11:01:17 -04:00
Dan Ditomaso
40c2bfa535 Add pnpm install to release step (#795)
* more github changes

* moar changes
2025-08-19 14:50:54 -04:00
Dan Ditomaso
a10023fad6 more github changes (#794) 2025-08-19 13:19:28 -04:00
Dan Ditomaso
0a1afa988e update order of steps in nightly github action (#793) 2025-08-19 11:33:00 -04:00
Dan Ditomaso
97b884f119 add lock file (#792) 2025-08-18 09:19:34 -04:00
Dan Ditomaso
eb9b7159d4 Refactor github actions with monorepo support (#783)
* refactor github actions with monorepo support

* Update .github/workflows/release-packages.yml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update .github/workflows/release-packages.yml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* updates

* changed order of ci/pr steps

* Update .github/workflows/release-web.yml

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* adding lock file

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-18 09:07:13 -04:00
Dan Ditomaso
65a53388bb Fix docker nginx config (#786)
* Fix docker-nginx-config sub directory issue

* Fix docker-nginx-config sub directory issue

* Update packages/web/infra/default.conf

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update packages/web/infra/default.conf

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* adding lock file

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-18 09:06:48 -04:00
Jeremy Gallant
2ca3eb5685 Missing validation strings (#791)
Partially revert 43143bf

Co-authored-by: philon- <philon-@users.noreply.github.com>
2025-08-18 09:03:15 -04:00
Dan Ditomaso
a4c817cd30 Added contribution guidelines doc (#788)
* added contribution guidelines doc

* Update packages/web/CONTRIBUTIONS.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-17 14:32:10 -04:00
github-actions[bot]
d5b03c09db chore(i18n): New Crowdin Translations by GitHub Action (#784)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-08-17 11:43:18 -04:00
Henri Bergius
0b9ebade38 Initial Node.js serial transport (#779)
* Initial Node.js serial transport

* Minor doc fixes

* Add serialport to lockfile

* Typo fix

* Fix link:
2025-08-14 21:56:34 -04:00
Dan Ditomaso
ee1758a548 Add DFU mode to command menu (#781)
* feat: add dfu mode to command menu

* Update packages/web/src/components/CommandPalette/index.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-13 21:24:45 -04:00
Dan Ditomaso
a2a45ac898 Update release-web.yml (#777) 2025-08-11 13:55:24 -04:00
Dan Ditomaso
59d172765d Update release-web.yml (#776) 2025-08-11 13:48:35 -04:00
Jeremy Gallant
d453ff809a Refactor and consolidate store imports (#774)
* Refactor  and consolitdate store imports

- Created a new index file in the core stores directory to export all stores from a single module.
- Updated imports to use consolidated store exports.

* Remove unnecessary import

* Update imports

* Use named exports

* Change store import after merge

---------

Co-authored-by: philon- <philon-@users.noreply.github.com>
2025-08-11 12:36:33 -04:00
Dan Ditomaso
ed0a99dbd9 Update release-web.yml (#775)
added adhoc runs.
2025-08-11 11:20:48 -04:00
Jeremy Gallant
32f31cb502 Add client notification (#771)
* ClientNotification WIP

* Test

* ClientNotification WIP

* Add client notification dialog and related functionality

* Update ClientNotificationDialog.tsx

---------

Co-authored-by: philon- <philon-@users.noreply.github.com>
2025-08-10 21:59:03 -04:00
Jeremy Gallant
176d554ef9 Improve NodeDetailsDialog UI and add security info (#770)
* Improve NodeDetailsDialog UI and add security info

Refactored NodeDetailsDialog to use tables for better layout and readability, added a security section displaying public key and verification status, and included messageable status. Updated i18n files with new keys and improved battery level formatting. Fixed logic in Nodes page for handling location packets and improved hardware model sorting.

* Update NodeDetailsDialog.tsx
2025-08-10 21:58:26 -04:00
Jeremy Gallant
2735c37fad Fix Docker and CI builds (#773)
* Fix Docker and CI builds

* Fix indentation

* Fix release

---------

Co-authored-by: philon- <philon-@users.noreply.github.com>
2025-08-10 21:58:09 -04:00
Jeremy Gallant
f04ec36faf Update VSCode settings (#769)
Update extensions

Co-authored-by: philon- <philon-@users.noreply.github.com>
2025-08-10 07:50:51 -04:00
github-actions[bot]
28f0ca4337 chore(i18n): New Crowdin Translations by GitHub Action (#772)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-08-10 07:50:30 -04:00
Jeremy Gallant
1dbf0b07b6 refactor-ota-dialog (#768)
Co-authored-by: philon- <philon-@users.noreply.github.com>
2025-08-10 07:49:42 -04:00
Jeremy Gallant
27ed4e58bd Fix admin PKI validation (#766)
Admin PKI fields were falsely flagged as unchanged.

Co-authored-by: philon- <philon-@users.noreply.github.com>
2025-08-09 22:38:59 -04:00
Jeremy Gallant
a7f56c0bd5 Fix checkbox tests (#767)
Co-authored-by: philon- <philon-@users.noreply.github.com>
2025-08-09 22:37:31 -04:00
634 changed files with 74980 additions and 28782 deletions

View File

@@ -0,0 +1,13 @@
{
"name": "meshtastic-web",
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm",
"features": {
"ghcr.io/r3dpoint/devcontainer-features/tailwindcss-standalone-cli:1": {
"version": "latest"
},
"ghcr.io/devcontainers-extra/features/pnpm:2": {
"version": "latest"
}
}
}

View File

@@ -16,51 +16,64 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: latest
- name: Setup Deno
uses: denoland/setup-deno@v2
- name: Setup Node.js
uses: actions/setup-node@v4
with:
deno-version: v2.x
node-version: 22
cache: pnpm
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Cache Deno dependencies
uses: actions/cache@v4
with:
path: ~/.cache/deno
key: ${{ runner.os }}-deno-${{ hashFiles('**/deno.lock', '**/package.json') }}
restore-keys: |
${{ runner.os }}-deno-
- name: Prewarm & Install (workspace)
run: |
set -euo pipefail
pnpm fetch
pnpm install --frozen-lockfile --offline
- name: Cache pnpm dependencies
uses: actions/cache@v4
with:
path: |
~/.pnpm-store
packages/web/node_modules
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
- name: Build All Packages
- name: Build All Packages (with exclusions)
shell: bash
run: |
set -euo pipefail
for pkg_dir in packages/*/; do
pkg_dir=${pkg_dir%/} # Remove trailing slash
echo "🔍 Inspecting $pkg_dir..."
# List packages to exclude (full paths under repo root)
EXCLUDED_DIRS=("packages/protobufs" "packages/transport-deno" "packages/ui" "pacakges/web")
if [[ -f "$pkg_dir/package.json" ]] && [[ "$pkg_dir" != "packages/web" ]]; then
echo "🔧 Building with pnpm: $pkg_dir"
(cd "$pkg_dir" && pnpm install && pnpm run build:npm)
else
echo "⚠️ Skipping $pkg_dir (web package or no package.json)"
is_excluded() {
local dir="$1"
for ex in "${EXCLUDED_DIRS[@]}"; do
if [[ "$dir" == "$ex" ]]; then
return 0
fi
done
return 1
}
for pkg_dir in packages/*/; do
pkg_dir="${pkg_dir%/}" # trim trailing slash
# Must be a directory with a package.json
if [[ ! -f "$pkg_dir/package.json" ]]; then
echo "⚠️ Skipping $pkg_dir (no package.json)"
continue
fi
# Allow for exclusions
if is_excluded "$pkg_dir"; then
echo "🚫 Skipping $pkg_dir (excluded)"
continue
fi
# Optionally skip Deno-first packages automatically
if [[ -f "$pkg_dir/deno.json" || -f "$pkg_dir/deno.jsonc" ]]; then
echo "🦕 Skipping $pkg_dir (deno project)"
continue
fi
echo "🔧 Building: $pkg_dir"
# No per-package install needed; workspace install already done
pnpm -C "$pkg_dir" run build:npm
done

24
.github/workflows/inactive-issue.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: Close inactive issues
on:
schedule:
- cron: "0 */4 * * *" # every 4 hours
workflow_dispatch: # allow manual runs
jobs:
close-issues:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v10
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-issue-stale: 60
days-before-issue-close: 14
stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has been open for 60 days with no activity."
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
days-before-pr-stale: -1
days-before-pr-close: -1
only-issue-labels: "Bug"
remove-stale-when-updated: true

View File

@@ -1,101 +1,120 @@
name: "Nightly Release"
name: Nightly Release
on:
schedule:
- cron: "0 5 * * *" # Run every day at 5am UTC
- cron: "0 5 * * *" # 05:00 UTC daily
workflow_dispatch: {} # allow manual runs too
permissions:
contents: write
contents: read
packages: write
env:
REGISTRY_IMAGE: ghcr.io/${{ github.repository }}
jobs:
nightly-build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: latest
- name: Cache pnpm dependencies
uses: actions/cache@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
path: |
~/.pnpm-store
packages/web/node_modules
key: ${{ runner.os }}-pnpm-${{ hashFiles('packages/web/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
node-version: 22
cache: pnpm
cache-dependency-path: '**/pnpm-lock.yaml'
# - name: Run tests
# working-directory: packages/web
# run: deno task test
- name: Install dependencies (root)
run: pnpm install --frozen-lockfile
- name: Install Dependencies
working-directory: packages/web
run: pnpm install
- name: Run tests
run: pnpm run test
- name: Build Package
- name: Build web package
working-directory: packages/web
run: pnpm run build
- name: Package Output
- name: Package output
working-directory: packages/web
run: pnpm run package
- name: Archive compressed build
- name: Upload compressed build (artifact)
uses: actions/upload-artifact@v4
with:
name: build
name: web-build-nightly
path: packages/web/dist/build.tar
if-no-files-found: error
- name: Get latest release version
id: get_release
- name: Compute tags and labels
id: meta
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
LATEST_TAG=$(curl -sL \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r ".tag_name")
# Fallback to a default if no release is found
if [ -z "$LATEST_TAG" ]; then
LATEST_TAG="2.6.0"
set -euo pipefail
DATE="$(date -u +%Y%m%d)"
ISO_CREATED="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
SHORTSHA="$(git rev-parse --short=12 HEAD)"
# Try to use latest release tag if it exists; fallback to package version; else date
LATEST_TAG="$(gh release view --json tagName --jq .tagName 2>/dev/null || true)"
if [ -z "$LATEST_TAG" ] && [ -f packages/web/package.json ]; then
LATEST_TAG="v$(jq -r .version packages/web/package.json)"
fi
echo "tag=${LATEST_TAG}" >> $GITHUB_OUTPUT
if [ -n "${LATEST_TAG:-}" ] && [ "$LATEST_TAG" != "vnull" ]; then
IMMUTABLE="nightly-${LATEST_TAG}-${SHORTSHA}"
else
IMMUTABLE="nightly-${DATE}-${SHORTSHA}"
fi
# Outputs
echo "moving_tag=nightly" >> "$GITHUB_OUTPUT"
echo "immutable_tag=${IMMUTABLE}" >> "$GITHUB_OUTPUT"
echo "all_tags=nightly ${IMMUTABLE}" >> "$GITHUB_OUTPUT"
echo "created=${ISO_CREATED}" >> "$GITHUB_OUTPUT"
echo "Resolved tags: nightly and ${IMMUTABLE}"
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Buildah Build
- name: Build Container Image (multi-arch)
id: build-container
uses: redhat-actions/buildah-build@v2
with:
containerfiles: |
./packages/web/infra/Containerfile
image: ${{ github.event.repository.full_name }}
tags: nightly-${{ steps.get_release.outputs.tag }}-${{ github.sha }}
image: ${{ env.REGISTRY_IMAGE }}
tags: |
${{ steps.meta.outputs.moving_tag }}
${{ steps.meta.outputs.immutable_tag }}
oci: true
platforms: linux/amd64, linux/arm64
platforms: linux/amd64,linux/arm64,linux/arm/v7
labels: |
org.opencontainers.image.source=${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.created=${{ steps.meta.outputs.created }}
- name: Push To Registry
- name: Push To GHCR
id: push-to-registry
uses: redhat-actions/push-to-registry@v2
with:
image: ${{ steps.build-container.outputs.image }}
tags: ${{ steps.build-container.outputs.tags }}
# Push the same tags used at build time:
tags: ${{ steps.meta.outputs.all_tags }}
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Print image url
run: echo "Image pushed to ${{ steps.push-to-registry.outputs.registry-paths }}"
- name: Print image URLs
run: |
echo "Moving tag: ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.moving_tag }}"
echo "Immutable tag: ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.immutable_tag }}"

View File

@@ -1,49 +1,52 @@
name: Pull Request CI
on: pull_request
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
permissions:
contents: write
packages: write
contents: read
concurrency:
group: pr-${{ github.event.pull_request.number }}-ci
cancel-in-progress: true
env:
CI: true
jobs:
build-and-package:
runs-on: ubuntu-latest
# Skip for draft PRs; remove this line if you want to run on drafts too
if: ${{ github.event.pull_request.draft == false }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: latest
- name: Install Dependencies
# Commands will run from 'packages/web'
working-directory: packages/web
run: pnpm install
- name: Cache pnpm dependencies
uses: actions/cache@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
path: |
~/.pnpm-store
packages/web/node_modules
key: ${{ runner.os }}-pnpm-${{ hashFiles('packages/web/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
node-version: 22
cache: pnpm
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run linter
run: pnpm run lint
- name: Check formatter
- name: Check formatter
run: pnpm run check
- name: Build Package
working-directory: packages/web
run: pnpm run build
- name: Run tests
run: pnpm run test
- name: Build web package
run: pnpm --filter "./packages/web" run build

View File

@@ -4,83 +4,145 @@ on:
workflow_dispatch:
inputs:
packages:
description: 'Packages to release (comma-separated, or "all" for all packages)'
description: 'Packages to release (comma-separated, or "all")'
required: false
default: 'all'
bump:
description: 'Semver bump (patch | minor | major)'
required: false
default: 'patch'
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write # we commit the bumped versions back
id-token: write # required for JSR OIDC
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
steps:
- name: Checkout code
uses: actions/checkout@v4
# --- Setup Node.js and pnpm ---
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: latest
# --- Setup Deno ---
- name: Setup Deno
uses: denoland/setup-deno@v2
- name: Setup Node.js
uses: actions/setup-node@v4
with:
deno-version: v2.x
node-version: 22
cache: pnpm
cache-dependency-path: '**/pnpm-lock.yaml'
registry-url: 'https://registry.npmjs.org'
# --- Cache pnpm Dependencies ---
- name: Cache pnpm Dependencies
uses: actions/cache@v4
with:
path: |
~/.pnpm-store
packages/web/node_modules
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
- name: Install dependencies
run: pnpm install --frozen-lockfile
# --- Cache Deno Dependencies ---
- name: Cache Deno Dependencies
uses: actions/cache@v4
with:
path: ~/.cache/deno
key: ${{ runner.os }}-deno-${{ hashFiles('**/deno.lock') }}
restore-keys: |
${{ runner.os }}-deno-
- name: Configure pnpm registry
run: pnpm config set registry https://registry.npmjs.org/
- name: Configure pnpm auth
run: pnpm config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}
- name: Publish packages to npm and JSR
- name: Resolve package list
id: pkgs
shell: bash
run: |
for dir in packages/*; do
echo "Processing $dir"
set -euo pipefail
if [ "${{ github.event.inputs.packages }}" = "all" ] || [ -z "${{ github.event.inputs.packages }}" ]; then
mapfile -t TARGETS < <(ls -d packages/* | grep -v 'packages/web')
else
IFS=',' read -ra TARGETS <<< "${{ github.event.inputs.packages }}"
TARGETS=("${TARGETS[@]/#/packages/}")
fi
printf '%s\n' "${TARGETS[@]}" | paste -sd, - > targets.txt
echo "list=$(cat targets.txt)" >> "$GITHUB_OUTPUT"
echo "Targets: $(cat targets.txt)"
cd $dir
# Build and publish to npm if package.json exists
if [ -f "package.json" ]; then
echo "Building and publishing $dir to npm..."
pnpm run build:npm
pnpm run publish:npm || echo "npm publish failed for $dir"
- name: Bump package.json versions (no git tag)
shell: bash
run: |
set -euo pipefail
BUMP="${{ github.event.inputs.bump }}"
IFS=',' read -ra TARGETS <<< "${{ steps.pkgs.outputs.list }}"
for dir in "${TARGETS[@]}"; do
if [ -f "$dir/package.json" ]; then
echo "Bumping $dir -> $BUMP"
(cd "$dir" && npm version "$BUMP" --no-git-tag-version --allow-same-version)
fi
pnpm run prepare:jsr
# Publish to JSR if jsr.json exists
if [ -f "jsr.json" ]; then
echo "Publishing $dir to jsr..."
deno publish || echo "JSR publish failed for $dir"
fi
cd - > /dev/null
done
- name: Generate jsr.json from package.json (pkg-to-jsr)
shell: bash
run: |
set -euo pipefail
IFS=',' read -ra TARGETS <<< "${{ steps.pkgs.outputs.list }}"
for dir in "${TARGETS[@]}"; do
if [ -f "$dir/package.json" ]; then
echo "Generating jsr.json for $dir"
pnpm dlx pkg-to-jsr --root "$dir"
# # Optional: show result
# jq -C . "$dir/jsr.json" || cat "$dir/jsr.json"
fi
done
- name: Commit version bumps
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add packages/*/package.json packages/*/jsr.json 2>/dev/null || true
if ! git diff --cached --quiet; then
git checkout -b version_bump
git commit -m "chore(release): bump package versions (${{ github.event.inputs.bump }})"
git push -u origin version_bump
gh pr create --title "Bump package versions" --body "Bump versions of NPM and JSR packages" --base main --head version_bump
echo "Pushed to branch version_bump"
else
echo "No changes to commit."
fi
- name: Build selected packages
shell: bash
run: |
set -euo pipefail
IFS=',' read -ra TARGETS <<< "${{ steps.pkgs.outputs.list }}"
for dir in "${TARGETS[@]}"; do
echo "Building $dir"
pnpm --filter "./$dir" run build
done
- name: Publish to JSR (OIDC)
shell: bash
run: |
set -euo pipefail
IFS=',' read -ra TARGETS <<< "${{ steps.pkgs.outputs.list }}"
for dir in "${TARGETS[@]}"; do
if [ -f "$dir/jsr.json" ]; then
echo "Publishing $dir to JSR via OIDC…"
( cd "$dir"
[ -d dist ] || pnpm run build
npx --yes jsr publish
)
fi
done
- name: Configure npm auth
run: |
pnpm config set //registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}
pnpm config set registry https://registry.npmjs.org/
- name: Publish to npm
shell: bash
run: |
set -euo pipefail
IFS=',' read -ra TARGETS <<< "${{ steps.pkgs.outputs.list }}"
for dir in "${TARGETS[@]}"; do
if [ -f "$dir/package.json" ]; then
echo "Publishing $dir to npm…"
( cd "$dir"
[ -d dist ] || pnpm run build
npm publish --access public
)
fi
done

143
.github/workflows/release-protobufs.yml vendored Normal file
View File

@@ -0,0 +1,143 @@
name: Create Protobuf Release for JSR
on:
workflow_dispatch:
inputs:
version:
description: "Version to publish (e.g. v1.2.3). Used when manually dispatching."
required: true
type: string
dry_run:
description: "Don't actually publish to JSR (passes --dry-run)."
required: false
type: boolean
default: false
permissions: write-all
env:
PROTOBUF_DIR: ./packages/protobufs
jobs:
codegen:
runs-on: ubuntu-24.04
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Show files exist
run: |
set -euxo pipefail
ls -la $PROTOBUF_DIR/packages/ts || true
cat $PROTOBUF_DIR/packages/ts/deno.json
- name: Determine VERSION
run: |
set -euo pipefail
if [ -n "${{ inputs.version }}" ]; then
VERSION="${{ inputs.version }}"
else
echo "No 'version' input. Provide inputs.version." >&2
exit 1
fi
STRIPPED="${VERSION#v}"
echo "VERSION=$STRIPPED" >> "$GITHUB_ENV"
echo "Resolved VERSION=$STRIPPED"
- name: Set Package Versions to current tag
working-directory: ${{ env.PROTOBUF_DIR }}/packages/ts
run: |
set -euxo pipefail
for f in deno.json; do
test -f "$f"
jq --arg version "${VERSION//\//-}" '
walk(
if type == "string" and test("__PACKAGE_VERSION__")
then sub("__PACKAGE_VERSION__"; $version)
else .
end
)
' "$f" > "$f.tmp"
mv "$f.tmp" "$f"
done
- name: Setup Buf
uses: bufbuild/buf-setup-action@main
with:
github_token: ${{ github.token }}
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: latest
- name: Install workspace dependencies
run: pnpm install --frozen-lockfile
- name: Generate code
run: pnpm --filter @meshtastic/protobufs-ws build
- name: Move generated .ts files and clean up
run: |
set -euxo pipefail
SRC_DIR="$PROTOBUF_DIR/packages/ts/dist/meshtastic"
DEST_DIR="$PROTOBUF_DIR/packages/ts/dist"
if [ -d "$SRC_DIR" ]; then
shopt -s nullglob
ts_files=("$SRC_DIR"/*.ts)
if [ ${#ts_files[@]} -gt 0 ]; then
mv "$SRC_DIR"/*.ts "$DEST_DIR"/
fi
rm -rf "$SRC_DIR"
else
echo "Source directory not found: $SRC_DIR" >&2
exit 1
fi
# Remove nanopb_pb.ts if present
if [ -f "$DEST_DIR/nanopb_pb.ts" ]; then
rm "$DEST_DIR/nanopb_pb.ts"
fi
- name: Copy license & README
run: |
cp LICENSE $PROTOBUF_DIR/packages/ts
cp README.md $PROTOBUF_DIR/packages/ts
- name: Upload TypeScript code
uses: actions/upload-artifact@v4
with:
name: ts_code
path: ${{ env.PROTOBUF_DIR }}/packages/ts
publish-jsr:
runs-on: ubuntu-24.04
needs: codegen
permissions:
contents: read
id-token: write
env:
DRY_RUN: ${{ inputs.dry_run }}
steps:
- name: Download TypeScript code
uses: actions/download-artifact@v4
with:
name: ts_code
- name: Set up Deno
uses: denoland/setup-deno@main
with:
deno-version: rc
- name: Publish to JSR
run: |
set -euxo pipefail
FLAGS="--unstable-sloppy-imports"
if [ "${DRY_RUN}" = "true" ]; then
FLAGS="$FLAGS --dry-run"
echo "Running publish in dry-run mode..."
fi
deno publish $FLAGS

View File

@@ -3,77 +3,158 @@ name: Release Web
on:
release:
types: [released, prereleased]
workflow_dispatch:
inputs:
ref:
description: "Git ref (branch, tag, or SHA) to build"
required: false
default: ""
tag_name:
description: "Tag to use for artifacts/images (defaults to <sha>)"
required: false
default: ""
attach_to_release:
description: "Upload build.tar to an existing GitHub Release (requires tag_name)"
required: false
type: boolean
default: false
permissions:
contents: write
packages: write
env:
REGISTRY_IMAGE: ghcr.io/${{ github.repository }}
jobs:
release-web:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ inputs.ref != '' && inputs.ref || github.ref }}
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
bun-version: latest
version: latest
- name: Cache Bun Dependencies
uses: actions/cache@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
path: |
~/.bun/install/cache
packages/web/node_modules
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}
restore-keys: |
${{ runner.os }}-bun-
node-version: 22
cache: pnpm
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run Web App Tests
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build web package
working-directory: packages/web
run: bun run test
run: pnpm run build
- name: Create Web App Release Archive
- name: Create release archive
working-directory: packages/web
run: bun run package
run: pnpm run package
- name: Upload Web App Archive
- name: Upload archive (artifact)
uses: actions/upload-artifact@v4
with:
name: web-build
if-no-files-found: error
path: packages/web/dist/build.tar
if-no-files-found: error
- name: Attach Web Archive to GitHub Release
run: gh release upload ${{ github.event.release.tag_name }} packages/web/dist/build.tar
- name: Attach archive to GitHub Release
if: ${{ github.event_name == 'release' || inputs.attach_to_release == true }}
uses: softprops/action-gh-release@v2
with:
files: packages/web/dist/build.tar
tag_name: ${{ github.event_name == 'release' && github.event.release.tag_name || inputs.tag_name }}
fail_on_unmatched_files: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Compute created timestamp & short SHA for labels/tags
- name: Compute meta
id: gen
shell: bash
run: |
echo "created=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
echo "shortsha=$(git rev-parse --short=12 HEAD)" >> "$GITHUB_OUTPUT"
# Build/tag matrix:
# - For release events: vX.Y.Z, semver helpers, and latest (only if NOT prerelease)
# - For prerelease events: vX.Y.Z and semver helpers (NO latest)
# - For manual runs:
# * if tag_name set: that raw tag
# * else: adhoc-<shortsha>
- name: Docker metadata (tags & labels)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY_IMAGE }}
labels: |
org.opencontainers.image.source=${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.created=${{ steps.gen.outputs.created }}
tags: |
# Full release (semantic tag)
type=raw,value=${{ github.event.release.tag_name }},enable=${{ github.event_name == 'release' }}
# Semver helpers (major, major.minor) for releases/prereleases
type=semver,pattern={{version}},value=${{ github.event.release.tag_name }},enable=${{ github.event_name == 'release' }}
type=semver,pattern={{major}}.{{minor}},value=${{ github.event.release.tag_name }},enable=${{ github.event_name == 'release' && github.event.release.prerelease == false }}
type=semver,pattern={{major}},value=${{ github.event.release.tag_name }},enable=${{ github.event_name == 'release' && github.event.release.prerelease == false }}
# Latest only for full releases (not prereleases)
type=raw,value=latest,enable=${{ github.event_name == 'release' && github.event.release.prerelease == false }}
# Manual: provided tag_name
type=raw,value=${{ inputs.tag_name }},enable=${{ github.event_name == 'workflow_dispatch' && inputs.tag_name != '' }}
# Manual: fallback to adhoc-<shortsha>
type=raw,value=adhoc-${{ steps.gen.outputs.shortsha }},enable=${{ github.event_name == 'workflow_dispatch' && inputs.tag_name == '' }}
# Always add immutable sha tag for traceability
type=raw,value=sha-${{ steps.gen.outputs.shortsha }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Build Container Image
id: build-container
uses: redhat-actions/buildah-build@v2
with:
containerfiles: |
./infra/Containerfile
image: ghcr.io/${{ github.repository }}
tags: latest, ${{ github.event.release.tag_name }}
oci: true
platforms: linux/amd64, linux/arm64
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
- name: Push Container to GHCR
id: push-to-registry
uses: redhat-actions/push-to-registry@v2
- name: Login to GHCR
uses: docker/login-action@v3
with:
image: ${{ steps.build-container.outputs.image }}
tags: ${{ steps.build-container.outputs.tags }}
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Output Image URL
run: echo "🖼️ Image pushed to ${{ steps.push-to-registry.outputs.registry-paths }}"
# Push images for:
# - release (released or prereleased)
# - manual with a tag_name (adhoc builds can still be pushed by name)
- name: Build and push image
uses: docker/build-push-action@v6
with:
context: .
file: ./packages/web/infra/Containerfile
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: ${{ github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.tag_name != '') }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
provenance: false
- name: Output image refs
if: ${{ github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.tag_name != '') }}
run: |
echo "🖼️ Pushed tags:"
echo "${{ steps.meta.outputs.tags }}"
echo
echo "Pull examples:"
if [[ "${{ github.event_name }}" == "release" && "${{ github.event.release.prerelease }}" == "false" ]]; then
echo " docker pull ${{ env.REGISTRY_IMAGE }}:latest"
fi
# Always available:
echo " docker pull ${{ env.REGISTRY_IMAGE }}:${{ github.event.release.tag_name || inputs.tag_name || format('adhoc-{0}', steps.gen.outputs.shortsha) }}"
echo " docker pull ${{ env.REGISTRY_IMAGE }}:sha-${{ steps.gen.outputs.shortsha }}"
- name: Explain no image push
if: ${{ !(github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.tag_name != '')) }}
run: echo " No image pushed (manual run without tag_name)."

View File

@@ -7,44 +7,89 @@ on:
permissions:
contents: write
concurrency:
group: update-stable-${{ github.run_id }}
cancel-in-progress: true
jobs:
update-stable-branch:
name: Update Stable Branch from Main
name: Update stable from latest release source
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
fetch-tags: true # IMPORTANT: we need tags to resolve the release commit
- name: Configure Git
- name: Configure Git author
run: |
git config user.name "GitHub Actions Bot"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Fetch latest main and stable branches
- name: Determine source SHA (prefer tag)
id: meta
shell: bash
run: |
git fetch origin main:main
git fetch origin stable:stable || echo "Stable branch not found remotely, will create."
set -euo pipefail
- name: Get latest main commit SHA
id: get_main_sha
run: echo "MAIN_SHA=$(git rev-parse main)" >> $GITHUB_ENV
TAG="${{ github.event.release.tag_name }}"
TARGET="${{ github.event.release.target_commitish || '' }}"
- name: Check out stable branch
run: |
if git show-ref --verify --quiet refs/heads/stable; then
git checkout stable
git pull origin stable # Sync with remote stable if it exists
else
echo "Creating local stable branch based on main HEAD."
git checkout -b stable ${{ env.MAIN_SHA }}
# Always ensure we have latest remote heads/tags
git fetch --tags --prune origin
SHA=""
SRC_DESC=""
# 1) Prefer the release tag commit
if [ -n "${TAG}" ] && git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
SHA="$(git rev-list -n1 "${TAG}")"
SRC_DESC="tag:${TAG}"
fi
- name: Reset stable branch to latest main
run: git reset --hard ${{ env.MAIN_SHA }}
# 2) Fall back to target_commitish if it points to a branch on origin
if [ -z "${SHA}" ] && [ -n "${TARGET}" ] && git ls-remote --exit-code --heads origin "${TARGET}" >/dev/null 2>&1; then
git fetch origin "${TARGET}:${TARGET}" --prune
SHA="$(git rev-parse "${TARGET}^{commit}")"
SRC_DESC="branch:${TARGET}"
fi
- name: Force push stable branch
run: git push origin stable --force
# 3) Fall back to main if present
if [ -z "${SHA}" ] && git ls-remote --exit-code --heads origin "main" >/dev/null 2>&1; then
git fetch origin "main:main" --prune
SHA="$(git rev-parse "main^{commit}")"
SRC_DESC="branch:main"
fi
if [ -z "${SHA}" ]; then
echo "::error::Unable to resolve source commit from tag (${TAG}), target_commitish (${TARGET}), or main."
exit 1
fi
echo "Using source: ${SRC_DESC}"
echo "sha=${SHA}" >> "$GITHUB_OUTPUT"
echo "source=${SRC_DESC}" >> "$GITHUB_OUTPUT"
- name: Create/reset local stable to SHA
shell: bash
run: |
set -euo pipefail
# Make sure we know if remote stable exists (non-fatal if not)
git fetch origin stable:refs/remotes/origin/stable || true
# Create or reset local stable in one command
git checkout -B stable "${{ steps.meta.outputs.sha }}"
git status --short --branch
- name: Push stable (force-with-lease)
run: |
# Safer than --force; refuses if remote moved unexpectedly (protects against races)
REMOTE_STABLE_SHA="$(git rev-parse refs/remotes/origin/stable || echo '')"
if [ -z "$REMOTE_STABLE_SHA" ]; then
# If remote stable doesn't exist, just use --force-with-lease=stable (no SHA)
git push origin stable:stable --force-with-lease=stable
else
# Use the specific SHA for maximum safety
git push origin stable:stable --force-with-lease=stable:$REMOTE_STABLE_SHA
fi

10
.gitignore vendored
View File

@@ -8,3 +8,13 @@ __screenshots__*
*.diff
npm/
.idea
**/LICENSE
.DS_Store
packages/protobufs/packages/ts/dist
.pnpm-store/
# Local dev certs
*.pem
*.crt
*.key

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "packages/packages/protobufs"]
path = packages/packages/protobufs
url = https://github.com/meshtastic/protobufs

View File

@@ -1,3 +1,3 @@
{
"recommendations": ["bradlc.vscode-tailwindcss", "denoland.vscode-deno"]
"recommendations": ["bradlc.vscode-tailwindcss", "biomejs.biome"]
}

View File

@@ -1,6 +1,10 @@
{
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit",
}
"source.fixAll.biome": "explicit"
},
"search.exclude": {
"**/i18n/locales/*-*/**": true
},
"vitest.workspaceConfig": "vitest.config.ts"
}

View File

@@ -12,6 +12,9 @@ This monorepo consolidates the official [Meshtastic](https://meshtastic.org) web
interface and its supporting JavaScript libraries. It aims to provide a unified
development experience for interacting with Meshtastic devices.
> [!NOTE]
> You can find the main Meshtastic documentation at https://meshtastic.org/docs/introduction/.
### Projects within this Monorepo (`packages/`)
All projects are located within the `packages/` directory:
@@ -21,17 +24,19 @@ All projects are located within the `packages/` directory:
- **[Hosted version](https://client.meshtastic.org)**
- **`packages/core`:** Core functionality for Meshtastic JS.
- **`packages/transport-node`:** TCP Transport for the NodeJS runtime.
- **`packages/transport-node-serial`:** NodeJS Serial Transport for the NodeJS runtime.
- **`packages/transport-deno`:** TCP Transport for the Deno runtime.
- **`packages/transport-http`:** HTTP Transport.
- **`packages/transport-web-bluetooth`:** Web Bluetooth Transport.
- **`packages/transport-web-serial`:** Web Serial Transport.
- **`packages/protobufs`:** Git submodule containing Meshtastics shared protobuf definitions, used to generate and publish the JSR protobuf package.
All `Meshtastic JS` packages (core and transports) are published both to
[JSR](https://jsr.io/@meshtastic). [NPM](https://www.npmjs.com/org/meshtastic)
---
## Stats
## Repository activity
| Project | Repobeats |
| :------------- | :-------------------------------------------------------------------------------------------------------------------- |
@@ -72,6 +77,9 @@ Follow the installation instructions on their home page.
```
This command installs all necessary dependencies for all packages within the
monorepo.
3. **Install the Buf CLI**
Required for building `packages/protobufs`
https://buf.build/docs/cli/installation/
### Running Projects
@@ -85,28 +93,18 @@ If you encounter any issues, please report them in our
[issues tracker](https://github.com/meshtastic/web/issues). Your feedback helps
improve the stability of future releases
### Contributing
## Star history
We welcome contributions! Heres how the deployment flow works for pull
requests:
<a href="https://star-history.com/#meshtastic/web&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=meshtastic/web&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=meshtastic/web&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=meshtastic/web&type=Date" width="100%" />
</picture>
</a>
- **Preview Deployments:**\
Every pull request automatically generates a preview deployment on Vercel.
This allows you and reviewers to easily preview changes before merging.
## Contributors
- **Staging Environment (`client-test`):**\
Once your PR is merged, your changes will be available on our staging site:
[client-test.meshtastic.org](https://client-test.meshtastic.org/).\
This environment supports rapid feature iteration and testing without
impacting the production site.
- **Production Releases:**\
At regular intervals, stable and fully tested releases are promoted to our
production site: [client.meshtastic.org](https://client.meshtastic.org/).\
This is the primary interface used by the public to connect with their
Meshtastic nodes.
Please review our
[Contribution Guidelines](https://github.com/meshtastic/web/blob/main/CONTRIBUTING.md)
before submitting a pull request. We appreciate your help in making the project
better!
<a href="https://github.com/meshtastic/web/graphs/contributors">
<img src="https://contrib.rocks/image?repo=meshtastic/web" width="100%"/>
</a>

View File

@@ -1,44 +1,75 @@
{
"files": {
"includes": ["**/*.ts", "**/*.tsx", "!**/*.test.ts", "!**/*.test.tsx", "!npm_modules/**", "!dist/**", "!npm/**"],
"ignoreUnknown": false
},
"formatter": {
"enabled": true,
"formatWithErrors": false,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 80,
"attributePosition": "auto"
},
"linter": {
"enabled": true,
"includes": ["**", "!test/**"],
"rules": {
"recommended": true,
"suspicious": {
"noExplicitAny": "error",
"noDebugger": "error"
},
"style": {
"useBlockStatements": "error",
"useSingleVarDeclarator": "off"
},
"correctness": {
"noUnusedVariables": "error",
"noUnusedImports": "error"
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "double",
"semicolons": "always"
}
},
"json": {
"formatter": {
"enabled": false
"files": {
"includes": [
"**/*.ts",
"**/*.tsx",
"!npm_modules",
"!**/dist",
"!**/protobufs",
"!**/.*",
"!npm",
"**/*.json",
"!**/locales/*-*/*.json",
"!**/packages/ui/"
],
"ignoreUnknown": false
},
"formatter": {
"enabled": true,
"formatWithErrors": false,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 80,
"attributePosition": "auto"
},
"linter": {
"enabled": true,
"includes": ["**", "!test/**", "!**/dist/**"],
"rules": {
"recommended": true,
"suspicious": {
"noExplicitAny": "error",
"noDebugger": "error"
},
"style": {
"useBlockStatements": "error",
"useSingleVarDeclarator": "off"
},
"correctness": {
"noUnusedVariables": "error",
"noUnusedImports": "error",
"useImportExtensions": "error"
}
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "double",
"semicolons": "always"
}
},
"json": {
"formatter": {
"enabled": true
}
},
"overrides": [
{
"includes": [
"**/*.test.ts",
"**/*.test.tsx",
"**/__tests__/**/*.{ts,tsx}"
],
"linter": {
"rules": {
"suspicious": {
"noExplicitAny": "off"
},
"style": {
"noNonNullAssertion": "off"
}
}
}
}
]
}

View File

@@ -1,42 +1,53 @@
{
"name": "meshtastic-web",
"version": "2.7.0-0",
"type": "module",
"description": "Meshtastic web client monorepo",
"license": "GPL-3.0-only",
"repository": {
"type": "git",
"url": "git+https://github.com/meshtastic/web.git"
},
"bugs": {
"url": "https://github.com/meshtastic/web/issues"
},
"homepage": "https://meshtastic.org",
"simple-git-hooks": {
"pre-commit": "pnpm run check:fix"
},
"scripts": {
"preinstall": "npx only-allow pnpm",
"lint": "biome lint",
"lint:fix": "biome lint --write",
"format": "biome format",
"format:fix": "biome format . --write",
"check": "biome check",
"check:fix": "biome check --write",
"build:all": "pnpm run --filter '*' build",
"clean:all": "pnpm run --filter '*' clean",
"publish:packages": "pnpm run --filter 'packages/transport-* packages/core' build"
},
"dependencies": {
"@bufbuild/protobuf": "^2.6.1",
"@meshtastic/protobufs": "npm:@jsr/meshtastic__protobufs@^2.7.0",
"ste-simple-events": "^3.0.11",
"tslog": "^4.9.3"
},
"devDependencies": {
"@types/node": "^22.16.4",
"biome": "^0.3.3",
"tsdown": "^0.13.4",
"typescript": "^5.8.3"
}
"name": "@meshtastic/web",
"version": "2.7.0-0",
"type": "module",
"description": "Meshtastic web client monorepo",
"license": "GPL-3.0-only",
"repository": {
"type": "git",
"url": "git+https://github.com/meshtastic/web.git"
},
"bugs": {
"url": "https://github.com/meshtastic/web/issues"
},
"homepage": "https://meshtastic.org",
"simple-git-hooks": {
"pre-commit": "pnpm run check:fix"
},
"scripts": {
"preinstall": "npx only-allow pnpm",
"lint": "biome lint",
"lint:fix": "biome lint --write",
"format": "biome format",
"format:fix": "biome format . --write",
"check": "biome check",
"check:fix": "biome check --write",
"build:all": "pnpm run --filter '*' build",
"clean:all": "pnpm run --filter '*' clean",
"publish:packages": "pnpm run build --filter 'packages/transport-*'",
"test": "vitest"
},
"dependencies": {
"@bufbuild/protobuf": "^2.9.0",
"@meshtastic/protobufs": "npm:@jsr/meshtastic__protobufs@^2.7.12-1",
"ste-simple-events": "^3.0.11",
"tslog": "^4.9.3"
},
"devDependencies": {
"@biomejs/biome": "2.2.4",
"@types/node": "^24.3.1",
"tsdown": "^0.15.0",
"typescript": "^5.9.2",
"vitest": "^3.2.4"
},
"pnpm": {
"onlyBuiltDependencies": [
"@serialport/bindings-cpp",
"@tailwindcss/oxide",
"core-js",
"esbuild",
"simple-git-hooks"
]
}
}

View File

@@ -1,7 +0,0 @@
{
"name": "@meshtastic/core",
"version": "2.6.6",
"exports": {
".": "./mod.ts"
}
}

View File

@@ -1,31 +1,50 @@
{
"name": "@meshtastic/core",
"version": "2.6.6",
"description": "Core functionalities for Meshtastic web applications.",
"version": "2.6.7",
"description": "Core functionalities for Meshtastic web applications.",
"exports": {
".": "./mod.ts"
},
"main": "./dist/mod.mjs",
"module": "./dist/mod.mjs",
"types": "./dist/mod.d.mts",
".": "./mod.ts"
},
"type": "module",
"main": "./dist/mod.js",
"module": "./dist/mod.js",
"types": "./dist/mod.d.ts",
"license": "GPL-3.0-only",
"tsdown": {
"entry": ["mod.ts"],
"entry": "mod.ts",
"platform": "browser",
"dts": true,
"format": ["esm"],
"format": [
"esm"
],
"splitting": false,
"clean": true
},
"jsrInclude": [
"mod.ts",
"src",
"README.md",
"LICENSE"
],
"jsrExclude": [
"src/**/*.test.ts"
],
"files": [
"package.json",
"README.md",
"LICENSE",
"dist"
],
"scripts": {
"preinstall": "npx only-allow pnpm",
"prepack": "cp ../../LICENSE ./LICENSE",
"clean": "rm -rf dist LICENSE",
"build:npm": "tsdown",
"publish:npm": "pnpm clean && pnpm build:npm && pnpm publish --access public",
"publish:npm": "pnpm clean && pnpm build:npm && pnpm publish --access public --no-git-checks",
"prepare:jsr": "rm -rf dist && pnpm dlx pkg-to-jsr",
"publish:jsr": "pnpm run prepack && pnpm prepare:jsr && deno publish --allow-dirty --no-check"
},
"dependencies": {
"crc": "npm:crc@^4.3.2"
}
}
}

View File

@@ -64,6 +64,11 @@ export class MeshDevice {
this.isConfigured = true;
} else if (status === DeviceStatusEnum.DeviceConfiguring) {
this.isConfigured = false;
} else if (status === DeviceStatusEnum.DeviceDisconnected) {
if (this._heartbeatIntervalId !== undefined) {
clearInterval(this._heartbeatIntervalId);
}
this.complete();
}
});
@@ -378,6 +383,53 @@ export class MeshDevice {
);
}
/**
* Sets the fixed position of a device. Can be used to
* position GPS-less devices.
*/
public async setFixedPosition(
latitude: number,
longitude: number,
): Promise<number> {
const setPositionMessage = create(Protobuf.Admin.AdminMessageSchema, {
payloadVariant: {
case: "setFixedPosition",
value: create(Protobuf.Mesh.PositionSchema, {
latitudeI: Math.floor(latitude / 1e-7),
longitudeI: Math.floor(longitude / 1e-7),
}),
},
});
return await this.sendPacket(
toBinary(Protobuf.Admin.AdminMessageSchema, setPositionMessage),
Protobuf.Portnums.PortNum.ADMIN_APP,
"self",
0,
true,
false,
);
}
/**
* Remove the fixed position of a device
*/
public async removeFixedPosition(): Promise<number> {
const removePositionMessage = create(Protobuf.Admin.AdminMessageSchema, {
payloadVariant: {
case: "removeFixedPosition",
value: true,
},
});
return await this.sendPacket(
toBinary(Protobuf.Admin.AdminMessageSchema, removePositionMessage),
Protobuf.Portnums.PortNum.ADMIN_APP,
"self",
0,
true,
false,
);
}
/**
* Gets specified channel information from the radio
*/
@@ -626,7 +678,7 @@ export class MeshDevice {
public async reboot(time: number): Promise<number> {
this.log.debug(
Emitter[Emitter.Reboot],
`🔌 Rebooting node ${time > 0 ? "now" : `in ${time} seconds`}`,
`🔌 Rebooting node ${time === 0 ? "now" : `in ${time} seconds`}`,
);
const reboot = create(Protobuf.Admin.AdminMessageSchema, {
@@ -649,7 +701,7 @@ export class MeshDevice {
public async rebootOta(time: number): Promise<number> {
this.log.debug(
Emitter[Emitter.RebootOta],
`🔌 Rebooting into OTA mode ${time > 0 ? "now" : `in ${time} seconds`}`,
`🔌 Rebooting into OTA mode ${time === 0 ? "now" : `in ${time} seconds`}`,
);
const rebootOta = create(Protobuf.Admin.AdminMessageSchema, {
@@ -723,7 +775,14 @@ export class MeshDevice {
},
});
return this.sendRaw(toBinary(Protobuf.Mesh.ToRadioSchema, toRadio));
return this.sendRaw(toBinary(Protobuf.Mesh.ToRadioSchema, toRadio)).catch(
(e) => {
if (this.deviceStatus === DeviceStatusEnum.DeviceDisconnected) {
throw new Error("Device connection lost");
}
throw e;
},
);
}
/**
@@ -750,7 +809,12 @@ export class MeshDevice {
clearInterval(this._heartbeatIntervalId);
}
this._heartbeatIntervalId = setInterval(() => {
this.heartbeat();
this.heartbeat().catch((err) => {
this.log.error(
Emitter[Emitter.Ping],
`⚠️ Unable to send heartbeat: ${err.message}`,
);
});
}, interval);
}

View File

@@ -10,7 +10,12 @@ interface DebugLog {
data: string;
}
export type DeviceOutput = Packet | DebugLog;
interface StatusEvent {
type: "status";
data: { status: DeviceStatusEnum; reason?: string };
}
export type DeviceOutput = Packet | DebugLog | StatusEvent;
export interface Transport {
toDevice: WritableStream<Uint8Array>;
@@ -41,6 +46,7 @@ export enum DeviceStatusEnum {
DeviceConnected = 5,
DeviceConfiguring = 6,
DeviceConfigured = 7,
DeviceError = 8,
}
export type LogEventPacket = LogEvent & { date: Date };
@@ -101,6 +107,7 @@ export enum Emitter {
RemoveNodeByNum = 32,
SetCannedMessages = 33,
Disconnect = 34,
ConnectionStatus = 35,
}
export interface LogEvent {

View File

@@ -330,6 +330,15 @@ export class EventSystem {
PacketMetadata<Uint8Array>
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
/**
* Fires when a new MeshPacket message containing a ClientNotification packet has been
* received from device
*
* @event onClientNotificationPacket
*/
public readonly onClientNotificationPacket: SimpleEventDispatcher<Protobuf.Mesh.ClientNotification> =
new SimpleEventDispatcher<Protobuf.Mesh.ClientNotification>();
/**
* Fires when the devices connection or configuration status changes
*
@@ -378,4 +387,12 @@ export class EventSystem {
*/
public readonly onQueueStatus: SimpleEventDispatcher<Protobuf.Mesh.QueueStatus> =
new SimpleEventDispatcher<Protobuf.Mesh.QueueStatus>();
/**
* Fires when a configCompleteId message is received from the device
*
* @event onConfigComplete
*/
public readonly onConfigComplete: SimpleEventDispatcher<number> =
new SimpleEventDispatcher<number>();
}

View File

@@ -117,6 +117,14 @@ export class Queue {
await writer.write(item.data);
item.sent = true;
} catch (error) {
if (
error?.code === "ECONNRESET" ||
error?.code === "ERR_INVALID_STATE"
) {
writer.releaseLock();
this.lock = false;
throw error;
}
console.error(`Error sending packet ${item.id}`, error);
}
}

View File

@@ -7,20 +7,51 @@ export const decodePacket = (device: MeshDevice) =>
new WritableStream<DeviceOutput>({
write(chunk) {
switch (chunk.type) {
case "status": {
const { status, reason } = chunk.data as {
status: Types.DeviceStatusEnum;
reason?: string;
};
device.updateDeviceStatus(status);
device.log.info(
Types.Emitter[Types.Emitter.ConnectionStatus],
`🔗 ${Types.DeviceStatusEnum[status]} ${reason ? `(${reason})` : ""}`,
);
break;
}
case "debug": {
break;
}
case "packet": {
const decodedMessage = fromBinary(
Protobuf.Mesh.FromRadioSchema,
chunk.data,
);
let decodedMessage: Protobuf.Mesh.FromRadio;
try {
decodedMessage = fromBinary(
Protobuf.Mesh.FromRadioSchema,
chunk.data,
);
} catch (e) {
device.log.error(
Types.Emitter[Types.Emitter.HandleFromRadio],
"⚠️ Received undecodable packet",
e,
);
break;
}
device.events.onFromRadio.dispatch(decodedMessage);
/** @todo Add map here when `all=true` gets fixed. */
switch (decodedMessage.payloadVariant.case) {
case "packet": {
device.handleMeshPacket(decodedMessage.payloadVariant.value);
try {
device.handleMeshPacket(decodedMessage.payloadVariant.value);
} catch (e) {
device.log.error(
Types.Emitter[Types.Emitter.HandleFromRadio],
"⚠️ Unable to handle mesh packet",
e,
);
}
break;
}
@@ -104,21 +135,27 @@ export const decodePacket = (device: MeshDevice) =>
}
case "configCompleteId": {
if (decodedMessage.payloadVariant.value !== device.configId) {
device.log.error(
Types.Emitter[Types.Emitter.HandleFromRadio],
`❌ Invalid config id received from device, expected ${device.configId} but received ${decodedMessage.payloadVariant.value}`,
);
}
device.log.info(
Types.Emitter[Types.Emitter.HandleFromRadio],
`⚙️ Valid config id received from device: ${device.configId}`,
`⚙️ Received config complete id: ${decodedMessage.payloadVariant.value}`,
);
device.updateDeviceStatus(
Types.DeviceStatusEnum.DeviceConfigured,
// Emit the configCompleteId event for MeshService to handle two-stage flow
device.events.onConfigComplete.dispatch(
decodedMessage.payloadVariant.value,
);
// For backward compatibility: if configId matches, update device status
// MeshService will override this behavior for two-stage flow
if (decodedMessage.payloadVariant.value === device.configId) {
device.log.info(
Types.Emitter[Types.Emitter.HandleFromRadio],
`⚙️ Config id matches device.configId: ${device.configId}`,
);
device.updateDeviceStatus(
Types.DeviceStatusEnum.DeviceConfigured,
);
}
break;
}
@@ -209,6 +246,18 @@ export const decodePacket = (device: MeshDevice) =>
break;
}
case "clientNotification": {
device.log.trace(
Types.Emitter[Types.Emitter.HandleFromRadio],
`📣 Received ClientNotification: ${decodedMessage.payloadVariant.value.message}`,
);
device.events.onClientNotificationPacket.dispatch(
decodedMessage.payloadVariant.value,
);
break;
}
default: {
device.log.warn(
Types.Emitter[Types.Emitter.HandleFromRadio],

View File

@@ -14,7 +14,7 @@ export const fromDeviceStream: () => TransformStream<Uint8Array, DeviceOutput> =
byteBuffer = new Uint8Array([...byteBuffer, ...chunk]);
let processingExhausted = false;
while (byteBuffer.length !== 0 && !processingExhausted) {
const framingIndex = byteBuffer.findIndex((byte) => byte === 0x94);
const framingIndex = byteBuffer.indexOf(0x94);
const framingByte2 = byteBuffer[framingIndex + 1];
if (framingByte2 === 0xc3) {
if (byteBuffer.subarray(0, framingIndex).length) {
@@ -35,9 +35,7 @@ export const fromDeviceStream: () => TransformStream<Uint8Array, DeviceOutput> =
) {
const packet = byteBuffer.subarray(4, 4 + (msb << 8) + lsb);
const malformedDetectorIndex = packet.findIndex(
(byte) => byte === 0x94,
);
const malformedDetectorIndex = packet.indexOf(0x94);
if (
malformedDetectorIndex !== -1 &&
packet[malformedDetectorIndex + 1] === 0xc3

View File

@@ -1,16 +1,18 @@
/**
* Pads packets with appropriate framing information before writing to the output stream.
*/
export const toDeviceStream: TransformStream<Uint8Array, Uint8Array> =
new TransformStream<Uint8Array, Uint8Array>({
transform(chunk: Uint8Array, controller): void {
const bufLen = chunk.length;
const header = new Uint8Array([
0x94,
0xc3,
(bufLen >> 8) & 0xff,
bufLen & 0xff,
]);
controller.enqueue(new Uint8Array([...header, ...chunk]));
},
});
export const toDeviceStream: () => TransformStream<Uint8Array, Uint8Array> =
() => {
return new TransformStream<Uint8Array, Uint8Array>({
transform(chunk: Uint8Array, controller): void {
const bufLen = chunk.length;
const header = new Uint8Array([
0x94,
0xc3,
(bufLen >> 8) & 0xff,
bufLen & 0xff,
]);
controller.enqueue(new Uint8Array([...header, ...chunk]));
},
});
};

View File

@@ -1,4 +1,4 @@
{
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"module": "ESNext",
@@ -7,12 +7,7 @@
"outDir": "./dist",
"moduleResolution": "bundler",
"emitDeclarationOnly": false,
"esModuleInterop": true,
"esModuleInterop": true
},
"include": ["src"]
}

3
packages/protobufs/.gitattributes vendored Normal file
View File

@@ -0,0 +1,3 @@
* text=auto eol=lf
*.{cmd,[cC][mM][dD]} text eol=crlf
*.{bat,[bB][aA][tT]} text eol=crlf

View File

@@ -0,0 +1,30 @@
<!-- Describe what you are intending to change -->
# What does this PR do?
<!-- Please remove or replace the issue url -->
> [Related Issue](https://github.com/meshtastic/protobufs/issues/0)
## Checklist before merging
- [ ] All top level messages commented
- [ ] All enum members have unique descriptions
### New Hardware Model Acceptance Policy
Due to limited availability and ongoing support, new Hardware Models will only be accepted from [Meshtastic Backers and Partners](https://meshtastic.com/). The Meshtastic team reserves the right to make exceptions to this policy.
#### Alternative for Community Contributors
You are welcome to use one of the existing DIY hardware models in your PlatformIO environment and create a pull request in the firmware project. Please note the following conditions:
- The device will **not** be officially supported by the core Meshtastic team.
- The device will **not** appear in the [Web Flasher](https://flasher.meshtastic.org/) or Github release assets.
- You will be responsible for ongoing maintenance and support.
- Community-contributed / DIY hardware models are considered experimental and will likely have limited or no testing.
#### Getting Official Support
To have your hardware model officially supported and included in the Meshtastic ecosystem, consider becoming a Meshtastic Backer or Partner. Visit [meshtastic.com](https://meshtastic.com/) for more information about partnership opportunities.

View File

@@ -0,0 +1,24 @@
name: Push commit to schema registry
permissions:
contents: read
on:
push:
branches:
- master
jobs:
push_to_registry:
name: Push to schema registry
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Push to schema registry
uses: bufbuild/buf-action@v1.2.0
with:
github_token: ${{ github.token }}
token: ${{ secrets.BUF_TOKEN }}
push: true

View File

@@ -0,0 +1,71 @@
name: Create tag
permissions:
contents: write
on:
workflow_dispatch:
inputs:
increment_type:
type: choice
description: Select the type of version increment
required: true
options:
- patch
- minor
- major
jobs:
increment_version:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- id: version
name: Get current version
run: |
VERSION=$(git describe --abbrev=0 --tags)
# Split version into major, minor, and patch
MAJOR=$(echo $VERSION | awk -F '.' '{print $1}' | cut -c 2-)
MINOR=$(echo $VERSION | awk -F '.' '{print $2}')
PATCH=$(echo $VERSION | awk -F '.' '{print $3}')
# Increment the appropriate part of the version
if [[ ${{ inputs.increment_type }} == "patch" ]]; then
PATCH=$((PATCH + 1))
elif [[ ${{ inputs.increment_type }} == "minor" ]]; then
MINOR=$((MINOR + 1))
PATCH=0
elif [[ ${{ inputs.increment_type }} == "major" ]]; then
MAJOR=$((MAJOR + 1))
MINOR=0
PATCH=0
fi
# Update the version
echo "NEW_VERSION=v$MAJOR.$MINOR.$PATCH" >> $GITHUB_OUTPUT
- name: Create release
uses: ncipollo/release-action@v1
with:
name: Meshtastic Protobufs ${{ steps.version.outputs.NEW_VERSION }}
tag: ${{ steps.version.outputs.NEW_VERSION }}
generateReleaseNotes: true
token: ${{ github.token }}
- name: Setup Buf
uses: bufbuild/buf-action@v1.2.0
with:
github_token: ${{ github.token }}
token: ${{ secrets.BUF_TOKEN }}
setup_only: true
- name: Push to schema registry
env:
BUF_TOKEN: ${{ secrets.BUF_TOKEN }}
run: |
buf push --tag ${{ steps.version.outputs.NEW_VERSION }}

View File

@@ -0,0 +1,30 @@
name: Push new version to schema registry
permissions:
contents: read
on:
push:
tags:
- "**"
jobs:
push_to_registry:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Buf
uses: bufbuild/buf-action@v1.2.0
with:
github_token: ${{ github.token }}
token: ${{ secrets.BUF_TOKEN }}
setup_only: true
- name: Push to schema registry
env:
BUF_TOKEN: ${{ secrets.BUF_TOKEN }}
run: |
buf push --tag ${{ github.ref_name }}

View File

@@ -0,0 +1,23 @@
name: pull-request
permissions:
contents: read
pull-requests: write
on: pull_request
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Buf PR Checks
uses: bufbuild/buf-action@v1.2.0
with:
github_token: ${{ github.token }}
token: ${{ secrets.BUF_TOKEN }}
format: true
lint: true
breaking: true

1
packages/protobufs/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.DS_Store

2
packages/protobufs/.gitmodules vendored Normal file
View File

@@ -0,0 +1,2 @@
[submodule "packages/protobufs"]
branch = master

View File

@@ -0,0 +1,3 @@
{
"recommendations": ["pbkit.vscode-pbkit", "bufbuild.vscode-buf"]
}

View File

@@ -0,0 +1,4 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "pbkit.vscode-pbkit"
}

View File

@@ -0,0 +1,16 @@
# Meshtastic Protobuf Definitions
[![CI](https://img.shields.io/github/actions/workflow/status/meshtastic/protobufs/ci.yml?branch=master&label=actions&logo=github&color=yellow)](https://github.com/meshtastic/protobufs/actions/workflows/ci.yml)
[![CLA assistant](https://cla-assistant.io/readme/badge/meshtastic/protobufs)](https://cla-assistant.io/meshtastic/protobufs)
[![Fiscal Contributors](https://opencollective.com/meshtastic/tiers/badge.svg?label=Fiscal%20Contributors&color=deeppink)](https://opencollective.com/meshtastic/)
[![Vercel](https://img.shields.io/static/v1?label=Powered%20by&message=Vercel&style=flat&logo=vercel&color=000000)](https://vercel.com?utm_source=meshtastic&utm_campaign=oss)
## Overview
The [Protobuf](https://developers.google.com/protocol-buffers) message definitions for the Meshtastic project (used by apps and the device firmware).
**[Documentation/API Reference](https://buf.build/meshtastic/protobufs)**
## Stats
![Alt](https://repobeats.axiom.co/api/embed/47e9ee1d81d9c0fdd2b4b5b4c673adb1756f6db5.svg "Repobeats analytics image")

View File

@@ -0,0 +1,5 @@
version: v2
plugins:
- remote: buf.build/bufbuild/es
out: packages/ts/dist
opt: target=ts

View File

@@ -0,0 +1,15 @@
version: v1
name: buf.build/meshtastic/protobufs
deps: []
build:
excludes:
- node_modules
breaking:
use:
- FILE
lint:
ignore_only:
PACKAGE_DEFINED:
- nanopb.proto
use:
- MINIMAL

View File

@@ -0,0 +1,13 @@
{
"name": "@meshtastic/protobufs",
"version": "__PACKAGE_VERSION__",
"exports": {
".": "./mod.ts"
},
"imports": {
"@bufbuild/protobuf": "npm:@bufbuild/protobuf@^2.9.0"
},
"publish": {
"exclude": ["!dist"]
}
}

View File

@@ -0,0 +1,19 @@
*AdminMessage.payload_variant anonymous_oneof:true
*AdminMessage.session_passkey max_size:8
*AdminMessage.InputEvent.event_code int_size:8
*AdminMessage.InputEvent.kb_char int_size:8
*AdminMessage.InputEvent.touch_x int_size:16
*AdminMessage.InputEvent.touch_y int_size:16
*AdminMessage.set_canned_message_module_messages max_size:201
*AdminMessage.get_canned_message_module_messages_response max_size:201
*AdminMessage.delete_file_request max_size:201
*AdminMessage.set_ringtone_message max_size:231
*AdminMessage.get_ringtone_response max_size:231
*HamParameters.call_sign max_size:8
*HamParameters.short_name max_size:5
*NodeRemoteHardwarePinsResponse.node_remote_hardware_pins max_count:16

View File

@@ -0,0 +1,582 @@
syntax = "proto3";
package meshtastic;
import "meshtastic/channel.proto";
import "meshtastic/config.proto";
import "meshtastic/connection_status.proto";
import "meshtastic/device_ui.proto";
import "meshtastic/mesh.proto";
import "meshtastic/module_config.proto";
option csharp_namespace = "Meshtastic.Protobufs";
option go_package = "github.com/meshtastic/go/generated";
option java_outer_classname = "AdminProtos";
option java_package = "com.geeksville.mesh";
option swift_prefix = "";
/*
* This message is handled by the Admin module and is responsible for all settings/channel read/write operations.
* This message is used to do settings operations to both remote AND local nodes.
* (Prior to 1.2 these operations were done via special ToRadio operations)
*/
message AdminMessage {
/*
* The node generates this key and sends it with any get_x_response packets.
* The client MUST include the same key with any set_x commands. Key expires after 300 seconds.
* Prevents replay attacks for admin messages.
*/
bytes session_passkey = 101;
/*
* TODO: REPLACE
*/
enum ConfigType {
/*
* TODO: REPLACE
*/
DEVICE_CONFIG = 0;
/*
* TODO: REPLACE
*/
POSITION_CONFIG = 1;
/*
* TODO: REPLACE
*/
POWER_CONFIG = 2;
/*
* TODO: REPLACE
*/
NETWORK_CONFIG = 3;
/*
* TODO: REPLACE
*/
DISPLAY_CONFIG = 4;
/*
* TODO: REPLACE
*/
LORA_CONFIG = 5;
/*
* TODO: REPLACE
*/
BLUETOOTH_CONFIG = 6;
/*
* TODO: REPLACE
*/
SECURITY_CONFIG = 7;
/*
* Session key config
*/
SESSIONKEY_CONFIG = 8;
/*
* device-ui config
*/
DEVICEUI_CONFIG = 9;
}
/*
* TODO: REPLACE
*/
enum ModuleConfigType {
/*
* TODO: REPLACE
*/
MQTT_CONFIG = 0;
/*
* TODO: REPLACE
*/
SERIAL_CONFIG = 1;
/*
* TODO: REPLACE
*/
EXTNOTIF_CONFIG = 2;
/*
* TODO: REPLACE
*/
STOREFORWARD_CONFIG = 3;
/*
* TODO: REPLACE
*/
RANGETEST_CONFIG = 4;
/*
* TODO: REPLACE
*/
TELEMETRY_CONFIG = 5;
/*
* TODO: REPLACE
*/
CANNEDMSG_CONFIG = 6;
/*
* TODO: REPLACE
*/
AUDIO_CONFIG = 7;
/*
* TODO: REPLACE
*/
REMOTEHARDWARE_CONFIG = 8;
/*
* TODO: REPLACE
*/
NEIGHBORINFO_CONFIG = 9;
/*
* TODO: REPLACE
*/
AMBIENTLIGHTING_CONFIG = 10;
/*
* TODO: REPLACE
*/
DETECTIONSENSOR_CONFIG = 11;
/*
* TODO: REPLACE
*/
PAXCOUNTER_CONFIG = 12;
}
enum BackupLocation {
/*
* Backup to the internal flash
*/
FLASH = 0;
/*
* Backup to the SD card
*/
SD = 1;
}
/*
* Input event message to be sent to the node.
*/
message InputEvent {
/*
* The input event code
*/
uint32 event_code = 1;
/*
* Keyboard character code
*/
uint32 kb_char = 2;
/*
* The touch X coordinate
*/
uint32 touch_x = 3;
/*
* The touch Y coordinate
*/
uint32 touch_y = 4;
}
/*
* TODO: REPLACE
*/
oneof payload_variant {
/*
* Send the specified channel in the response to this message
* NOTE: This field is sent with the channel index + 1 (to ensure we never try to send 'zero' - which protobufs treats as not present)
*/
uint32 get_channel_request = 1;
/*
* TODO: REPLACE
*/
Channel get_channel_response = 2;
/*
* Send the current owner data in the response to this message.
*/
bool get_owner_request = 3;
/*
* TODO: REPLACE
*/
User get_owner_response = 4;
/*
* Ask for the following config data to be sent
*/
ConfigType get_config_request = 5;
/*
* Send the current Config in the response to this message.
*/
Config get_config_response = 6;
/*
* Ask for the following config data to be sent
*/
ModuleConfigType get_module_config_request = 7;
/*
* Send the current Config in the response to this message.
*/
ModuleConfig get_module_config_response = 8;
/*
* Get the Canned Message Module messages in the response to this message.
*/
bool get_canned_message_module_messages_request = 10;
/*
* Get the Canned Message Module messages in the response to this message.
*/
string get_canned_message_module_messages_response = 11;
/*
* Request the node to send device metadata (firmware, protobuf version, etc)
*/
bool get_device_metadata_request = 12;
/*
* Device metadata response
*/
DeviceMetadata get_device_metadata_response = 13;
/*
* Get the Ringtone in the response to this message.
*/
bool get_ringtone_request = 14;
/*
* Get the Ringtone in the response to this message.
*/
string get_ringtone_response = 15;
/*
* Request the node to send it's connection status
*/
bool get_device_connection_status_request = 16;
/*
* Device connection status response
*/
DeviceConnectionStatus get_device_connection_status_response = 17;
/*
* Setup a node for licensed amateur (ham) radio operation
*/
HamParameters set_ham_mode = 18;
/*
* Get the mesh's nodes with their available gpio pins for RemoteHardware module use
*/
bool get_node_remote_hardware_pins_request = 19;
/*
* Respond with the mesh's nodes with their available gpio pins for RemoteHardware module use
*/
NodeRemoteHardwarePinsResponse get_node_remote_hardware_pins_response = 20;
/*
* Enter (UF2) DFU mode
* Only implemented on NRF52 currently
*/
bool enter_dfu_mode_request = 21;
/*
* Delete the file by the specified path from the device
*/
string delete_file_request = 22;
/*
* Set zero and offset for scale chips
*/
uint32 set_scale = 23;
/*
* Backup the node's preferences
*/
BackupLocation backup_preferences = 24;
/*
* Restore the node's preferences
*/
BackupLocation restore_preferences = 25;
/*
* Remove backups of the node's preferences
*/
BackupLocation remove_backup_preferences = 26;
/*
* Send an input event to the node.
* This is used to trigger physical input events like button presses, touch events, etc.
*/
InputEvent send_input_event = 27;
/*
* Set the owner for this node
*/
User set_owner = 32;
/*
* Set channels (using the new API).
* A special channel is the "primary channel".
* The other records are secondary channels.
* Note: only one channel can be marked as primary.
* If the client sets a particular channel to be primary, the previous channel will be set to SECONDARY automatically.
*/
Channel set_channel = 33;
/*
* Set the current Config
*/
Config set_config = 34;
/*
* Set the current Config
*/
ModuleConfig set_module_config = 35;
/*
* Set the Canned Message Module messages text.
*/
string set_canned_message_module_messages = 36;
/*
* Set the ringtone for ExternalNotification.
*/
string set_ringtone_message = 37;
/*
* Remove the node by the specified node-num from the NodeDB on the device
*/
uint32 remove_by_nodenum = 38;
/*
* Set specified node-num to be favorited on the NodeDB on the device
*/
uint32 set_favorite_node = 39;
/*
* Set specified node-num to be un-favorited on the NodeDB on the device
*/
uint32 remove_favorite_node = 40;
/*
* Set fixed position data on the node and then set the position.fixed_position = true
*/
Position set_fixed_position = 41;
/*
* Clear fixed position coordinates and then set position.fixed_position = false
*/
bool remove_fixed_position = 42;
/*
* Set time only on the node
* Convenience method to set the time on the node (as Net quality) without any other position data
*/
fixed32 set_time_only = 43;
/*
* Tell the node to send the stored ui data.
*/
bool get_ui_config_request = 44;
/*
* Reply stored device ui data.
*/
DeviceUIConfig get_ui_config_response = 45;
/*
* Tell the node to store UI data persistently.
*/
DeviceUIConfig store_ui_config = 46;
/*
* Set specified node-num to be ignored on the NodeDB on the device
*/
uint32 set_ignored_node = 47;
/*
* Set specified node-num to be un-ignored on the NodeDB on the device
*/
uint32 remove_ignored_node = 48;
/*
* Begins an edit transaction for config, module config, owner, and channel settings changes
* This will delay the standard *implicit* save to the file system and subsequent reboot behavior until committed (commit_edit_settings)
*/
bool begin_edit_settings = 64;
/*
* Commits an open transaction for any edits made to config, module config, owner, and channel settings
*/
bool commit_edit_settings = 65;
/*
* Add a contact (User) to the nodedb
*/
SharedContact add_contact = 66;
/*
* Initiate or respond to a key verification request
*/
KeyVerificationAdmin key_verification = 67;
/*
* Tell the node to factory reset config everything; all device state and configuration will be returned to factory defaults and BLE bonds will be cleared.
*/
int32 factory_reset_device = 94;
/*
* Tell the node to reboot into the OTA Firmware in this many seconds (or <0 to cancel reboot)
* Only Implemented for ESP32 Devices. This needs to be issued to send a new main firmware via bluetooth.
*/
int32 reboot_ota_seconds = 95;
/*
* This message is only supported for the simulator Portduino build.
* If received the simulator will exit successfully.
*/
bool exit_simulator = 96;
/*
* Tell the node to reboot in this many seconds (or <0 to cancel reboot)
*/
int32 reboot_seconds = 97;
/*
* Tell the node to shutdown in this many seconds (or <0 to cancel shutdown)
*/
int32 shutdown_seconds = 98;
/*
* Tell the node to factory reset config; all device state and configuration will be returned to factory defaults; BLE bonds will be preserved.
*/
int32 factory_reset_config = 99;
/*
* Tell the node to reset the nodedb.
*/
int32 nodedb_reset = 100;
}
}
/*
* Parameters for setting up Meshtastic for ameteur radio usage
*/
message HamParameters {
/*
* Amateur radio call sign, eg. KD2ABC
*/
string call_sign = 1;
/*
* Transmit power in dBm at the LoRA transceiver, not including any amplification
*/
int32 tx_power = 2;
/*
* The selected frequency of LoRA operation
* Please respect your local laws, regulations, and band plans.
* Ensure your radio is capable of operating of the selected frequency before setting this.
*/
float frequency = 3;
/*
* Optional short name of user
*/
string short_name = 4;
}
/*
* Response envelope for node_remote_hardware_pins
*/
message NodeRemoteHardwarePinsResponse {
/*
* Nodes and their respective remote hardware GPIO pins
*/
repeated NodeRemoteHardwarePin node_remote_hardware_pins = 1;
}
message SharedContact {
/*
* The node number of the contact
*/
uint32 node_num = 1;
/*
* The User of the contact
*/
User user = 2;
/*
* Add this contact to the blocked / ignored list
*/
bool should_ignore = 3;
/*
* Set the IS_KEY_MANUALLY_VERIFIED bit
*/
bool manually_verified = 4;
}
/*
* This message is used by a client to initiate or complete a key verification
*/
message KeyVerificationAdmin {
/*
* Three stages of this request.
*/
enum MessageType {
/*
* This is the first stage, where a client initiates
*/
INITIATE_VERIFICATION = 0;
/*
* After the nonce has been returned over the mesh, the client prompts for the security number
* And uses this message to provide it to the node.
*/
PROVIDE_SECURITY_NUMBER = 1;
/*
* Once the user has compared the verification message, this message notifies the node.
*/
DO_VERIFY = 2;
/*
* This is the cancel path, can be taken at any point
*/
DO_NOT_VERIFY = 3;
}
MessageType message_type = 1;
/*
* The nodenum we're requesting
*/
uint32 remote_nodenum = 2;
/*
* The nonce is used to track the connection
*/
uint64 nonce = 3;
/*
* The 4 digit code generated by the remote node, and communicated outside the mesh
*/
optional uint32 security_number = 4;
}

View File

@@ -0,0 +1 @@
*ChannelSet.settings max_count:8

View File

@@ -0,0 +1,31 @@
syntax = "proto3";
package meshtastic;
import "meshtastic/channel.proto";
import "meshtastic/config.proto";
option csharp_namespace = "Meshtastic.Protobufs";
option go_package = "github.com/meshtastic/go/generated";
option java_outer_classname = "AppOnlyProtos";
option java_package = "com.geeksville.mesh";
option swift_prefix = "";
/*
* This is the most compact possible representation for a set of channels.
* It includes only one PRIMARY channel (which must be first) and
* any SECONDARY channels.
* No DISABLED channels are included.
* This abstraction is used only on the the 'app side' of the world (ie python, javascript and android etc) to show a group of Channels as a (long) URL
*/
message ChannelSet {
/*
* Channel list with settings
*/
repeated ChannelSettings settings = 1;
/*
* LoRa config
*/
Config.LoRaConfig lora_config = 2;
}

View File

@@ -0,0 +1,8 @@
*Contact.callsign max_size:120
*Contact.device_callsign max_size:120
*Status.battery int_size:8
*PLI.course int_size:16
*GeoChat.message max_size:200
*GeoChat.to max_size:120
*GeoChat.to_callsign max_size:120
*TAKPacket.detail max_size:220

View File

@@ -0,0 +1,263 @@
syntax = "proto3";
package meshtastic;
option csharp_namespace = "Meshtastic.Protobufs";
option go_package = "github.com/meshtastic/go/generated";
option java_outer_classname = "ATAKProtos";
option java_package = "com.geeksville.mesh";
option swift_prefix = "";
/*
* Packets for the official ATAK Plugin
*/
message TAKPacket {
/*
* Are the payloads strings compressed for LoRA transport?
*/
bool is_compressed = 1;
/*
* The contact / callsign for ATAK user
*/
Contact contact = 2;
/*
* The group for ATAK user
*/
Group group = 3;
/*
* The status of the ATAK EUD
*/
Status status = 4;
/*
* The payload of the packet
*/
oneof payload_variant {
/*
* TAK position report
*/
PLI pli = 5;
/*
* ATAK GeoChat message
*/
GeoChat chat = 6;
/*
* Generic CoT detail XML
* May be compressed / truncated by the sender (EUD)
*/
bytes detail = 7;
}
}
/*
* ATAK GeoChat message
*/
message GeoChat {
/*
* The text message
*/
string message = 1;
/*
* Uid recipient of the message
*/
optional string to = 2;
/*
* Callsign of the recipient for the message
*/
optional string to_callsign = 3;
}
/*
* ATAK Group
* <__group role='Team Member' name='Cyan'/>
*/
message Group {
/*
* Role of the group member
*/
MemberRole role = 1;
/*
* Team (color)
* Default Cyan
*/
Team team = 2;
}
enum Team {
/*
* Unspecifed
*/
Unspecifed_Color = 0;
/*
* White
*/
White = 1;
/*
* Yellow
*/
Yellow = 2;
/*
* Orange
*/
Orange = 3;
/*
* Magenta
*/
Magenta = 4;
/*
* Red
*/
Red = 5;
/*
* Maroon
*/
Maroon = 6;
/*
* Purple
*/
Purple = 7;
/*
* Dark Blue
*/
Dark_Blue = 8;
/*
* Blue
*/
Blue = 9;
/*
* Cyan
*/
Cyan = 10;
/*
* Teal
*/
Teal = 11;
/*
* Green
*/
Green = 12;
/*
* Dark Green
*/
Dark_Green = 13;
/*
* Brown
*/
Brown = 14;
}
/*
* Role of the group member
*/
enum MemberRole {
/*
* Unspecifed
*/
Unspecifed = 0;
/*
* Team Member
*/
TeamMember = 1;
/*
* Team Lead
*/
TeamLead = 2;
/*
* Headquarters
*/
HQ = 3;
/*
* Airsoft enthusiast
*/
Sniper = 4;
/*
* Medic
*/
Medic = 5;
/*
* ForwardObserver
*/
ForwardObserver = 6;
/*
* Radio Telephone Operator
*/
RTO = 7;
/*
* Doggo
*/
K9 = 8;
}
/*
* ATAK EUD Status
* <status battery='100' />
*/
message Status {
/*
* Battery level
*/
uint32 battery = 1;
}
/*
* ATAK Contact
* <contact endpoint='0.0.0.0:4242:tcp' phone='+12345678' callsign='FALKE'/>
*/
message Contact {
/*
* Callsign
*/
string callsign = 1;
/*
* Device callsign
*/
string device_callsign = 2;
/*
* IP address of endpoint in integer form (0.0.0.0 default)
*/
// fixed32 enpoint_address = 3;
/*
* Port of endpoint (4242 default)
*/
// uint32 endpoint_port = 4;
/*
* Phone represented as integer
* Terrible practice, but we really need the wire savings
*/
// uint32 phone = 4;
}
/*
* Position Location Information from ATAK
*/
message PLI {
/*
* The new preferred location encoding, multiply by 1e-7 to get degrees
* in floating point
*/
sfixed32 latitude_i = 1;
/*
* The new preferred location encoding, multiply by 1e-7 to get degrees
* in floating point
*/
sfixed32 longitude_i = 2;
/*
* Altitude (ATAK prefers HAE)
*/
int32 altitude = 3;
/*
* Speed
*/
uint32 speed = 4;
/*
* Course in degrees
*/
uint32 course = 5;
}

View File

@@ -0,0 +1 @@
*CannedMessageModuleConfig.messages max_size:201

View File

@@ -0,0 +1,19 @@
syntax = "proto3";
package meshtastic;
option csharp_namespace = "Meshtastic.Protobufs";
option go_package = "github.com/meshtastic/go/generated";
option java_outer_classname = "CannedMessageConfigProtos";
option java_package = "com.geeksville.mesh";
option swift_prefix = "";
/*
* Canned message module configuration.
*/
message CannedMessageModuleConfig {
/*
* Predefined messages for canned message module separated by '|' characters.
*/
string messages = 1;
}

View File

@@ -0,0 +1,5 @@
*Channel.index int_size:8
# 256 bit or 128 bit psk key
*ChannelSettings.psk max_size:32
*ChannelSettings.name max_size:12

View File

@@ -0,0 +1,161 @@
syntax = "proto3";
package meshtastic;
option csharp_namespace = "Meshtastic.Protobufs";
option go_package = "github.com/meshtastic/go/generated";
option java_outer_classname = "ChannelProtos";
option java_package = "com.geeksville.mesh";
option swift_prefix = "";
/*
* This information can be encoded as a QRcode/url so that other users can configure
* their radio to join the same channel.
* A note about how channel names are shown to users: channelname-X
* poundsymbol is a prefix used to indicate this is a channel name (idea from @professr).
* Where X is a letter from A-Z (base 26) representing a hash of the PSK for this
* channel - so that if the user changes anything about the channel (which does
* force a new PSK) this letter will also change. Thus preventing user confusion if
* two friends try to type in a channel name of "BobsChan" and then can't talk
* because their PSKs will be different.
* The PSK is hashed into this letter by "0x41 + [xor all bytes of the psk ] modulo 26"
* This also allows the option of someday if people have the PSK off (zero), the
* users COULD type in a channel name and be able to talk.
* FIXME: Add description of multi-channel support and how primary vs secondary channels are used.
* FIXME: explain how apps use channels for security.
* explain how remote settings and remote gpio are managed as an example
*/
message ChannelSettings {
/*
* Deprecated in favor of LoraConfig.channel_num
*/
uint32 channel_num = 1 [deprecated = true];
/*
* A simple pre-shared key for now for crypto.
* Must be either 0 bytes (no crypto), 16 bytes (AES128), or 32 bytes (AES256).
* A special shorthand is used for 1 byte long psks.
* These psks should be treated as only minimally secure,
* because they are listed in this source code.
* Those bytes are mapped using the following scheme:
* `0` = No crypto
* `1` = The special "default" channel key: {0xd4, 0xf1, 0xbb, 0x3a, 0x20, 0x29, 0x07, 0x59, 0xf0, 0xbc, 0xff, 0xab, 0xcf, 0x4e, 0x69, 0x01}
* `2` through 10 = The default channel key, except with 1 through 9 added to the last byte.
* Shown to user as simple1 through 10
*/
bytes psk = 2;
/*
* A SHORT name that will be packed into the URL.
* Less than 12 bytes.
* Something for end users to call the channel
* If this is the empty string it is assumed that this channel
* is the special (minimally secure) "Default"channel.
* In user interfaces it should be rendered as a local language translation of "X".
* For channel_num hashing empty string will be treated as "X".
* Where "X" is selected based on the English words listed above for ModemPreset
*/
string name = 3;
/*
* Used to construct a globally unique channel ID.
* The full globally unique ID will be: "name.id" where ID is shown as base36.
* Assuming that the number of meshtastic users is below 20K (true for a long time)
* the chance of this 64 bit random number colliding with anyone else is super low.
* And the penalty for collision is low as well, it just means that anyone trying to decrypt channel messages might need to
* try multiple candidate channels.
* Any time a non wire compatible change is made to a channel, this field should be regenerated.
* There are a small number of 'special' globally known (and fairly) insecure standard channels.
* Those channels do not have a numeric id included in the settings, but instead it is pulled from
* a table of well known IDs.
* (see Well Known Channels FIXME)
*/
fixed32 id = 4;
/*
* If true, messages on the mesh will be sent to the *public* internet by any gateway ndoe
*/
bool uplink_enabled = 5;
/*
* If true, messages seen on the internet will be forwarded to the local mesh.
*/
bool downlink_enabled = 6;
/*
* Per-channel module settings.
*/
ModuleSettings module_settings = 7;
/*
* Whether or not we should receive notifactions / alerts through this channel
*/
bool mute = 8;
}
/*
* This message is specifically for modules to store per-channel configuration data.
*/
message ModuleSettings {
/*
* Bits of precision for the location sent in position packets.
*/
uint32 position_precision = 1;
/*
* Controls whether or not the phone / clients should mute the current channel
* Useful for noisy public channels you don't necessarily want to disable
*/
bool is_client_muted = 2;
}
/*
* A pair of a channel number, mode and the (sharable) settings for that channel
*/
message Channel {
/*
* How this channel is being used (or not).
* Note: this field is an enum to give us options for the future.
* In particular, someday we might make a 'SCANNING' option.
* SCANNING channels could have different frequencies and the radio would
* occasionally check that freq to see if anything is being transmitted.
* For devices that have multiple physical radios attached, we could keep multiple PRIMARY/SCANNING channels active at once to allow
* cross band routing as needed.
* If a device has only a single radio (the common case) only one channel can be PRIMARY at a time
* (but any number of SECONDARY channels can't be sent received on that common frequency)
*/
enum Role {
/*
* This channel is not in use right now
*/
DISABLED = 0;
/*
* This channel is used to set the frequency for the radio - all other enabled channels must be SECONDARY
*/
PRIMARY = 1;
/*
* Secondary channels are only used for encryption/decryption/authentication purposes.
* Their radio settings (freq etc) are ignored, only psk is used.
*/
SECONDARY = 2;
}
/*
* The index of this channel in the channel table (from 0 to MAX_NUM_CHANNELS-1)
* (Someday - not currently implemented) An index of -1 could be used to mean "set by name",
* in which case the target node will find and set the channel by settings.name.
*/
int32 index = 1;
/*
* The new settings, or NULL to disable that channel
*/
ChannelSettings settings = 2;
/*
* TODO: REPLACE
*/
Role role = 3;
}

View File

@@ -0,0 +1,4 @@
*DeviceProfile.long_name max_size:40
*DeviceProfile.short_name max_size:5
*DeviceProfile.ringtone max_size:231
*DeviceProfile.canned_messages max_size:201

View File

@@ -0,0 +1,58 @@
syntax = "proto3";
package meshtastic;
import "meshtastic/localonly.proto";
import "meshtastic/mesh.proto";
option csharp_namespace = "Meshtastic.Protobufs";
option go_package = "github.com/meshtastic/go/generated";
option java_outer_classname = "ClientOnlyProtos";
option java_package = "com.geeksville.mesh";
option swift_prefix = "";
/*
* This abstraction is used to contain any configuration for provisioning a node on any client.
* It is useful for importing and exporting configurations.
*/
message DeviceProfile {
/*
* Long name for the node
*/
optional string long_name = 1;
/*
* Short name of the node
*/
optional string short_name = 2;
/*
* The url of the channels from our node
*/
optional string channel_url = 3;
/*
* The Config of the node
*/
optional LocalConfig config = 4;
/*
* The ModuleConfig of the node
*/
optional LocalModuleConfig module_config = 5;
/*
* Fixed position data
*/
optional Position fixed_position = 6;
/*
* Ringtone for ExternalNotification
*/
optional string ringtone = 7;
/*
* Predefined messages for CannedMessage
*/
optional string canned_messages = 8;
}

View File

@@ -0,0 +1,24 @@
# longest current is 45 chars, plan with a bit of buffer
*DeviceConfig.tzdef max_size:65
*DeviceConfig.buzzer_mode int_size:8
*NetworkConfig.wifi_ssid max_size:33
*NetworkConfig.wifi_psk max_size:65
*NetworkConfig.ntp_server max_size:33
*NetworkConfig.rsyslog_server max_size:33
# Max of three ignored nodes for our testing
*LoRaConfig.ignore_incoming max_count:3
*LoRaConfig.tx_power int_size:8
*LoRaConfig.bandwidth int_size:16
*LoRaConfig.coding_rate int_size:8
*LoRaConfig.channel_num int_size:16
*PowerConfig.device_battery_ina_address int_size:8
*SecurityConfig.public_key max_size:32
*SecurityConfig.private_key max_size:32
*SecurityConfig.admin_key max_size:32
*SecurityConfig.admin_key max_count:3

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
*WifiConnectionStatus.ssid max_size:33

View File

@@ -0,0 +1,120 @@
syntax = "proto3";
package meshtastic;
option csharp_namespace = "Meshtastic.Protobufs";
option go_package = "github.com/meshtastic/go/generated";
option java_outer_classname = "ConnStatusProtos";
option java_package = "com.geeksville.mesh";
option swift_prefix = "";
message DeviceConnectionStatus {
/*
* WiFi Status
*/
optional WifiConnectionStatus wifi = 1;
/*
* WiFi Status
*/
optional EthernetConnectionStatus ethernet = 2;
/*
* Bluetooth Status
*/
optional BluetoothConnectionStatus bluetooth = 3;
/*
* Serial Status
*/
optional SerialConnectionStatus serial = 4;
}
/*
* WiFi connection status
*/
message WifiConnectionStatus {
/*
* Connection status
*/
NetworkConnectionStatus status = 1;
/*
* WiFi access point SSID
*/
string ssid = 2;
/*
* RSSI of wireless connection
*/
int32 rssi = 3;
}
/*
* Ethernet connection status
*/
message EthernetConnectionStatus {
/*
* Connection status
*/
NetworkConnectionStatus status = 1;
}
/*
* Ethernet or WiFi connection status
*/
message NetworkConnectionStatus {
/*
* IP address of device
*/
fixed32 ip_address = 1;
/*
* Whether the device has an active connection or not
*/
bool is_connected = 2;
/*
* Whether the device has an active connection to an MQTT broker or not
*/
bool is_mqtt_connected = 3;
/*
* Whether the device is actively remote syslogging or not
*/
bool is_syslog_connected = 4;
}
/*
* Bluetooth connection status
*/
message BluetoothConnectionStatus {
/*
* The pairing PIN for bluetooth
*/
uint32 pin = 1;
/*
* RSSI of bluetooth connection
*/
int32 rssi = 2;
/*
* Whether the device has an active connection or not
*/
bool is_connected = 3;
}
/*
* Serial connection status
*/
message SerialConnectionStatus {
/*
* Serial baud rate
*/
uint32 baud = 1;
/*
* Whether the device has an active connection or not
*/
bool is_connected = 2;
}

View File

@@ -0,0 +1,12 @@
*DeviceUIConfig.screen_brightness int_size:8
*DeviceUIConfig.screen_timeout int_size:16
*DeviceUIConfig.ring_tone_id int_size:8
*DeviceUIConfig.calibration_data max_size:16
*DeviceUIConfig.compass_mode int_size:8
*DeviceUIConfig.gps_format int_size:8
*NodeFilter.node_name max_size:16
*NodeFilter.hops_away int_size:8
*NodeFilter.channel int_size:8
*NodeHighlight.node_name max_size:16
*GeoPoint.zoom int_size:8
*Map.style max_size:20

View File

@@ -0,0 +1,389 @@
syntax = "proto3";
package meshtastic;
option csharp_namespace = "Meshtastic.Protobufs";
option go_package = "github.com/meshtastic/go/generated";
option java_outer_classname = "DeviceUIProtos";
option java_package = "com.geeksville.mesh";
option swift_prefix = "";
/*
* Protobuf structures for device-ui persistency
*/
message DeviceUIConfig {
/*
* A version integer used to invalidate saved files when we make incompatible changes.
*/
uint32 version = 1;
/*
* TFT display brightness 1..255
*/
uint32 screen_brightness = 2;
/*
* Screen timeout 0..900
*/
uint32 screen_timeout = 3;
/*
* Screen/Settings lock enabled
*/
bool screen_lock = 4;
bool settings_lock = 5;
uint32 pin_code = 6;
/*
* Color theme
*/
Theme theme = 7;
/*
* Audible message, banner and ring tone
*/
bool alert_enabled = 8;
bool banner_enabled = 9;
uint32 ring_tone_id = 10;
/*
* Localization
*/
Language language = 11;
/*
* Node list filter
*/
NodeFilter node_filter = 12;
/*
* Node list highlightening
*/
NodeHighlight node_highlight = 13;
/*
* 8 integers for screen calibration data
*/
bytes calibration_data = 14;
/*
* Map related data
*/
Map map_data = 15;
/*
* Compass mode
*/
CompassMode compass_mode = 16;
/*
* RGB color for BaseUI
* 0xRRGGBB format, e.g. 0xFF0000 for red
*/
uint32 screen_rgb_color = 17;
/*
* Clockface analog style
* true for analog clockface, false for digital clockface
*/
bool is_clockface_analog = 18;
/*
* How the GPS coordinates are formatted on the OLED screen.
*/
GpsCoordinateFormat gps_format = 19;
/*
* How the GPS coordinates are displayed on the OLED screen.
*/
enum GpsCoordinateFormat {
/*
* GPS coordinates are displayed in the normal decimal degrees format:
* DD.DDDDDD DDD.DDDDDD
*/
DEC = 0;
/*
* GPS coordinates are displayed in the degrees minutes seconds format:
* DD°MM'SS"C DDD°MM'SS"C, where C is the compass point representing the locations quadrant
*/
DMS = 1;
/*
* Universal Transverse Mercator format:
* ZZB EEEEEE NNNNNNN, where Z is zone, B is band, E is easting, N is northing
*/
UTM = 2;
/*
* Military Grid Reference System format:
* ZZB CD EEEEE NNNNN, where Z is zone, B is band, C is the east 100k square, D is the north 100k square,
* E is easting, N is northing
*/
MGRS = 3;
/*
* Open Location Code (aka Plus Codes).
*/
OLC = 4;
/*
* Ordnance Survey Grid Reference (the National Grid System of the UK).
* Format: AB EEEEE NNNNN, where A is the east 100k square, B is the north 100k square,
* E is the easting, N is the northing
*/
OSGR = 5;
/*
* Maidenhead Locator System
* Described here: https://en.wikipedia.org/wiki/Maidenhead_Locator_System
*/
MLS = 6;
}
}
message NodeFilter {
/*
* Filter unknown nodes
*/
bool unknown_switch = 1;
/*
* Filter offline nodes
*/
bool offline_switch = 2;
/*
* Filter nodes w/o public key
*/
bool public_key_switch = 3;
/*
* Filter based on hops away
*/
int32 hops_away = 4;
/*
* Filter nodes w/o position
*/
bool position_switch = 5;
/*
* Filter nodes by matching name string
*/
string node_name = 6;
/*
* Filter based on channel
*/
int32 channel = 7;
}
message NodeHighlight {
/*
* Hightlight nodes w/ active chat
*/
bool chat_switch = 1;
/*
* Highlight nodes w/ position
*/
bool position_switch = 2;
/*
* Highlight nodes w/ telemetry data
*/
bool telemetry_switch = 3;
/*
* Highlight nodes w/ iaq data
*/
bool iaq_switch = 4;
/*
* Highlight nodes by matching name string
*/
string node_name = 5;
}
message GeoPoint {
/*
* Zoom level
*/
int32 zoom = 1;
/*
* Coordinate: latitude
*/
int32 latitude = 2;
/*
* Coordinate: longitude
*/
int32 longitude = 3;
}
message Map {
/*
* Home coordinates
*/
GeoPoint home = 1;
/*
* Map tile style
*/
string style = 2;
/*
* Map scroll follows GPS
*/
bool follow_gps = 3;
}
enum CompassMode {
/*
* Compass with dynamic ring and heading
*/
DYNAMIC = 0;
/*
* Compass with fixed ring and heading
*/
FIXED_RING = 1;
/*
* Compass with heading and freeze option
*/
FREEZE_HEADING = 2;
}
enum Theme {
/*
* Dark
*/
DARK = 0;
/*
* Light
*/
LIGHT = 1;
/*
* Red
*/
RED = 2;
}
/*
* Localization
*/
enum Language {
/*
* English
*/
ENGLISH = 0;
/*
* French
*/
FRENCH = 1;
/*
* German
*/
GERMAN = 2;
/*
* Italian
*/
ITALIAN = 3;
/*
* Portuguese
*/
PORTUGUESE = 4;
/*
* Spanish
*/
SPANISH = 5;
/*
* Swedish
*/
SWEDISH = 6;
/*
* Finnish
*/
FINNISH = 7;
/*
* Polish
*/
POLISH = 8;
/*
* Turkish
*/
TURKISH = 9;
/*
* Serbian
*/
SERBIAN = 10;
/*
* Russian
*/
RUSSIAN = 11;
/*
* Dutch
*/
DUTCH = 12;
/*
* Greek
*/
GREEK = 13;
/*
* Norwegian
*/
NORWEGIAN = 14;
/*
* Slovenian
*/
SLOVENIAN = 15;
/*
* Ukrainian
*/
UKRAINIAN = 16;
/*
* Bulgarian
*/
BULGARIAN = 17;
/*
* Czech
*/
CZECH = 18;
/*
* Danish
*/
DANISH = 19;
/*
* Simplified Chinese (experimental)
*/
SIMPLIFIED_CHINESE = 30;
/*
* Traditional Chinese (experimental)
*/
TRADITIONAL_CHINESE = 31;
}

View File

@@ -0,0 +1,18 @@
# options for nanopb
# https://jpa.kapsi.fi/nanopb/docs/reference.html#proto-file-options
# FIXME - max_count is actually 32 but we save/load this as one long string of preencoded MeshPacket bytes - not a big array in RAM
*DeviceState.receive_queue max_count:1
*ChannelFile.channels max_count:8
*DeviceState.node_remote_hardware_pins max_count:12
*NodeInfoLite.channel int_size:8
*NodeInfoLite.hops_away int_size:8
*NodeInfoLite.next_hop int_size:8
*UserLite.long_name max_size:40
*UserLite.short_name max_size:5
*UserLite.public_key max_size:32 # public key
*UserLite.macaddr max_size:6 fixed_length:true

View File

@@ -0,0 +1,301 @@
syntax = "proto3";
package meshtastic;
import "meshtastic/channel.proto";
import "meshtastic/config.proto";
import "meshtastic/localonly.proto";
import "meshtastic/mesh.proto";
import "meshtastic/telemetry.proto";
import "nanopb.proto";
option csharp_namespace = "Meshtastic.Protobufs";
option go_package = "github.com/meshtastic/go/generated";
option java_outer_classname = "DeviceOnly";
option java_package = "com.geeksville.mesh";
option swift_prefix = "";
option (nanopb_fileopt).include = "<vector>";
/*
* Position with static location information only for NodeDBLite
*/
message PositionLite {
/*
* The new preferred location encoding, multiply by 1e-7 to get degrees
* in floating point
*/
sfixed32 latitude_i = 1;
/*
* TODO: REPLACE
*/
sfixed32 longitude_i = 2;
/*
* In meters above MSL (but see issue #359)
*/
int32 altitude = 3;
/*
* This is usually not sent over the mesh (to save space), but it is sent
* from the phone so that the local device can set its RTC If it is sent over
* the mesh (because there are devices on the mesh without GPS), it will only
* be sent by devices which has a hardware GPS clock.
* seconds since 1970
*/
fixed32 time = 4;
/*
* TODO: REPLACE
*/
Position.LocSource location_source = 5;
}
message UserLite {
/*
* This is the addr of the radio.
*/
bytes macaddr = 1 [deprecated = true];
/*
* A full name for this user, i.e. "Kevin Hester"
*/
string long_name = 2;
/*
* A VERY short name, ideally two characters.
* Suitable for a tiny OLED screen
*/
string short_name = 3;
/*
* TBEAM, HELTEC, etc...
* Starting in 1.2.11 moved to hw_model enum in the NodeInfo object.
* Apps will still need the string here for older builds
* (so OTA update can find the right image), but if the enum is available it will be used instead.
*/
HardwareModel hw_model = 4;
/*
* In some regions Ham radio operators have different bandwidth limitations than others.
* If this user is a licensed operator, set this flag.
* Also, "long_name" should be their licence number.
*/
bool is_licensed = 5;
/*
* Indicates that the user's role in the mesh
*/
Config.DeviceConfig.Role role = 6;
/*
* The public key of the user's device.
* This is sent out to other nodes on the mesh to allow them to compute a shared secret key.
*/
bytes public_key = 7;
/*
* Whether or not the node can be messaged
*/
optional bool is_unmessagable = 9;
}
message NodeInfoLite {
/*
* The node number
*/
uint32 num = 1;
/*
* The user info for this node
*/
UserLite user = 2;
/*
* This position data. Note: before 1.2.14 we would also store the last time we've heard from this node in position.time, that is no longer true.
* Position.time now indicates the last time we received a POSITION from that node.
*/
PositionLite position = 3;
/*
* Returns the Signal-to-noise ratio (SNR) of the last received message,
* as measured by the receiver. Return SNR of the last received message in dB
*/
float snr = 4;
/*
* Set to indicate the last time we received a packet from this node
*/
fixed32 last_heard = 5;
/*
* The latest device metrics for the node.
*/
DeviceMetrics device_metrics = 6;
/*
* local channel index we heard that node on. Only populated if its not the default channel.
*/
uint32 channel = 7;
/*
* True if we witnessed the node over MQTT instead of LoRA transport
*/
bool via_mqtt = 8;
/*
* Number of hops away from us this node is (0 if direct neighbor)
*/
optional uint32 hops_away = 9;
/*
* True if node is in our favorites list
* Persists between NodeDB internal clean ups
*/
bool is_favorite = 10;
/*
* True if node is in our ignored list
* Persists between NodeDB internal clean ups
*/
bool is_ignored = 11;
/*
* Last byte of the node number of the node that should be used as the next hop to reach this node.
*/
uint32 next_hop = 12;
/*
* Bitfield for storing booleans.
* LSB 0 is_key_manually_verified
*/
uint32 bitfield = 13;
}
/*
* This message is never sent over the wire, but it is used for serializing DB
* state to flash in the device code
* FIXME, since we write this each time we enter deep sleep (and have infinite
* flash) it would be better to use some sort of append only data structure for
* the receive queue and use the preferences store for the other stuff
*/
message DeviceState {
/*
* Read only settings/info about this node
*/
MyNodeInfo my_node = 2;
/*
* My owner info
*/
User owner = 3;
/*
* Received packets saved for delivery to the phone
*/
repeated MeshPacket receive_queue = 5;
/*
* A version integer used to invalidate old save files when we make
* incompatible changes This integer is set at build time and is private to
* NodeDB.cpp in the device code.
*/
uint32 version = 8;
/*
* We keep the last received text message (only) stored in the device flash,
* so we can show it on the screen.
* Might be null
*/
MeshPacket rx_text_message = 7;
/*
* Used only during development.
* Indicates developer is testing and changes should never be saved to flash.
* Deprecated in 2.3.1
*/
bool no_save = 9 [deprecated = true];
/*
* Previously used to manage GPS factory resets.
* Deprecated in 2.5.23
*/
bool did_gps_reset = 11 [deprecated = true];
/*
* We keep the last received waypoint stored in the device flash,
* so we can show it on the screen.
* Might be null
*/
MeshPacket rx_waypoint = 12;
/*
* The mesh's nodes with their available gpio pins for RemoteHardware module
*/
repeated NodeRemoteHardwarePin node_remote_hardware_pins = 13;
}
message NodeDatabase {
/*
* A version integer used to invalidate old save files when we make
* incompatible changes This integer is set at build time and is private to
* NodeDB.cpp in the device code.
*/
uint32 version = 1;
/*
* New lite version of NodeDB to decrease memory footprint
*/
repeated NodeInfoLite nodes = 2 [(nanopb).callback_datatype = "std::vector<meshtastic_NodeInfoLite>"];
}
/*
* The on-disk saved channels
*/
message ChannelFile {
/*
* The channels our node knows about
*/
repeated Channel channels = 1;
/*
* A version integer used to invalidate old save files when we make
* incompatible changes This integer is set at build time and is private to
* NodeDB.cpp in the device code.
*/
uint32 version = 2;
}
/*
* The on-disk backup of the node's preferences
*/
message BackupPreferences {
/*
* The version of the backup
*/
uint32 version = 1;
/*
* The timestamp of the backup (if node has time)
*/
fixed32 timestamp = 2;
/*
* The node's configuration
*/
LocalConfig config = 3;
/*
* The node's module configuration
*/
LocalModuleConfig module_config = 4;
/*
* The node's channels
*/
ChannelFile channels = 5;
/*
* The node's user (owner) information
*/
User owner = 6;
}

View File

@@ -0,0 +1 @@
*InterdeviceMessage.nmea max_size:1024

View File

@@ -0,0 +1,44 @@
syntax = "proto3";
package meshtastic;
option csharp_namespace = "Meshtastic.Protobufs";
option go_package = "github.com/meshtastic/go/generated";
option java_outer_classname = "InterdeviceProtos";
option java_package = "com.geeksville.mesh";
option swift_prefix = "";
// encapsulate up to 1k of NMEA string data
enum MessageType {
ACK = 0;
COLLECT_INTERVAL = 160; // in ms
BEEP_ON = 161; // duration ms
BEEP_OFF = 162; // cancel prematurely
SHUTDOWN = 163;
POWER_ON = 164;
SCD41_TEMP = 176;
SCD41_HUMIDITY = 177;
SCD41_CO2 = 178;
AHT20_TEMP = 179;
AHT20_HUMIDITY = 180;
TVOC_INDEX = 181;
}
message SensorData {
// The message type
MessageType type = 1;
// The sensor data, either as a float or an uint32
oneof data {
float float_value = 2;
uint32 uint32_value = 3;
}
}
message InterdeviceMessage {
// The message data
oneof data {
string nmea = 1;
SensorData sensor = 2;
}
}

View File

@@ -0,0 +1,140 @@
syntax = "proto3";
package meshtastic;
import "meshtastic/config.proto";
import "meshtastic/module_config.proto";
option csharp_namespace = "Meshtastic.Protobufs";
option go_package = "github.com/meshtastic/go/generated";
option java_outer_classname = "LocalOnlyProtos";
option java_package = "com.geeksville.mesh";
option swift_prefix = "";
/*
* Protobuf structures common to apponly.proto and deviceonly.proto
* This is never sent over the wire, only for local use
*/
message LocalConfig {
/*
* The part of the config that is specific to the Device
*/
Config.DeviceConfig device = 1;
/*
* The part of the config that is specific to the GPS Position
*/
Config.PositionConfig position = 2;
/*
* The part of the config that is specific to the Power settings
*/
Config.PowerConfig power = 3;
/*
* The part of the config that is specific to the Wifi Settings
*/
Config.NetworkConfig network = 4;
/*
* The part of the config that is specific to the Display
*/
Config.DisplayConfig display = 5;
/*
* The part of the config that is specific to the Lora Radio
*/
Config.LoRaConfig lora = 6;
/*
* The part of the config that is specific to the Bluetooth settings
*/
Config.BluetoothConfig bluetooth = 7;
/*
* A version integer used to invalidate old save files when we make
* incompatible changes This integer is set at build time and is private to
* NodeDB.cpp in the device code.
*/
uint32 version = 8;
/*
* The part of the config that is specific to Security settings
*/
Config.SecurityConfig security = 9;
}
message LocalModuleConfig {
/*
* The part of the config that is specific to the MQTT module
*/
ModuleConfig.MQTTConfig mqtt = 1;
/*
* The part of the config that is specific to the Serial module
*/
ModuleConfig.SerialConfig serial = 2;
/*
* The part of the config that is specific to the ExternalNotification module
*/
ModuleConfig.ExternalNotificationConfig external_notification = 3;
/*
* The part of the config that is specific to the Store & Forward module
*/
ModuleConfig.StoreForwardConfig store_forward = 4;
/*
* The part of the config that is specific to the RangeTest module
*/
ModuleConfig.RangeTestConfig range_test = 5;
/*
* The part of the config that is specific to the Telemetry module
*/
ModuleConfig.TelemetryConfig telemetry = 6;
/*
* The part of the config that is specific to the Canned Message module
*/
ModuleConfig.CannedMessageConfig canned_message = 7;
/*
* The part of the config that is specific to the Audio module
*/
ModuleConfig.AudioConfig audio = 9;
/*
* The part of the config that is specific to the Remote Hardware module
*/
ModuleConfig.RemoteHardwareConfig remote_hardware = 10;
/*
* The part of the config that is specific to the Neighbor Info module
*/
ModuleConfig.NeighborInfoConfig neighbor_info = 11;
/*
* The part of the config that is specific to the Ambient Lighting module
*/
ModuleConfig.AmbientLightingConfig ambient_lighting = 12;
/*
* The part of the config that is specific to the Detection Sensor module
*/
ModuleConfig.DetectionSensorConfig detection_sensor = 13;
/*
* Paxcounter Config
*/
ModuleConfig.PaxcounterConfig paxcounter = 14;
/*
* A version integer used to invalidate old save files when we make
* incompatible changes This integer is set at build time and is private to
* NodeDB.cpp in the device code.
*/
uint32 version = 8;
}

View File

@@ -0,0 +1,92 @@
# options for nanopb
# https://jpa.kapsi.fi/nanopb/docs/reference.html#proto-file-options
*macaddr max_size:6 fixed_length:true # macaddrs
*id max_size:16 # node id strings
*public_key max_size:32 # public key
*User.long_name max_size:40
*User.short_name max_size:5
*RouteDiscovery.route max_count:8
*RouteDiscovery.snr_towards max_count:8
*RouteDiscovery.snr_towards int_size:8
*RouteDiscovery.route_back max_count:8
*RouteDiscovery.snr_back max_count:8
*RouteDiscovery.snr_back int_size:8
# note: this payload length is ONLY the bytes that are sent inside of the Data protobuf (excluding protobuf overhead). The 16 byte header is
# outside of this envelope
*Data.payload max_size:233
*Data.bitfield int_size:8
*NodeInfo.channel int_size:8
*NodeInfo.hops_away int_size:8
# Big enough for 1.2.28.568032c-d
*MyNodeInfo.firmware_version max_size:18
*MyNodeInfo.device_id max_size:16
*MyNodeInfo.pio_env max_size:40
*MyNodeInfo.air_period_tx max_count:8
*MyNodeInfo.air_period_rx max_count:8
*MyNodeInfo.firmware_edition int_size:8
*MyNodeInfo.nodedb_count int_size:16
# Note: the actual limit (because of header bytes) on the size of encrypted payloads is 251 bytes, but I use 256
# here because we might need to fill with zeros for padding to encryption block size (16 bytes per block)
*MeshPacket.encrypted max_size:256
*MeshPacket.payload_variant anonymous_oneof:true
*MeshPacket.hop_limit int_size:8
*MeshPacket.hop_start int_size:8
*MeshPacket.channel int_size:8
*MeshPacket.next_hop int_size:8
*MeshPacket.relay_node int_size:8
*QueueStatus.res int_size:8
*QueueStatus.free int_size:8
*QueueStatus.maxlen int_size:8
*ToRadio.payload_variant anonymous_oneof:true
*FromRadio.payload_variant anonymous_oneof:true
*Routing.variant anonymous_oneof:true
*LogRecord.message max_size:384
*LogRecord.source max_size:32
*FileInfo.file_name max_size:228
*ClientNotification.message max_size:400
*KeyVerificationNumberInform.remote_longname max_size:40
*KeyVerificationNumberRequest.remote_longname max_size:40
*KeyVerificationFinal.remote_longname max_size:40
*KeyVerificationFinal.verification_characters max_size:10
*KeyVerification.hash1 max_size:32
*KeyVerification.hash2 max_size:32
# MyMessage.name max_size:40
# or fixed_length or fixed_count, or max_count
#This value may want to be a few bytes smaller to compensate for the parent fields.
*Compressed.data max_size:233
*Waypoint.name max_size:30
*Waypoint.description max_size:100
*NeighborInfo.neighbors max_count:10
*DeviceMetadata.firmware_version max_size:18
*MqttClientProxyMessage.topic max_size:60
*MqttClientProxyMessage.data max_size:435
*MqttClientProxyMessage.text max_size:435
*ChunkedPayload.chunk_count int_size:16
*ChunkedPayload.chunk_index int_size:16
*ChunkedPayload.payload_chunk max_size:228

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
*CannedMessageConfig.allow_input_source max_size:16
*MQTTConfig.address max_size:64
*MQTTConfig.username max_size:64
*MQTTConfig.password max_size:32
*MQTTConfig.root max_size:32
*AudioConfig.ptt_pin int_size:8
*AudioConfig.i2s_ws int_size:8
*AudioConfig.i2s_sd int_size:8
*AudioConfig.i2s_din int_size:8
*AudioConfig.i2s_sck int_size:8
*ExternalNotificationConfig.output_vibra int_size:8
*ExternalNotificationConfig.output_buzzer int_size:8
*ExternalNotificationConfig.nag_timeout int_size:16
*RemoteHardwareConfig.available_pins max_count:4
*RemoteHardwarePin.name max_size:15
*RemoteHardwarePin.gpio_pin int_size:8
*AmbientLightingConfig.current int_size:8
*AmbientLightingConfig.red int_size:8
*AmbientLightingConfig.green int_size:8
*AmbientLightingConfig.blue int_size:8
*DetectionSensorConfig.monitor_pin int_size:8
*DetectionSensorConfig.name max_size:20
*DetectionSensorConfig.detection_trigger_type max_size:8

View File

@@ -0,0 +1,870 @@
syntax = "proto3";
package meshtastic;
option csharp_namespace = "Meshtastic.Protobufs";
option go_package = "github.com/meshtastic/go/generated";
option java_outer_classname = "ModuleConfigProtos";
option java_package = "com.geeksville.mesh";
option swift_prefix = "";
/*
* Module Config
*/
message ModuleConfig {
/*
* MQTT Client Config
*/
message MQTTConfig {
/*
* If a meshtastic node is able to reach the internet it will normally attempt to gateway any channels that are marked as
* is_uplink_enabled or is_downlink_enabled.
*/
bool enabled = 1;
/*
* The server to use for our MQTT global message gateway feature.
* If not set, the default server will be used
*/
string address = 2;
/*
* MQTT username to use (most useful for a custom MQTT server).
* If using a custom server, this will be honoured even if empty.
* If using the default server, this will only be honoured if set, otherwise the device will use the default username
*/
string username = 3;
/*
* MQTT password to use (most useful for a custom MQTT server).
* If using a custom server, this will be honoured even if empty.
* If using the default server, this will only be honoured if set, otherwise the device will use the default password
*/
string password = 4;
/*
* Whether to send encrypted or decrypted packets to MQTT.
* This parameter is only honoured if you also set server
* (the default official mqtt.meshtastic.org server can handle encrypted packets)
* Decrypted packets may be useful for external systems that want to consume meshtastic packets
*/
bool encryption_enabled = 5;
/*
* Whether to send / consume json packets on MQTT
*/
bool json_enabled = 6;
/*
* If true, we attempt to establish a secure connection using TLS
*/
bool tls_enabled = 7;
/*
* The root topic to use for MQTT messages. Default is "msh".
* This is useful if you want to use a single MQTT server for multiple meshtastic networks and separate them via ACLs
*/
string root = 8;
/*
* If true, we can use the connected phone / client to proxy messages to MQTT instead of a direct connection
*/
bool proxy_to_client_enabled = 9;
/*
* If true, we will periodically report unencrypted information about our node to a map via MQTT
*/
bool map_reporting_enabled = 10;
/*
* Settings for reporting information about our node to a map via MQTT
*/
MapReportSettings map_report_settings = 11;
}
/*
* Settings for reporting unencrypted information about our node to a map via MQTT
*/
message MapReportSettings {
/*
* How often we should report our info to the map (in seconds)
*/
uint32 publish_interval_secs = 1;
/*
* Bits of precision for the location sent (default of 32 is full precision).
*/
uint32 position_precision = 2;
/*
* Whether we have opted-in to report our location to the map
*/
bool should_report_location = 3;
}
/*
* RemoteHardwareModule Config
*/
message RemoteHardwareConfig {
/*
* Whether the Module is enabled
*/
bool enabled = 1;
/*
* Whether the Module allows consumers to read / write to pins not defined in available_pins
*/
bool allow_undefined_pin_access = 2;
/*
* Exposes the available pins to the mesh for reading and writing
*/
repeated RemoteHardwarePin available_pins = 3;
}
/*
* NeighborInfoModule Config
*/
message NeighborInfoConfig {
/*
* Whether the Module is enabled
*/
bool enabled = 1;
/*
* Interval in seconds of how often we should try to send our
* Neighbor Info (minimum is 14400, i.e., 4 hours)
*/
uint32 update_interval = 2;
/*
* Whether in addition to sending it to MQTT and the PhoneAPI, our NeighborInfo should be transmitted over LoRa.
* Note that this is not available on a channel with default key and name.
*/
bool transmit_over_lora = 3;
}
/*
* Detection Sensor Module Config
*/
message DetectionSensorConfig {
enum TriggerType {
// Event is triggered if pin is low
LOGIC_LOW = 0;
// Event is triggered if pin is high
LOGIC_HIGH = 1;
// Event is triggered when pin goes high to low
FALLING_EDGE = 2;
// Event is triggered when pin goes low to high
RISING_EDGE = 3;
// Event is triggered on every pin state change, low is considered to be
// "active"
EITHER_EDGE_ACTIVE_LOW = 4;
// Event is triggered on every pin state change, high is considered to be
// "active"
EITHER_EDGE_ACTIVE_HIGH = 5;
}
/*
* Whether the Module is enabled
*/
bool enabled = 1;
/*
* Interval in seconds of how often we can send a message to the mesh when a
* trigger event is detected
*/
uint32 minimum_broadcast_secs = 2;
/*
* Interval in seconds of how often we should send a message to the mesh
* with the current state regardless of trigger events When set to 0, only
* trigger events will be broadcasted Works as a sort of status heartbeat
* for peace of mind
*/
uint32 state_broadcast_secs = 3;
/*
* Send ASCII bell with alert message
* Useful for triggering ext. notification on bell
*/
bool send_bell = 4;
/*
* Friendly name used to format message sent to mesh
* Example: A name "Motion" would result in a message "Motion detected"
* Maximum length of 20 characters
*/
string name = 5;
/*
* GPIO pin to monitor for state changes
*/
uint32 monitor_pin = 6;
/*
* The type of trigger event to be used
*/
TriggerType detection_trigger_type = 7;
/*
* Whether or not use INPUT_PULLUP mode for GPIO pin
* Only applicable if the board uses pull-up resistors on the pin
*/
bool use_pullup = 8;
}
/*
* Audio Config for codec2 voice
*/
message AudioConfig {
/*
* Baudrate for codec2 voice
*/
enum Audio_Baud {
CODEC2_DEFAULT = 0;
CODEC2_3200 = 1;
CODEC2_2400 = 2;
CODEC2_1600 = 3;
CODEC2_1400 = 4;
CODEC2_1300 = 5;
CODEC2_1200 = 6;
CODEC2_700 = 7;
CODEC2_700B = 8;
}
/*
* Whether Audio is enabled
*/
bool codec2_enabled = 1;
/*
* PTT Pin
*/
uint32 ptt_pin = 2;
/*
* The audio sample rate to use for codec2
*/
Audio_Baud bitrate = 3;
/*
* I2S Word Select
*/
uint32 i2s_ws = 4;
/*
* I2S Data IN
*/
uint32 i2s_sd = 5;
/*
* I2S Data OUT
*/
uint32 i2s_din = 6;
/*
* I2S Clock
*/
uint32 i2s_sck = 7;
}
/*
* Config for the Paxcounter Module
*/
message PaxcounterConfig {
/*
* Enable the Paxcounter Module
*/
bool enabled = 1;
/*
* Interval in seconds of how often we should try to send our
* metrics to the mesh
*/
uint32 paxcounter_update_interval = 2;
/*
* WiFi RSSI threshold. Defaults to -80
*/
int32 wifi_threshold = 3;
/*
* BLE RSSI threshold. Defaults to -80
*/
int32 ble_threshold = 4;
}
/*
* Serial Config
*/
message SerialConfig {
/*
* TODO: REPLACE
*/
enum Serial_Baud {
BAUD_DEFAULT = 0;
BAUD_110 = 1;
BAUD_300 = 2;
BAUD_600 = 3;
BAUD_1200 = 4;
BAUD_2400 = 5;
BAUD_4800 = 6;
BAUD_9600 = 7;
BAUD_19200 = 8;
BAUD_38400 = 9;
BAUD_57600 = 10;
BAUD_115200 = 11;
BAUD_230400 = 12;
BAUD_460800 = 13;
BAUD_576000 = 14;
BAUD_921600 = 15;
}
/*
* TODO: REPLACE
*/
enum Serial_Mode {
DEFAULT = 0;
SIMPLE = 1;
PROTO = 2;
TEXTMSG = 3;
NMEA = 4;
// NMEA messages specifically tailored for CalTopo
CALTOPO = 5;
// Ecowitt WS85 weather station
WS85 = 6;
// VE.Direct is a serial protocol used by Victron Energy products
// https://beta.ivc.no/wiki/index.php/Victron_VE_Direct_DIY_Cable
VE_DIRECT = 7;
//Used to configure and view some parameters of MeshSolar.
//https://heltec.org/project/meshsolar/
MS_CONFIG = 8;
}
/*
* Preferences for the SerialModule
*/
bool enabled = 1;
/*
* TODO: REPLACE
*/
bool echo = 2;
/*
* RX pin (should match Arduino gpio pin number)
*/
uint32 rxd = 3;
/*
* TX pin (should match Arduino gpio pin number)
*/
uint32 txd = 4;
/*
* Serial baud rate
*/
Serial_Baud baud = 5;
/*
* TODO: REPLACE
*/
uint32 timeout = 6;
/*
* Mode for serial module operation
*/
Serial_Mode mode = 7;
/*
* Overrides the platform's defacto Serial port instance to use with Serial module config settings
* This is currently only usable in output modes like NMEA / CalTopo and may behave strangely or not work at all in other modes
* Existing logging over the Serial Console will still be present
*/
bool override_console_serial_port = 8;
}
/*
* External Notifications Config
*/
message ExternalNotificationConfig {
/*
* Enable the ExternalNotificationModule
*/
bool enabled = 1;
/*
* When using in On/Off mode, keep the output on for this many
* milliseconds. Default 1000ms (1 second).
*/
uint32 output_ms = 2;
/*
* Define the output pin GPIO setting Defaults to
* EXT_NOTIFY_OUT if set for the board.
* In standalone devices this pin should drive the LED to match the UI.
*/
uint32 output = 3;
/*
* Optional: Define a secondary output pin for a vibra motor
* This is used in standalone devices to match the UI.
*/
uint32 output_vibra = 8;
/*
* Optional: Define a tertiary output pin for an active buzzer
* This is used in standalone devices to to match the UI.
*/
uint32 output_buzzer = 9;
/*
* IF this is true, the 'output' Pin will be pulled active high, false
* means active low.
*/
bool active = 4;
/*
* True: Alert when a text message arrives (output)
*/
bool alert_message = 5;
/*
* True: Alert when a text message arrives (output_vibra)
*/
bool alert_message_vibra = 10;
/*
* True: Alert when a text message arrives (output_buzzer)
*/
bool alert_message_buzzer = 11;
/*
* True: Alert when the bell character is received (output)
*/
bool alert_bell = 6;
/*
* True: Alert when the bell character is received (output_vibra)
*/
bool alert_bell_vibra = 12;
/*
* True: Alert when the bell character is received (output_buzzer)
*/
bool alert_bell_buzzer = 13;
/*
* use a PWM output instead of a simple on/off output. This will ignore
* the 'output', 'output_ms' and 'active' settings and use the
* device.buzzer_gpio instead.
*/
bool use_pwm = 7;
/*
* The notification will toggle with 'output_ms' for this time of seconds.
* Default is 0 which means don't repeat at all. 60 would mean blink
* and/or beep for 60 seconds
*/
uint32 nag_timeout = 14;
/*
* When true, enables devices with native I2S audio output to use the RTTTL over speaker like a buzzer
* T-Watch S3 and T-Deck for example have this capability
*/
bool use_i2s_as_buzzer = 15;
}
/*
* Store and Forward Module Config
*/
message StoreForwardConfig {
/*
* Enable the Store and Forward Module
*/
bool enabled = 1;
/*
* TODO: REPLACE
*/
bool heartbeat = 2;
/*
* TODO: REPLACE
*/
uint32 records = 3;
/*
* TODO: REPLACE
*/
uint32 history_return_max = 4;
/*
* TODO: REPLACE
*/
uint32 history_return_window = 5;
/*
* Set to true to let this node act as a server that stores received messages and resends them upon request.
*/
bool is_server = 6;
}
/*
* Preferences for the RangeTestModule
*/
message RangeTestConfig {
/*
* Enable the Range Test Module
*/
bool enabled = 1;
/*
* Send out range test messages from this node
*/
uint32 sender = 2;
/*
* Bool value indicating that this node should save a RangeTest.csv file.
* ESP32 Only
*/
bool save = 3;
/*
* Bool indicating that the node should cleanup / destroy it's RangeTest.csv file.
* ESP32 Only
*/
bool clear_on_reboot = 4;
}
/*
* Configuration for both device and environment metrics
*/
message TelemetryConfig {
/*
* Interval in seconds of how often we should try to send our
* device metrics to the mesh
*/
uint32 device_update_interval = 1;
/*
* Interval in seconds of how often we should try to send our
* environment measurements to the mesh
*/
uint32 environment_update_interval = 2;
/*
* Preferences for the Telemetry Module (Environment)
* Enable/Disable the telemetry measurement module measurement collection
*/
bool environment_measurement_enabled = 3;
/*
* Enable/Disable the telemetry measurement module on-device display
*/
bool environment_screen_enabled = 4;
/*
* We'll always read the sensor in Celsius, but sometimes we might want to
* display the results in Fahrenheit as a "user preference".
*/
bool environment_display_fahrenheit = 5;
/*
* Enable/Disable the air quality metrics
*/
bool air_quality_enabled = 6;
/*
* Interval in seconds of how often we should try to send our
* air quality metrics to the mesh
*/
uint32 air_quality_interval = 7;
/*
* Enable/disable Power metrics
*/
bool power_measurement_enabled = 8;
/*
* Interval in seconds of how often we should try to send our
* power metrics to the mesh
*/
uint32 power_update_interval = 9;
/*
* Enable/Disable the power measurement module on-device display
*/
bool power_screen_enabled = 10;
/*
* Preferences for the (Health) Telemetry Module
* Enable/Disable the telemetry measurement module measurement collection
*/
bool health_measurement_enabled = 11;
/*
* Interval in seconds of how often we should try to send our
* health metrics to the mesh
*/
uint32 health_update_interval = 12;
/*
* Enable/Disable the health telemetry module on-device display
*/
bool health_screen_enabled = 13;
/*
* Enable/Disable the device telemetry module to send metrics to the mesh
* Note: We will still send telemtry to the connected phone / client every minute over the API
*/
bool device_telemetry_enabled = 14;
}
/*
* Canned Messages Module Config
*/
message CannedMessageConfig {
/*
* TODO: REPLACE
*/
enum InputEventChar {
/*
* TODO: REPLACE
*/
NONE = 0;
/*
* TODO: REPLACE
*/
UP = 17;
/*
* TODO: REPLACE
*/
DOWN = 18;
/*
* TODO: REPLACE
*/
LEFT = 19;
/*
* TODO: REPLACE
*/
RIGHT = 20;
/*
* '\n'
*/
SELECT = 10;
/*
* TODO: REPLACE
*/
BACK = 27;
/*
* TODO: REPLACE
*/
CANCEL = 24;
}
/*
* Enable the rotary encoder #1. This is a 'dumb' encoder sending pulses on both A and B pins while rotating.
*/
bool rotary1_enabled = 1;
/*
* GPIO pin for rotary encoder A port.
*/
uint32 inputbroker_pin_a = 2;
/*
* GPIO pin for rotary encoder B port.
*/
uint32 inputbroker_pin_b = 3;
/*
* GPIO pin for rotary encoder Press port.
*/
uint32 inputbroker_pin_press = 4;
/*
* Generate input event on CW of this kind.
*/
InputEventChar inputbroker_event_cw = 5;
/*
* Generate input event on CCW of this kind.
*/
InputEventChar inputbroker_event_ccw = 6;
/*
* Generate input event on Press of this kind.
*/
InputEventChar inputbroker_event_press = 7;
/*
* Enable the Up/Down/Select input device. Can be RAK rotary encoder or 3 buttons. Uses the a/b/press definitions from inputbroker.
*/
bool updown1_enabled = 8;
/*
* Enable/disable CannedMessageModule.
*/
bool enabled = 9 [deprecated = true];
/*
* Input event origin accepted by the canned message module.
* Can be e.g. "rotEnc1", "upDownEnc1", "scanAndSelect", "cardkb", "serialkb", or keyword "_any"
*/
string allow_input_source = 10 [deprecated = true];
/*
* CannedMessageModule also sends a bell character with the messages.
* ExternalNotificationModule can benefit from this feature.
*/
bool send_bell = 11;
}
/*
Ambient Lighting Module - Settings for control of onboard LEDs to allow users to adjust the brightness levels and respective color levels.
Initially created for the RAK14001 RGB LED module.
*/
message AmbientLightingConfig {
/*
* Sets LED to on or off.
*/
bool led_state = 1;
/*
* Sets the current for the LED output. Default is 10.
*/
uint32 current = 2;
/*
* Sets the red LED level. Values are 0-255.
*/
uint32 red = 3;
/*
* Sets the green LED level. Values are 0-255.
*/
uint32 green = 4;
/*
* Sets the blue LED level. Values are 0-255.
*/
uint32 blue = 5;
}
/*
* TODO: REPLACE
*/
oneof payload_variant {
/*
* TODO: REPLACE
*/
MQTTConfig mqtt = 1;
/*
* TODO: REPLACE
*/
SerialConfig serial = 2;
/*
* TODO: REPLACE
*/
ExternalNotificationConfig external_notification = 3;
/*
* TODO: REPLACE
*/
StoreForwardConfig store_forward = 4;
/*
* TODO: REPLACE
*/
RangeTestConfig range_test = 5;
/*
* TODO: REPLACE
*/
TelemetryConfig telemetry = 6;
/*
* TODO: REPLACE
*/
CannedMessageConfig canned_message = 7;
/*
* TODO: REPLACE
*/
AudioConfig audio = 8;
/*
* TODO: REPLACE
*/
RemoteHardwareConfig remote_hardware = 9;
/*
* TODO: REPLACE
*/
NeighborInfoConfig neighbor_info = 10;
/*
* TODO: REPLACE
*/
AmbientLightingConfig ambient_lighting = 11;
/*
* TODO: REPLACE
*/
DetectionSensorConfig detection_sensor = 12;
/*
* TODO: REPLACE
*/
PaxcounterConfig paxcounter = 13;
}
}
/*
* A GPIO pin definition for remote hardware module
*/
message RemoteHardwarePin {
/*
* GPIO Pin number (must match Arduino)
*/
uint32 gpio_pin = 1;
/*
* Name for the GPIO pin (i.e. Front gate, mailbox, etc)
*/
string name = 2;
/*
* Type of GPIO access available to consumers on the mesh
*/
RemoteHardwarePinType type = 3;
}
enum RemoteHardwarePinType {
/*
* Unset/unused
*/
UNKNOWN = 0;
/*
* GPIO pin can be read (if it is high / low)
*/
DIGITAL_READ = 1;
/*
* GPIO pin can be written to (high / low)
*/
DIGITAL_WRITE = 2;
}

View File

@@ -0,0 +1,8 @@
*ServiceEnvelope.packet type:FT_POINTER
*ServiceEnvelope.channel_id type:FT_POINTER
*ServiceEnvelope.gateway_id type:FT_POINTER
*MapReport.long_name max_size:40
*MapReport.short_name max_size:5
*MapReport.firmware_version max_size:18
*MapReport.num_online_local_nodes int_size:16

View File

@@ -0,0 +1,112 @@
syntax = "proto3";
package meshtastic;
import "meshtastic/config.proto";
import "meshtastic/mesh.proto";
option csharp_namespace = "Meshtastic.Protobufs";
option go_package = "github.com/meshtastic/go/generated";
option java_outer_classname = "MQTTProtos";
option java_package = "com.geeksville.mesh";
option swift_prefix = "";
/*
* This message wraps a MeshPacket with extra metadata about the sender and how it arrived.
*/
message ServiceEnvelope {
/*
* The (probably encrypted) packet
*/
MeshPacket packet = 1;
/*
* The global channel ID it was sent on
*/
string channel_id = 2;
/*
* The sending gateway node ID. Can we use this to authenticate/prevent fake
* nodeid impersonation for senders? - i.e. use gateway/mesh id (which is authenticated) + local node id as
* the globally trusted nodenum
*/
string gateway_id = 3;
}
/*
* Information about a node intended to be reported unencrypted to a map using MQTT.
*/
message MapReport {
/*
* A full name for this user, i.e. "Kevin Hester"
*/
string long_name = 1;
/*
* A VERY short name, ideally two characters.
* Suitable for a tiny OLED screen
*/
string short_name = 2;
/*
* Role of the node that applies specific settings for a particular use-case
*/
Config.DeviceConfig.Role role = 3;
/*
* Hardware model of the node, i.e. T-Beam, Heltec V3, etc...
*/
HardwareModel hw_model = 4;
/*
* Device firmware version string
*/
string firmware_version = 5;
/*
* The region code for the radio (US, CN, EU433, etc...)
*/
Config.LoRaConfig.RegionCode region = 6;
/*
* Modem preset used by the radio (LongFast, MediumSlow, etc...)
*/
Config.LoRaConfig.ModemPreset modem_preset = 7;
/*
* Whether the node has a channel with default PSK and name (LongFast, MediumSlow, etc...)
* and it uses the default frequency slot given the region and modem preset.
*/
bool has_default_channel = 8;
/*
* Latitude: multiply by 1e-7 to get degrees in floating point
*/
sfixed32 latitude_i = 9;
/*
* Longitude: multiply by 1e-7 to get degrees in floating point
*/
sfixed32 longitude_i = 10;
/*
* Altitude in meters above MSL
*/
int32 altitude = 11;
/*
* Indicates the bits of precision for latitude and longitude set by the sending node
*/
uint32 position_precision = 12;
/*
* Number of online nodes (heard in the last 2 hours) this node has in its list that were received locally (not via MQTT)
*/
uint32 num_online_local_nodes = 13;
/*
* User has opted in to share their location (map report) with the mqtt server
* Controlled by map_report.should_report_location
*/
bool has_opted_report_location = 14;
}

View File

@@ -0,0 +1,29 @@
syntax = "proto3";
package meshtastic;
option csharp_namespace = "Meshtastic.Protobufs";
option go_package = "github.com/meshtastic/go/generated";
option java_outer_classname = "PaxcountProtos";
option java_package = "com.geeksville.mesh";
option swift_prefix = "";
/*
* TODO: REPLACE
*/
message Paxcount {
/*
* seen Wifi devices
*/
uint32 wifi = 1;
/*
* Seen BLE devices
*/
uint32 ble = 2;
/*
* Uptime in seconds
*/
uint32 uptime = 3;
}

View File

@@ -0,0 +1,244 @@
syntax = "proto3";
package meshtastic;
option csharp_namespace = "Meshtastic.Protobufs";
option go_package = "github.com/meshtastic/go/generated";
option java_outer_classname = "Portnums";
option java_package = "com.geeksville.mesh";
option swift_prefix = "";
/*
* For any new 'apps' that run on the device or via sister apps on phones/PCs they should pick and use a
* unique 'portnum' for their application.
* If you are making a new app using meshtastic, please send in a pull request to add your 'portnum' to this
* master table.
* PortNums should be assigned in the following range:
* 0-63 Core Meshtastic use, do not use for third party apps
* 64-127 Registered 3rd party apps, send in a pull request that adds a new entry to portnums.proto to register your application
* 256-511 Use one of these portnums for your private applications that you don't want to register publically
* All other values are reserved.
* Note: This was formerly a Type enum named 'typ' with the same id #
* We have change to this 'portnum' based scheme for specifying app handlers for particular payloads.
* This change is backwards compatible by treating the legacy OPAQUE/CLEAR_TEXT values identically.
*/
enum PortNum {
/*
* Deprecated: do not use in new code (formerly called OPAQUE)
* A message sent from a device outside of the mesh, in a form the mesh does not understand
* NOTE: This must be 0, because it is documented in IMeshService.aidl to be so
* ENCODING: binary undefined
*/
UNKNOWN_APP = 0;
/*
* A simple UTF-8 text message, which even the little micros in the mesh
* can understand and show on their screen eventually in some circumstances
* even signal might send messages in this form (see below)
* ENCODING: UTF-8 Plaintext (?)
*/
TEXT_MESSAGE_APP = 1;
/*
* Reserved for built-in GPIO/example app.
* See remote_hardware.proto/HardwareMessage for details on the message sent/received to this port number
* ENCODING: Protobuf
*/
REMOTE_HARDWARE_APP = 2;
/*
* The built-in position messaging app.
* Payload is a Position message.
* ENCODING: Protobuf
*/
POSITION_APP = 3;
/*
* The built-in user info app.
* Payload is a User message.
* ENCODING: Protobuf
*/
NODEINFO_APP = 4;
/*
* Protocol control packets for mesh protocol use.
* Payload is a Routing message.
* ENCODING: Protobuf
*/
ROUTING_APP = 5;
/*
* Admin control packets.
* Payload is a AdminMessage message.
* ENCODING: Protobuf
*/
ADMIN_APP = 6;
/*
* Compressed TEXT_MESSAGE payloads.
* ENCODING: UTF-8 Plaintext (?) with Unishox2 Compression
* NOTE: The Device Firmware converts a TEXT_MESSAGE_APP to TEXT_MESSAGE_COMPRESSED_APP if the compressed
* payload is shorter. There's no need for app developers to do this themselves. Also the firmware will decompress
* any incoming TEXT_MESSAGE_COMPRESSED_APP payload and convert to TEXT_MESSAGE_APP.
*/
TEXT_MESSAGE_COMPRESSED_APP = 7;
/*
* Waypoint payloads.
* Payload is a Waypoint message.
* ENCODING: Protobuf
*/
WAYPOINT_APP = 8;
/*
* Audio Payloads.
* Encapsulated codec2 packets. On 2.4 GHZ Bandwidths only for now
* ENCODING: codec2 audio frames
* NOTE: audio frames contain a 3 byte header (0xc0 0xde 0xc2) and a one byte marker for the decompressed bitrate.
* This marker comes from the 'moduleConfig.audio.bitrate' enum minus one.
*/
AUDIO_APP = 9;
/*
* Same as Text Message but originating from Detection Sensor Module.
* NOTE: This portnum traffic is not sent to the public MQTT starting at firmware version 2.2.9
*/
DETECTION_SENSOR_APP = 10;
/*
* Same as Text Message but used for critical alerts.
*/
ALERT_APP = 11;
/*
* Module/port for handling key verification requests.
*/
KEY_VERIFICATION_APP = 12;
/*
* Provides a 'ping' service that replies to any packet it receives.
* Also serves as a small example module.
* ENCODING: ASCII Plaintext
*/
REPLY_APP = 32;
/*
* Used for the python IP tunnel feature
* ENCODING: IP Packet. Handled by the python API, firmware ignores this one and pases on.
*/
IP_TUNNEL_APP = 33;
/*
* Paxcounter lib included in the firmware
* ENCODING: protobuf
*/
PAXCOUNTER_APP = 34;
/*
* Provides a hardware serial interface to send and receive from the Meshtastic network.
* Connect to the RX/TX pins of a device with 38400 8N1. Packets received from the Meshtastic
* network is forwarded to the RX pin while sending a packet to TX will go out to the Mesh network.
* Maximum packet size of 240 bytes.
* Module is disabled by default can be turned on by setting SERIAL_MODULE_ENABLED = 1 in SerialPlugh.cpp.
* ENCODING: binary undefined
*/
SERIAL_APP = 64;
/*
* STORE_FORWARD_APP (Work in Progress)
* Maintained by Jm Casler (MC Hamster) : jm@casler.org
* ENCODING: Protobuf
*/
STORE_FORWARD_APP = 65;
/*
* Optional port for messages for the range test module.
* ENCODING: ASCII Plaintext
* NOTE: This portnum traffic is not sent to the public MQTT starting at firmware version 2.2.9
*/
RANGE_TEST_APP = 66;
/*
* Provides a format to send and receive telemetry data from the Meshtastic network.
* Maintained by Charles Crossan (crossan007) : crossan007@gmail.com
* ENCODING: Protobuf
*/
TELEMETRY_APP = 67;
/*
* Experimental tools for estimating node position without a GPS
* Maintained by Github user a-f-G-U-C (a Meshtastic contributor)
* Project files at https://github.com/a-f-G-U-C/Meshtastic-ZPS
* ENCODING: arrays of int64 fields
*/
ZPS_APP = 68;
/*
* Used to let multiple instances of Linux native applications communicate
* as if they did using their LoRa chip.
* Maintained by GitHub user GUVWAF.
* Project files at https://github.com/GUVWAF/Meshtasticator
* ENCODING: Protobuf (?)
*/
SIMULATOR_APP = 69;
/*
* Provides a traceroute functionality to show the route a packet towards
* a certain destination would take on the mesh. Contains a RouteDiscovery message as payload.
* ENCODING: Protobuf
*/
TRACEROUTE_APP = 70;
/*
* Aggregates edge info for the network by sending out a list of each node's neighbors
* ENCODING: Protobuf
*/
NEIGHBORINFO_APP = 71;
/*
* ATAK Plugin
* Portnum for payloads from the official Meshtastic ATAK plugin
*/
ATAK_PLUGIN = 72;
/*
* Provides unencrypted information about a node for consumption by a map via MQTT
*/
MAP_REPORT_APP = 73;
/*
* PowerStress based monitoring support (for automated power consumption testing)
*/
POWERSTRESS_APP = 74;
/*
* Reticulum Network Stack Tunnel App
* ENCODING: Fragmented RNS Packet. Handled by Meshtastic RNS interface
*/
RETICULUM_TUNNEL_APP = 76;
/*
* App for transporting Cayenne Low Power Payload, popular for LoRaWAN sensor nodes. Offers ability to send
* arbitrary telemetry over meshtastic that is not covered by telemetry.proto
* ENCODING: CayenneLLP
*/
CAYENNE_APP = 77;
/*
* Private applications should use portnums >= 256.
* To simplify initial development and testing you can use "PRIVATE_APP"
* in your code without needing to rebuild protobuf files (via [regen-protos.sh](https://github.com/meshtastic/firmware/blob/master/bin/regen-protos.sh))
*/
PRIVATE_APP = 256;
/*
* ATAK Forwarder Module https://github.com/paulmandal/atak-forwarder
* ENCODING: libcotshrink
*/
ATAK_FORWARDER = 257;
/*
* Currently we limit port nums to no higher than this value
*/
MAX = 511;
}

View File

@@ -0,0 +1,103 @@
syntax = "proto3";
package meshtastic;
option csharp_namespace = "Meshtastic.Protobufs";
option go_package = "github.com/meshtastic/go/generated";
option java_outer_classname = "PowerMonProtos";
option java_package = "com.geeksville.mesh";
option swift_prefix = "";
/* Note: There are no 'PowerMon' messages normally in use (PowerMons are sent only as structured logs - slogs).
* But we wrap our State enum in this message to effectively nest a namespace (without our linter yelling at us)
*/
message PowerMon {
/* Any significant power changing event in meshtastic should be tagged with a powermon state transition.
* If you are making new meshtastic features feel free to add new entries at the end of this definition.
*/
enum State {
None = 0;
CPU_DeepSleep = 0x01;
CPU_LightSleep = 0x02;
/*
The external Vext1 power is on. Many boards have auxillary power rails that the CPU turns on only
occasionally. In cases where that rail has multiple devices on it we usually want to have logging on
the state of that rail as an independent record.
For instance on the Heltec Tracker 1.1 board, this rail is the power source for the GPS and screen.
The log messages will be short and complete (see PowerMon.Event in the protobufs for details).
something like "S:PM:C,0x00001234,REASON" where the hex number is the bitmask of all current states.
(We use a bitmask for states so that if a log message gets lost it won't be fatal)
*/
Vext1_On = 0x04;
Lora_RXOn = 0x08;
Lora_TXOn = 0x10;
Lora_RXActive = 0x20;
BT_On = 0x40;
LED_On = 0x80;
Screen_On = 0x100;
Screen_Drawing = 0x200;
Wifi_On = 0x400;
/*
* GPS is actively trying to find our location
* See GPSPowerState for more details
*/
GPS_Active = 0x800;
}
}
/*
* PowerStress testing support via the C++ PowerStress module
*/
message PowerStressMessage {
/*
* What operation would we like the UUT to perform.
* note: senders should probably set want_response in their request packets, so that they can know when the state
* machine has started processing their request
*/
enum Opcode {
/*
* Unset/unused
*/
UNSET = 0;
PRINT_INFO = 1; // Print board version slog and send an ack that we are alive and ready to process commands
FORCE_QUIET = 2; // Try to turn off all automatic processing of packets, screen, sleeping, etc (to make it easier to measure in isolation)
END_QUIET = 3; // Stop powerstress processing - probably by just rebooting the board
SCREEN_ON = 16; // Turn the screen on
SCREEN_OFF = 17; // Turn the screen off
CPU_IDLE = 32; // Let the CPU run but we assume mostly idling for num_seconds
CPU_DEEPSLEEP = 33; // Force deep sleep for FIXME seconds
CPU_FULLON = 34; // Spin the CPU as fast as possible for num_seconds
LED_ON = 48; // Turn the LED on for num_seconds (and leave it on - for baseline power measurement purposes)
LED_OFF = 49; // Force the LED off for num_seconds
LORA_OFF = 64; // Completely turn off the LORA radio for num_seconds
LORA_TX = 65; // Send Lora packets for num_seconds
LORA_RX = 66; // Receive Lora packets for num_seconds (node will be mostly just listening, unless an external agent is helping stress this by sending packets on the current channel)
BT_OFF = 80; // Turn off the BT radio for num_seconds
BT_ON = 81; // Turn on the BT radio for num_seconds
WIFI_OFF = 96; // Turn off the WIFI radio for num_seconds
WIFI_ON = 97; // Turn on the WIFI radio for num_seconds
GPS_OFF = 112; // Turn off the GPS radio for num_seconds
GPS_ON = 113; // Turn on the GPS radio for num_seconds
}
/*
* What type of HardwareMessage is this?
*/
Opcode cmd = 1;
float num_seconds = 2;
}

View File

@@ -0,0 +1,75 @@
syntax = "proto3";
package meshtastic;
option csharp_namespace = "Meshtastic.Protobufs";
option go_package = "github.com/meshtastic/go/generated";
option java_outer_classname = "RemoteHardware";
option java_package = "com.geeksville.mesh";
option swift_prefix = "";
/*
* An example app to show off the module system. This message is used for
* REMOTE_HARDWARE_APP PortNums.
* Also provides easy remote access to any GPIO.
* In the future other remote hardware operations can be added based on user interest
* (i.e. serial output, spi/i2c input/output).
* FIXME - currently this feature is turned on by default which is dangerous
* because no security yet (beyond the channel mechanism).
* It should be off by default and then protected based on some TBD mechanism
* (a special channel once multichannel support is included?)
*/
message HardwareMessage {
/*
* TODO: REPLACE
*/
enum Type {
/*
* Unset/unused
*/
UNSET = 0;
/*
* Set gpio gpios based on gpio_mask/gpio_value
*/
WRITE_GPIOS = 1;
/*
* We are now interested in watching the gpio_mask gpios.
* If the selected gpios change, please broadcast GPIOS_CHANGED.
* Will implicitly change the gpios requested to be INPUT gpios.
*/
WATCH_GPIOS = 2;
/*
* The gpios listed in gpio_mask have changed, the new values are listed in gpio_value
*/
GPIOS_CHANGED = 3;
/*
* Read the gpios specified in gpio_mask, send back a READ_GPIOS_REPLY reply with gpio_value populated
*/
READ_GPIOS = 4;
/*
* A reply to READ_GPIOS. gpio_mask and gpio_value will be populated
*/
READ_GPIOS_REPLY = 5;
}
/*
* What type of HardwareMessage is this?
*/
Type type = 1;
/*
* What gpios are we changing. Not used for all MessageTypes, see MessageType for details
*/
uint64 gpio_mask = 2;
/*
* For gpios that were listed in gpio_mask as valid, what are the signal levels for those gpios.
* Not used for all MessageTypes, see MessageType for details
*/
uint64 gpio_value = 3;
}

View File

@@ -0,0 +1 @@
*RTTTLConfig.ringtone max_size:231

View File

@@ -0,0 +1,19 @@
syntax = "proto3";
package meshtastic;
option csharp_namespace = "Meshtastic.Protobufs";
option go_package = "github.com/meshtastic/go/generated";
option java_outer_classname = "RTTTLConfigProtos";
option java_package = "com.geeksville.mesh";
option swift_prefix = "";
/*
* Canned message module configuration.
*/
message RTTTLConfig {
/*
* Ringtone for PWM Buzzer in RTTTL Format.
*/
string ringtone = 1;
}

View File

@@ -0,0 +1 @@
*StoreAndForward.text max_size:233

View File

@@ -0,0 +1,218 @@
syntax = "proto3";
package meshtastic;
option csharp_namespace = "Meshtastic.Protobufs";
option go_package = "github.com/meshtastic/go/generated";
option java_outer_classname = "StoreAndForwardProtos";
option java_package = "com.geeksville.mesh";
option swift_prefix = "";
/*
* TODO: REPLACE
*/
message StoreAndForward {
/*
* 001 - 063 = From Router
* 064 - 127 = From Client
*/
enum RequestResponse {
/*
* Unset/unused
*/
UNSET = 0;
/*
* Router is an in error state.
*/
ROUTER_ERROR = 1;
/*
* Router heartbeat
*/
ROUTER_HEARTBEAT = 2;
/*
* Router has requested the client respond. This can work as a
* "are you there" message.
*/
ROUTER_PING = 3;
/*
* The response to a "Ping"
*/
ROUTER_PONG = 4;
/*
* Router is currently busy. Please try again later.
*/
ROUTER_BUSY = 5;
/*
* Router is responding to a request for history.
*/
ROUTER_HISTORY = 6;
/*
* Router is responding to a request for stats.
*/
ROUTER_STATS = 7;
/*
* Router sends a text message from its history that was a direct message.
*/
ROUTER_TEXT_DIRECT = 8;
/*
* Router sends a text message from its history that was a broadcast.
*/
ROUTER_TEXT_BROADCAST = 9;
/*
* Client is an in error state.
*/
CLIENT_ERROR = 64;
/*
* Client has requested a replay from the router.
*/
CLIENT_HISTORY = 65;
/*
* Client has requested stats from the router.
*/
CLIENT_STATS = 66;
/*
* Client has requested the router respond. This can work as a
* "are you there" message.
*/
CLIENT_PING = 67;
/*
* The response to a "Ping"
*/
CLIENT_PONG = 68;
/*
* Client has requested that the router abort processing the client's request
*/
CLIENT_ABORT = 106;
}
/*
* TODO: REPLACE
*/
message Statistics {
/*
* Number of messages we have ever seen
*/
uint32 messages_total = 1;
/*
* Number of messages we have currently saved our history.
*/
uint32 messages_saved = 2;
/*
* Maximum number of messages we will save
*/
uint32 messages_max = 3;
/*
* Router uptime in seconds
*/
uint32 up_time = 4;
/*
* Number of times any client sent a request to the S&F.
*/
uint32 requests = 5;
/*
* Number of times the history was requested.
*/
uint32 requests_history = 6;
/*
* Is the heartbeat enabled on the server?
*/
bool heartbeat = 7;
/*
* Maximum number of messages the server will return.
*/
uint32 return_max = 8;
/*
* Maximum history window in minutes the server will return messages from.
*/
uint32 return_window = 9;
}
/*
* TODO: REPLACE
*/
message History {
/*
* Number of that will be sent to the client
*/
uint32 history_messages = 1;
/*
* The window of messages that was used to filter the history client requested
*/
uint32 window = 2;
/*
* Index in the packet history of the last message sent in a previous request to the server.
* Will be sent to the client before sending the history and can be set in a subsequent request to avoid getting packets the server already sent to the client.
*/
uint32 last_request = 3;
}
/*
* TODO: REPLACE
*/
message Heartbeat {
/*
* Period in seconds that the heartbeat is sent out that will be sent to the client
*/
uint32 period = 1;
/*
* If set, this is not the primary Store & Forward router on the mesh
*/
uint32 secondary = 2;
}
/*
* TODO: REPLACE
*/
RequestResponse rr = 1;
/*
* TODO: REPLACE
*/
oneof variant {
/*
* TODO: REPLACE
*/
Statistics stats = 2;
/*
* TODO: REPLACE
*/
History history = 3;
/*
* TODO: REPLACE
*/
Heartbeat heartbeat = 4;
/*
* Text from history message.
*/
bytes text = 5;
}
}

View File

@@ -0,0 +1,18 @@
# options for nanopb
# https://jpa.kapsi.fi/nanopb/docs/reference.html#proto-file-options
*EnvironmentMetrics.iaq int_size:16
*EnvironmentMetrics.wind_direction int_size:16
*EnvironmentMetrics.soil_moisture int_size:8
*LocalStats.num_online_nodes int_size:16
*LocalStats.num_total_nodes int_size:16
*LocalStats.num_tx_dropped int_size:16
*HealthMetrics.heart_bpm int_size:8
*HealthMetrics.spO2 int_size:8
*HostMetrics.load1 int_size:16
*HostMetrics.load5 int_size:16
*HostMetrics.load15 int_size:16
*HostMetrics.user_string max_size:200

View File

@@ -0,0 +1,808 @@
syntax = "proto3";
package meshtastic;
option csharp_namespace = "Meshtastic.Protobufs";
option go_package = "github.com/meshtastic/go/generated";
option java_outer_classname = "TelemetryProtos";
option java_package = "com.geeksville.mesh";
option swift_prefix = "";
/*
* Key native device metrics such as battery level
*/
message DeviceMetrics {
/*
* 0-100 (>100 means powered)
*/
optional uint32 battery_level = 1;
/*
* Voltage measured
*/
optional float voltage = 2;
/*
* Utilization for the current channel, including well formed TX, RX and malformed RX (aka noise).
*/
optional float channel_utilization = 3;
/*
* Percent of airtime for transmission used within the last hour.
*/
optional float air_util_tx = 4;
/*
* How long the device has been running since the last reboot (in seconds)
*/
optional uint32 uptime_seconds = 5;
}
/*
* Weather station or other environmental metrics
*/
message EnvironmentMetrics {
/*
* Temperature measured
*/
optional float temperature = 1;
/*
* Relative humidity percent measured
*/
optional float relative_humidity = 2;
/*
* Barometric pressure in hPA measured
*/
optional float barometric_pressure = 3;
/*
* Gas resistance in MOhm measured
*/
optional float gas_resistance = 4;
/*
* Voltage measured (To be depreciated in favor of PowerMetrics in Meshtastic 3.x)
*/
optional float voltage = 5;
/*
* Current measured (To be depreciated in favor of PowerMetrics in Meshtastic 3.x)
*/
optional float current = 6;
/*
* relative scale IAQ value as measured by Bosch BME680 . value 0-500.
* Belongs to Air Quality but is not particle but VOC measurement. Other VOC values can also be put in here.
*/
optional uint32 iaq = 7;
/*
* RCWL9620 Doppler Radar Distance Sensor, used for water level detection. Float value in mm.
*/
optional float distance = 8;
/*
* VEML7700 high accuracy ambient light(Lux) digital 16-bit resolution sensor.
*/
optional float lux = 9;
/*
* VEML7700 high accuracy white light(irradiance) not calibrated digital 16-bit resolution sensor.
*/
optional float white_lux = 10;
/*
* Infrared lux
*/
optional float ir_lux = 11;
/*
* Ultraviolet lux
*/
optional float uv_lux = 12;
/*
* Wind direction in degrees
* 0 degrees = North, 90 = East, etc...
*/
optional uint32 wind_direction = 13;
/*
* Wind speed in m/s
*/
optional float wind_speed = 14;
/*
* Weight in KG
*/
optional float weight = 15;
/*
* Wind gust in m/s
*/
optional float wind_gust = 16;
/*
* Wind lull in m/s
*/
optional float wind_lull = 17;
/*
* Radiation in µR/h
*/
optional float radiation = 18;
/*
* Rainfall in the last hour in mm
*/
optional float rainfall_1h = 19;
/*
* Rainfall in the last 24 hours in mm
*/
optional float rainfall_24h = 20;
/*
* Soil moisture measured (% 1-100)
*/
optional uint32 soil_moisture = 21;
/*
* Soil temperature measured (*C)
*/
optional float soil_temperature = 22;
}
/*
* Power Metrics (voltage / current / etc)
*/
message PowerMetrics {
/*
* Voltage (Ch1)
*/
optional float ch1_voltage = 1;
/*
* Current (Ch1)
*/
optional float ch1_current = 2;
/*
* Voltage (Ch2)
*/
optional float ch2_voltage = 3;
/*
* Current (Ch2)
*/
optional float ch2_current = 4;
/*
* Voltage (Ch3)
*/
optional float ch3_voltage = 5;
/*
* Current (Ch3)
*/
optional float ch3_current = 6;
/*
* Voltage (Ch4)
*/
optional float ch4_voltage = 7;
/*
* Current (Ch4)
*/
optional float ch4_current = 8;
/*
* Voltage (Ch5)
*/
optional float ch5_voltage = 9;
/*
* Current (Ch5)
*/
optional float ch5_current = 10;
/*
* Voltage (Ch6)
*/
optional float ch6_voltage = 11;
/*
* Current (Ch6)
*/
optional float ch6_current = 12;
/*
* Voltage (Ch7)
*/
optional float ch7_voltage = 13;
/*
* Current (Ch7)
*/
optional float ch7_current = 14;
/*
* Voltage (Ch8)
*/
optional float ch8_voltage = 15;
/*
* Current (Ch8)
*/
optional float ch8_current = 16;
}
/*
* Air quality metrics
*/
message AirQualityMetrics {
/*
* Concentration Units Standard PM1.0 in ug/m3
*/
optional uint32 pm10_standard = 1;
/*
* Concentration Units Standard PM2.5 in ug/m3
*/
optional uint32 pm25_standard = 2;
/*
* Concentration Units Standard PM10.0 in ug/m3
*/
optional uint32 pm100_standard = 3;
/*
* Concentration Units Environmental PM1.0 in ug/m3
*/
optional uint32 pm10_environmental = 4;
/*
* Concentration Units Environmental PM2.5 in ug/m3
*/
optional uint32 pm25_environmental = 5;
/*
* Concentration Units Environmental PM10.0 in ug/m3
*/
optional uint32 pm100_environmental = 6;
/*
* 0.3um Particle Count in #/0.1l
*/
optional uint32 particles_03um = 7;
/*
* 0.5um Particle Count in #/0.1l
*/
optional uint32 particles_05um = 8;
/*
* 1.0um Particle Count in #/0.1l
*/
optional uint32 particles_10um = 9;
/*
* 2.5um Particle Count in #/0.1l
*/
optional uint32 particles_25um = 10;
/*
* 5.0um Particle Count in #/0.1l
*/
optional uint32 particles_50um = 11;
/*
* 10.0um Particle Count in #/0.1l
*/
optional uint32 particles_100um = 12;
/*
* CO2 concentration in ppm
*/
optional uint32 co2 = 13;
/*
* CO2 sensor temperature in degC
*/
optional float co2_temperature = 14;
/*
* CO2 sensor relative humidity in %
*/
optional float co2_humidity = 15;
/*
* Formaldehyde sensor formaldehyde concentration in ppb
*/
optional float form_formaldehyde = 16;
/*
* Formaldehyde sensor relative humidity in %RH
*/
optional float form_humidity = 17;
/*
* Formaldehyde sensor temperature in degrees Celsius
*/
optional float form_temperature = 18;
/*
* Concentration Units Standard PM4.0 in ug/m3
*/
optional uint32 pm40_standard = 19;
/*
* 4.0um Particle Count in #/0.1l
*/
optional uint32 particles_40um = 20;
/*
* PM Sensor Temperature
*/
optional float pm_temperature = 21;
/*
* PM Sensor humidity
*/
optional float pm_humidity = 22;
/*
* PM Sensor VOC Index
*/
optional float pm_voc_idx = 23;
/*
* PM Sensor NOx Index
*/
optional float pm_nox_idx = 24;
/*
* Typical Particle Size in um
*/
optional float particles_tps = 25;
}
/*
* Local device mesh statistics
*/
message LocalStats {
/*
* How long the device has been running since the last reboot (in seconds)
*/
uint32 uptime_seconds = 1;
/*
* Utilization for the current channel, including well formed TX, RX and malformed RX (aka noise).
*/
float channel_utilization = 2;
/*
* Percent of airtime for transmission used within the last hour.
*/
float air_util_tx = 3;
/*
* Number of packets sent
*/
uint32 num_packets_tx = 4;
/*
* Number of packets received (both good and bad)
*/
uint32 num_packets_rx = 5;
/*
* Number of packets received that are malformed or violate the protocol
*/
uint32 num_packets_rx_bad = 6;
/*
* Number of nodes online (in the past 2 hours)
*/
uint32 num_online_nodes = 7;
/*
* Number of nodes total
*/
uint32 num_total_nodes = 8;
/*
* Number of received packets that were duplicates (due to multiple nodes relaying).
* If this number is high, there are nodes in the mesh relaying packets when it's unnecessary, for example due to the ROUTER/REPEATER role.
*/
uint32 num_rx_dupe = 9;
/*
* Number of packets we transmitted that were a relay for others (not originating from ourselves).
*/
uint32 num_tx_relay = 10;
/*
* Number of times we canceled a packet to be relayed, because someone else did it before us.
* This will always be zero for ROUTERs/REPEATERs. If this number is high, some other node(s) is/are relaying faster than you.
*/
uint32 num_tx_relay_canceled = 11;
/*
* Number of bytes used in the heap
*/
uint32 heap_total_bytes = 12;
/*
* Number of bytes free in the heap
*/
uint32 heap_free_bytes = 13;
/*
* Number of packets that were dropped because the transmit queue was full.
*/
uint32 num_tx_dropped = 14;
}
/*
* Health telemetry metrics
*/
message HealthMetrics {
/*
* Heart rate (beats per minute)
*/
optional uint32 heart_bpm = 1;
/*
* SpO2 (blood oxygen saturation) level
*/
optional uint32 spO2 = 2;
/*
* Body temperature in degrees Celsius
*/
optional float temperature = 3;
}
/*
* Linux host metrics
*/
message HostMetrics {
/*
* Host system uptime
*/
uint32 uptime_seconds = 1;
/*
* Host system free memory
*/
uint64 freemem_bytes = 2;
/*
* Host system disk space free for /
*/
uint64 diskfree1_bytes = 3;
/*
* Secondary system disk space free
*/
optional uint64 diskfree2_bytes = 4;
/*
* Tertiary disk space free
*/
optional uint64 diskfree3_bytes = 5;
/*
* Host system one minute load in 1/100ths
*/
uint32 load1 = 6;
/*
* Host system five minute load in 1/100ths
*/
uint32 load5 = 7;
/*
* Host system fifteen minute load in 1/100ths
*/
uint32 load15 = 8;
/*
* Optional User-provided string for arbitrary host system information
* that doesn't make sense as a dedicated entry.
*/
optional string user_string = 9;
}
/*
* Types of Measurements the telemetry module is equipped to handle
*/
message Telemetry {
/*
* Seconds since 1970 - or 0 for unknown/unset
*/
fixed32 time = 1;
oneof variant {
/*
* Key native device metrics such as battery level
*/
DeviceMetrics device_metrics = 2;
/*
* Weather station or other environmental metrics
*/
EnvironmentMetrics environment_metrics = 3;
/*
* Air quality metrics
*/
AirQualityMetrics air_quality_metrics = 4;
/*
* Power Metrics
*/
PowerMetrics power_metrics = 5;
/*
* Local device mesh statistics
*/
LocalStats local_stats = 6;
/*
* Health telemetry metrics
*/
HealthMetrics health_metrics = 7;
/*
* Linux host metrics
*/
HostMetrics host_metrics = 8;
}
}
/*
* Supported I2C Sensors for telemetry in Meshtastic
*/
enum TelemetrySensorType {
/*
* No external telemetry sensor explicitly set
*/
SENSOR_UNSET = 0;
/*
* High accuracy temperature, pressure, humidity
*/
BME280 = 1;
/*
* High accuracy temperature, pressure, humidity, and air resistance
*/
BME680 = 2;
/*
* Very high accuracy temperature
*/
MCP9808 = 3;
/*
* Moderate accuracy current and voltage
*/
INA260 = 4;
/*
* Moderate accuracy current and voltage
*/
INA219 = 5;
/*
* High accuracy temperature and pressure
*/
BMP280 = 6;
/*
* High accuracy temperature and humidity
*/
SHTC3 = 7;
/*
* High accuracy pressure
*/
LPS22 = 8;
/*
* 3-Axis magnetic sensor
*/
QMC6310 = 9;
/*
* 6-Axis inertial measurement sensor
*/
QMI8658 = 10;
/*
* 3-Axis magnetic sensor
*/
QMC5883L = 11;
/*
* High accuracy temperature and humidity
*/
SHT31 = 12;
/*
* PM2.5 air quality sensor
*/
PMSA003I = 13;
/*
* INA3221 3 Channel Voltage / Current Sensor
*/
INA3221 = 14;
/*
* BMP085/BMP180 High accuracy temperature and pressure (older Version of BMP280)
*/
BMP085 = 15;
/*
* RCWL-9620 Doppler Radar Distance Sensor, used for water level detection
*/
RCWL9620 = 16;
/*
* Sensirion High accuracy temperature and humidity
*/
SHT4X = 17;
/*
* VEML7700 high accuracy ambient light(Lux) digital 16-bit resolution sensor.
*/
VEML7700 = 18;
/*
* MLX90632 non-contact IR temperature sensor.
*/
MLX90632 = 19;
/*
* TI OPT3001 Ambient Light Sensor
*/
OPT3001 = 20;
/*
* Lite On LTR-390UV-01 UV Light Sensor
*/
LTR390UV = 21;
/*
* AMS TSL25911FN RGB Light Sensor
*/
TSL25911FN = 22;
/*
* AHT10 Integrated temperature and humidity sensor
*/
AHT10 = 23;
/*
* DFRobot Lark Weather station (temperature, humidity, pressure, wind speed and direction)
*/
DFROBOT_LARK = 24;
/*
* NAU7802 Scale Chip or compatible
*/
NAU7802 = 25;
/*
* BMP3XX High accuracy temperature and pressure
*/
BMP3XX = 26;
/*
* ICM-20948 9-Axis digital motion processor
*/
ICM20948 = 27;
/*
* MAX17048 1S lipo battery sensor (voltage, state of charge, time to go)
*/
MAX17048 = 28;
/*
* Custom I2C sensor implementation based on https://github.com/meshtastic/i2c-sensor
*/
CUSTOM_SENSOR = 29;
/*
* MAX30102 Pulse Oximeter and Heart-Rate Sensor
*/
MAX30102 = 30;
/*
* MLX90614 non-contact IR temperature sensor
*/
MLX90614 = 31;
/*
* SCD40/SCD41 CO2, humidity, temperature sensor
*/
SCD4X = 32;
/*
* ClimateGuard RadSens, radiation, Geiger-Muller Tube
*/
RADSENS = 33;
/*
* High accuracy current and voltage
*/
INA226 = 34;
/*
* DFRobot Gravity tipping bucket rain gauge
*/
DFROBOT_RAIN = 35;
/*
* Infineon DPS310 High accuracy pressure and temperature
*/
DPS310 = 36;
/*
* RAKWireless RAK12035 Soil Moisture Sensor Module
*/
RAK12035 = 37;
/*
* MAX17261 lipo battery gauge
*/
MAX17261 = 38;
/*
* PCT2075 Temperature Sensor
*/
PCT2075 = 39;
/*
* ADS1X15 ADC
*/
ADS1X15 = 40;
/*
* ADS1X15 ADC_ALT
*/
ADS1X15_ALT = 41;
/*
* Sensirion SFA30 Formaldehyde sensor
*/
SFA30 = 42;
/*
* SEN5X PM SENSORS
*/
SEN5X = 43;
/*
* TSL2561 light sensor
*/
TSL2561 = 44;
}
/*
* NAU7802 Telemetry configuration, for saving to flash
*/
message Nau7802Config {
/*
* The offset setting for the NAU7802
*/
int32 zeroOffset = 1;
/*
* The calibration factor for the NAU7802
*/
float calibrationFactor = 2;
}

View File

@@ -0,0 +1,6 @@
# options for nanopb
# https://jpa.kapsi.fi/nanopb/docs/reference.html#proto-file-options
*XModem.buffer max_size:128
*XModem.seq int_size:16
*XModem.crc16 int_size:16

View File

@@ -0,0 +1,27 @@
syntax = "proto3";
package meshtastic;
option csharp_namespace = "Meshtastic.Protobufs";
option go_package = "github.com/meshtastic/go/generated";
option java_outer_classname = "XmodemProtos";
option java_package = "com.geeksville.mesh";
option swift_prefix = "";
message XModem {
enum Control {
NUL = 0;
SOH = 1;
STX = 2;
EOT = 4;
ACK = 6;
NAK = 21;
CAN = 24;
CTRLZ = 26;
}
Control control = 1;
uint32 seq = 2;
uint32 crc16 = 3;
bytes buffer = 4;
}

View File

@@ -0,0 +1,185 @@
// Custom options for defining:
// - Maximum size of string/bytes
// - Maximum number of elements in array
//
// These are used by nanopb to generate statically allocable structures
// for memory-limited environments.
syntax = "proto2";
import "google/protobuf/descriptor.proto";
option go_package = "github.com/meshtastic/go/generated";
option java_package = "fi.kapsi.koti.jpa.nanopb";
enum FieldType {
FT_DEFAULT = 0; // Automatically decide field type, generate static field if possible.
FT_CALLBACK = 1; // Always generate a callback field.
FT_POINTER = 4; // Always generate a dynamically allocated field.
FT_STATIC = 2; // Generate a static field or raise an exception if not possible.
FT_IGNORE = 3; // Ignore the field completely.
FT_INLINE = 5; // Legacy option, use the separate 'fixed_length' option instead
}
enum IntSize {
IS_DEFAULT = 0; // Default, 32/64bit based on type in .proto
IS_8 = 8;
IS_16 = 16;
IS_32 = 32;
IS_64 = 64;
}
enum TypenameMangling {
M_NONE = 0; // Default, no typename mangling
M_STRIP_PACKAGE = 1; // Strip current package name
M_FLATTEN = 2; // Only use last path component
M_PACKAGE_INITIALS = 3; // Replace the package name by the initials
}
enum DescriptorSize {
DS_AUTO = 0; // Select minimal size based on field type
DS_1 = 1; // 1 word; up to 15 byte fields, no arrays
DS_2 = 2; // 2 words; up to 4095 byte fields, 4095 entry arrays
DS_4 = 4; // 4 words; up to 2^32-1 byte fields, 2^16-1 entry arrays
DS_8 = 8; // 8 words; up to 2^32-1 entry arrays
}
// This is the inner options message, which basically defines options for
// a field. When it is used in message or file scope, it applies to all
// fields.
message NanoPBOptions {
// Allocated size for 'bytes' and 'string' fields.
// For string fields, this should include the space for null terminator.
optional int32 max_size = 1;
// Maximum length for 'string' fields. Setting this is equivalent
// to setting max_size to a value of length+1.
optional int32 max_length = 14;
// Allocated number of entries in arrays ('repeated' fields)
optional int32 max_count = 2;
// Size of integer fields. Can save some memory if you don't need
// full 32 bits for the value.
optional IntSize int_size = 7 [default = IS_DEFAULT];
// Force type of field (callback or static allocation)
optional FieldType type = 3 [default = FT_DEFAULT];
// Use long names for enums, i.e. EnumName_EnumValue.
optional bool long_names = 4 [default = true];
// Add 'packed' attribute to generated structs.
// Note: this cannot be used on CPUs that break on unaligned
// accesses to variables.
optional bool packed_struct = 5 [default = false];
// Add 'packed' attribute to generated enums.
optional bool packed_enum = 10 [default = false];
// Skip this message
optional bool skip_message = 6 [default = false];
// Generate oneof fields as normal optional fields instead of union.
optional bool no_unions = 8 [default = false];
// integer type tag for a message
optional uint32 msgid = 9;
// decode oneof as anonymous union
optional bool anonymous_oneof = 11 [default = false];
// Proto3 singular field does not generate a "has_" flag
optional bool proto3 = 12 [default = false];
// Force proto3 messages to have no "has_" flag.
// This was default behavior until nanopb-0.4.0.
optional bool proto3_singular_msgs = 21 [default = false];
// Generate an enum->string mapping function (can take up lots of space).
optional bool enum_to_string = 13 [default = false];
// Generate bytes arrays with fixed length
optional bool fixed_length = 15 [default = false];
// Generate repeated field with fixed count
optional bool fixed_count = 16 [default = false];
// Generate message-level callback that is called before decoding submessages.
// This can be used to set callback fields for submsgs inside oneofs.
optional bool submsg_callback = 22 [default = false];
// Shorten or remove package names from type names.
// This option applies only on the file level.
optional TypenameMangling mangle_names = 17 [default = M_NONE];
// Data type for storage associated with callback fields.
optional string callback_datatype = 18 [default = "pb_callback_t"];
// Callback function used for encoding and decoding.
// Prior to nanopb-0.4.0, the callback was specified in per-field pb_callback_t
// structure. This is still supported, but does not work inside e.g. oneof or pointer
// fields. Instead, a new method allows specifying a per-message callback that
// will be called for all callback fields in a message type.
optional string callback_function = 19 [default = "pb_default_field_callback"];
// Select the size of field descriptors. This option has to be defined
// for the whole message, not per-field. Usually automatic selection is
// ok, but if it results in compilation errors you can increase the field
// size here.
optional DescriptorSize descriptorsize = 20 [default = DS_AUTO];
// Set default value for has_ fields.
optional bool default_has = 23 [default = false];
// Extra files to include in generated `.pb.h`
repeated string include = 24;
// Automatic includes to exclude from generated `.pb.h`
// Same as nanopb_generator.py command line flag -x.
repeated string exclude = 26;
// Package name that applies only for nanopb.
optional string package = 25;
// Override type of the field in generated C code. Only to be used with related field types
optional google.protobuf.FieldDescriptorProto.Type type_override = 27;
// Due to historical reasons, nanopb orders fields in structs by their tag number
// instead of the order in .proto. Set this to false to keep the .proto order.
// The default value will probably change to false in nanopb-0.5.0.
optional bool sort_by_tag = 28 [default = true];
// Set the FT_DEFAULT field conversion strategy.
// A field that can become a static member of a c struct (e.g. int, bool, etc)
// will be a a static field.
// Fields with dynamic length are converted to either a pointer or a callback.
optional FieldType fallback_type = 29 [default = FT_CALLBACK];
}
// Extensions to protoc 'Descriptor' type in order to define options
// inside a .proto file.
//
// Protocol Buffers extension number registry
// --------------------------------
// Project: Nanopb
// Contact: Petteri Aimonen <jpa@kapsi.fi>
// Web site: http://kapsi.fi/~jpa/nanopb
// Extensions: 1010 (all types)
// --------------------------------
extend google.protobuf.FileOptions {
optional NanoPBOptions nanopb_fileopt = 1010;
}
extend google.protobuf.MessageOptions {
optional NanoPBOptions nanopb_msgopt = 1010;
}
extend google.protobuf.EnumOptions {
optional NanoPBOptions nanopb_enumopt = 1010;
}
extend google.protobuf.FieldOptions {
optional NanoPBOptions nanopb = 1010;
}

View File

@@ -0,0 +1,30 @@
{
"name": "@meshtastic/protobufs-ws",
"private": true,
"version": "0.0.0",
"type": "module",
"description": "Workspace package for Meshtastic protobuf stubs (local dev only). This package is published to the JSR registry separately.",
"license": "GPL-3.0-only",
"files": [
"./packages/ts/dist/"
],
"exports": {
".": {
"types": "./packages/ts/dist/mod.d.ts",
"default": "./packages/ts/dist/mod.js"
},
"./*": "./packages/ts/dist/*"
},
"types": "./packages/ts/dist/mod.d.ts",
"sideEffects": false,
"scripts": {
"gen": "buf generate",
"clean": "rimraf dist",
"build": "pnpm run clean && pnpm run gen"
},
"dependencies": {},
"devDependencies": {
"@bufbuild/protoc-gen-es": "^1.9.0",
"rimraf": "^6.0.0"
}
}

View File

@@ -0,0 +1,16 @@
# Meshtastic Protobuf Definitions
[![CI](https://img.shields.io/github/actions/workflow/status/meshtastic/protobufs/ci.yml?branch=master&label=actions&logo=github&color=yellow)](https://github.com/meshtastic/protobufs/actions/workflows/ci.yml)
[![CLA assistant](https://cla-assistant.io/readme/badge/meshtastic/protobufs)](https://cla-assistant.io/meshtastic/protobufs)
[![Fiscal Contributors](https://opencollective.com/meshtastic/tiers/badge.svg?label=Fiscal%20Contributors&color=deeppink)](https://opencollective.com/meshtastic/)
[![Vercel](https://img.shields.io/static/v1?label=Powered%20by&message=Vercel&style=flat&logo=vercel&color=000000)](https://vercel.com?utm_source=meshtastic&utm_campaign=oss)
## Overview
The [Protobuf](https://developers.google.com/protocol-buffers) message definitions for the Meshtastic project (used by apps and the device firmware).
**[Documentation/API Reference](https://buf.build/meshtastic/protobufs)**
## Stats
![Alt](https://repobeats.axiom.co/api/embed/47e9ee1d81d9c0fdd2b4b5b4c673adb1756f6db5.svg "Repobeats analytics image")

View File

@@ -0,0 +1,13 @@
{
"name": "@meshtastic/protobufs",
"version": "__PACKAGE_VERSION__",
"exports": {
".": "./mod.ts"
},
"imports": {
"@bufbuild/protobuf": "npm:@bufbuild/protobuf@^2.9.0"
},
"publish": {
"exclude": ["!dist"]
}
}

384
packages/protobufs/packages/ts/deno.lock generated Normal file
View File

@@ -0,0 +1,384 @@
{
"version": "5",
"specifiers": {
"npm:@bufbuild/protobuf@^2.9.0": "2.9.0",
"npm:tsdown@~0.15.6": "0.15.6_typescript@5.9.3_rolldown@1.0.0-beta.42",
"npm:typescript@^5.9.3": "5.9.3"
},
"npm": {
"@babel/generator@7.28.3": {
"integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==",
"dependencies": [
"@babel/parser",
"@babel/types",
"@jridgewell/gen-mapping",
"@jridgewell/trace-mapping",
"jsesc"
]
},
"@babel/helper-string-parser@7.27.1": {
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="
},
"@babel/helper-validator-identifier@7.27.1": {
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="
},
"@babel/parser@7.28.4": {
"integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
"dependencies": [
"@babel/types"
],
"bin": true
},
"@babel/types@7.28.4": {
"integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
"dependencies": [
"@babel/helper-string-parser",
"@babel/helper-validator-identifier"
]
},
"@bufbuild/protobuf@2.9.0": {
"integrity": "sha512-rnJenoStJ8nvmt9Gzye8nkYd6V22xUAnu4086ER7h1zJ508vStko4pMvDeQ446ilDTFpV5wnoc5YS7XvMwwMqA=="
},
"@emnapi/core@1.5.0": {
"integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==",
"dependencies": [
"@emnapi/wasi-threads",
"tslib"
]
},
"@emnapi/runtime@1.5.0": {
"integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==",
"dependencies": [
"tslib"
]
},
"@emnapi/wasi-threads@1.1.0": {
"integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==",
"dependencies": [
"tslib"
]
},
"@jridgewell/gen-mapping@0.3.13": {
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dependencies": [
"@jridgewell/sourcemap-codec",
"@jridgewell/trace-mapping"
]
},
"@jridgewell/resolve-uri@3.1.2": {
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="
},
"@jridgewell/sourcemap-codec@1.5.5": {
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="
},
"@jridgewell/trace-mapping@0.3.31": {
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"dependencies": [
"@jridgewell/resolve-uri",
"@jridgewell/sourcemap-codec"
]
},
"@napi-rs/wasm-runtime@1.0.7": {
"integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==",
"dependencies": [
"@emnapi/core",
"@emnapi/runtime",
"@tybys/wasm-util"
]
},
"@oxc-project/types@0.94.0": {
"integrity": "sha512-+UgQT/4o59cZfH6Cp7G0hwmqEQ0wE+AdIwhikdwnhWI9Dp8CgSY081+Q3O67/wq3VJu8mgUEB93J9EHHn70fOw=="
},
"@quansync/fs@0.1.5": {
"integrity": "sha512-lNS9hL2aS2NZgNW7BBj+6EBl4rOf8l+tQ0eRY6JWCI8jI2kc53gSoqbjojU0OnAWhzoXiOjFyGsHcDGePB3lhA==",
"dependencies": [
"quansync"
]
},
"@rolldown/binding-android-arm64@1.0.0-beta.42": {
"integrity": "sha512-W5ZKF3TP3bOWuBfotAGp+UGjxOkGV7jRmIRbBA7NFjggx7Oi6vOmGDqpHEIX7kDCiry1cnIsWQaxNvWbMdkvzQ==",
"os": ["android"],
"cpu": ["arm64"]
},
"@rolldown/binding-darwin-arm64@1.0.0-beta.42": {
"integrity": "sha512-abw/wtgJA8OCgaTlL+xJxnN/Z01BwV1rfzIp5Hh9x+IIO6xOBfPsQ0nzi0+rWx3TyZ9FZXyC7bbC+5NpQ9EaXQ==",
"os": ["darwin"],
"cpu": ["arm64"]
},
"@rolldown/binding-darwin-x64@1.0.0-beta.42": {
"integrity": "sha512-Y/UrZIRVr8CvXVEB88t6PeC46r1K9/QdPEo2ASE/b/KBEyXIx+QbM6kv9QfQVWU2Atly2+SVsQzxQsIvuk3lZQ==",
"os": ["darwin"],
"cpu": ["x64"]
},
"@rolldown/binding-freebsd-x64@1.0.0-beta.42": {
"integrity": "sha512-zRM0oOk7BZiy6DoWBvdV4hyEg+j6+WcBZIMHVirMEZRu8hd18kZdJkg+bjVMfCEhwpWeFUfBfZ1qcaZ5UdYzlQ==",
"os": ["freebsd"],
"cpu": ["x64"]
},
"@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.42": {
"integrity": "sha512-6RjFaC52QNwo7ilU8C5H7swbGlgfTkG9pudXwzr3VYyT18s0C9gLg3mvc7OMPIGqNxnQ0M5lU8j6aQCk2DTRVg==",
"os": ["linux"],
"cpu": ["arm"]
},
"@rolldown/binding-linux-arm64-gnu@1.0.0-beta.42": {
"integrity": "sha512-LMYHM5Sf6ROq+VUwHMDVX2IAuEsWTv4SnlFEedBnMGpvRuQ14lCmD4m5Q8sjyAQCgyha9oghdGoK8AEg1sXZKg==",
"os": ["linux"],
"cpu": ["arm64"]
},
"@rolldown/binding-linux-arm64-musl@1.0.0-beta.42": {
"integrity": "sha512-/bNTYb9aKNhzdbPn3O4MK2aLv55AlrkUKPE4KNfBYjkoZUfDr4jWp7gsSlvTc5A/99V1RCm9axvt616ZzeXGyA==",
"os": ["linux"],
"cpu": ["arm64"]
},
"@rolldown/binding-linux-x64-gnu@1.0.0-beta.42": {
"integrity": "sha512-n/SLa4h342oyeGykZdch7Y3GNCNliRPL4k5wkeZ/5eQZs+c6/ZG1SHCJQoy7bZcmxiMyaXs9HoFmv1PEKrZgWg==",
"os": ["linux"],
"cpu": ["x64"]
},
"@rolldown/binding-linux-x64-musl@1.0.0-beta.42": {
"integrity": "sha512-4PSd46sFzqpLHSGdaSViAb1mk55sCUMpJg+X8ittXaVocQsV3QLG/uydSH8RyL0ngHX5fy3D70LcCzlB15AgHw==",
"os": ["linux"],
"cpu": ["x64"]
},
"@rolldown/binding-openharmony-arm64@1.0.0-beta.42": {
"integrity": "sha512-BmWoeJJyeZXmZBcfoxG6J9+rl2G7eO47qdTkAzEegj4n3aC6CBIHOuDcbE8BvhZaEjQR0nh0nJrtEDlt65Q7Sw==",
"os": ["openharmony"],
"cpu": ["arm64"]
},
"@rolldown/binding-wasm32-wasi@1.0.0-beta.42": {
"integrity": "sha512-2Ft32F7uiDTrGZUKws6CLNTlvTWHC33l4vpXrzUucf9rYtUThAdPCOt89Pmn13tNX6AulxjGEP2R0nZjTSW3eQ==",
"dependencies": [
"@napi-rs/wasm-runtime"
],
"cpu": ["wasm32"]
},
"@rolldown/binding-win32-arm64-msvc@1.0.0-beta.42": {
"integrity": "sha512-hC1kShXW/z221eG+WzQMN06KepvPbMBknF0iGR3VMYJLOe9gwnSTfGxFT5hf8XrPv7CEZqTWRd0GQpkSHRbGsw==",
"os": ["win32"],
"cpu": ["arm64"]
},
"@rolldown/binding-win32-ia32-msvc@1.0.0-beta.42": {
"integrity": "sha512-AICBYromawouGjj+GS33369E8Vwhy6UwhQEhQ5evfS8jPCsyVvoICJatbDGDGH01dwtVGLD5eDFzPicUOVpe4g==",
"os": ["win32"],
"cpu": ["ia32"]
},
"@rolldown/binding-win32-x64-msvc@1.0.0-beta.42": {
"integrity": "sha512-XpZ0M+tjoEiSc9c+uZR7FCnOI0uxDRNs1elGOMjeB0pUP1QmvVbZGYNsyLbLoP4u7e3VQN8rie1OQ8/mB6rcJg==",
"os": ["win32"],
"cpu": ["x64"]
},
"@rolldown/pluginutils@1.0.0-beta.42": {
"integrity": "sha512-N7pQzk9CyE7q0bBN/q0J8s6Db279r5kUZc6d7/wWRe9/zXqC52HQovVyu6iXPIDY4BEzzgbVLhVFXrOuGJ22ZQ=="
},
"@tybys/wasm-util@0.10.1": {
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
"dependencies": [
"tslib"
]
},
"ansis@4.2.0": {
"integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="
},
"ast-kit@2.1.3": {
"integrity": "sha512-TH+b3Lv6pUjy/Nu0m6A2JULtdzLpmqF9x1Dhj00ZoEiML8qvVA9j1flkzTKNYgdEhWrjDwtWNpyyCUbfQe514g==",
"dependencies": [
"@babel/parser",
"pathe"
]
},
"birpc@2.6.1": {
"integrity": "sha512-LPnFhlDpdSH6FJhJyn4M0kFO7vtQ5iPw24FnG0y21q09xC7e8+1LeR31S1MAIrDAHp4m7aas4bEkTDTvMAtebQ=="
},
"cac@6.7.14": {
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="
},
"chokidar@4.0.3": {
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"dependencies": [
"readdirp"
]
},
"debug@4.4.3": {
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dependencies": [
"ms"
]
},
"defu@6.1.4": {
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="
},
"diff@8.0.2": {
"integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="
},
"dts-resolver@2.1.2": {
"integrity": "sha512-xeXHBQkn2ISSXxbJWD828PFjtyg+/UrMDo7W4Ffcs7+YWCquxU8YjV1KoxuiL+eJ5pg3ll+bC6flVv61L3LKZg=="
},
"empathic@2.0.0": {
"integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="
},
"fdir@6.5.0_picomatch@4.0.3": {
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dependencies": [
"picomatch"
],
"optionalPeers": [
"picomatch"
]
},
"get-tsconfig@4.12.0": {
"integrity": "sha512-LScr2aNr2FbjAjZh2C6X6BxRx1/x+aTDExct/xyq2XKbYOiG5c0aK7pMsSuyc0brz3ibr/lbQiHD9jzt4lccJw==",
"dependencies": [
"resolve-pkg-maps"
]
},
"hookable@5.5.3": {
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="
},
"jiti@2.6.1": {
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"bin": true
},
"jsesc@3.1.0": {
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
"bin": true
},
"magic-string@0.30.19": {
"integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==",
"dependencies": [
"@jridgewell/sourcemap-codec"
]
},
"ms@2.1.3": {
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"pathe@2.0.3": {
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="
},
"picomatch@4.0.3": {
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="
},
"quansync@0.2.11": {
"integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="
},
"readdirp@4.1.2": {
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="
},
"resolve-pkg-maps@1.0.0": {
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="
},
"rolldown-plugin-dts@0.16.11_rolldown@1.0.0-beta.42_typescript@5.9.3": {
"integrity": "sha512-9IQDaPvPqTx3RjG2eQCK5GYZITo203BxKunGI80AGYicu1ySFTUyugicAaTZWRzFWh9DSnzkgNeMNbDWBbSs0w==",
"dependencies": [
"@babel/generator",
"@babel/parser",
"@babel/types",
"ast-kit",
"birpc",
"debug",
"dts-resolver",
"get-tsconfig",
"magic-string",
"rolldown",
"typescript"
],
"optionalPeers": [
"typescript"
]
},
"rolldown@1.0.0-beta.42": {
"integrity": "sha512-xaPcckj+BbJhYLsv8gOqezc8EdMcKKe/gk8v47B0KPvgABDrQ0qmNPAiT/gh9n9Foe0bUkEv2qzj42uU5q1WRg==",
"dependencies": [
"@oxc-project/types",
"@rolldown/pluginutils",
"ansis"
],
"optionalDependencies": [
"@rolldown/binding-android-arm64",
"@rolldown/binding-darwin-arm64",
"@rolldown/binding-darwin-x64",
"@rolldown/binding-freebsd-x64",
"@rolldown/binding-linux-arm-gnueabihf",
"@rolldown/binding-linux-arm64-gnu",
"@rolldown/binding-linux-arm64-musl",
"@rolldown/binding-linux-x64-gnu",
"@rolldown/binding-linux-x64-musl",
"@rolldown/binding-openharmony-arm64",
"@rolldown/binding-wasm32-wasi",
"@rolldown/binding-win32-arm64-msvc",
"@rolldown/binding-win32-ia32-msvc",
"@rolldown/binding-win32-x64-msvc"
],
"bin": true
},
"semver@7.7.3": {
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"bin": true
},
"tinyexec@1.0.1": {
"integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="
},
"tinyglobby@0.2.15_picomatch@4.0.3": {
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"dependencies": [
"fdir",
"picomatch"
]
},
"tree-kill@1.2.2": {
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
"bin": true
},
"tsdown@0.15.6_typescript@5.9.3_rolldown@1.0.0-beta.42": {
"integrity": "sha512-W6++O3JeV9gm3JY6P/vLiC7zzTcJbZhQxXb+p3AvRMpDOPBIg82yXULyZCcwjsihY/bFG+Qw37HkezZbP7fzUg==",
"dependencies": [
"ansis",
"cac",
"chokidar",
"debug",
"diff",
"empathic",
"hookable",
"rolldown",
"rolldown-plugin-dts",
"semver",
"tinyexec",
"tinyglobby",
"tree-kill",
"typescript",
"unconfig"
],
"optionalPeers": [
"typescript"
],
"bin": true
},
"tslib@2.8.1": {
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
"typescript@5.9.3": {
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"bin": true
},
"unconfig@7.3.3": {
"integrity": "sha512-QCkQoOnJF8L107gxfHL0uavn7WD9b3dpBcFX6HtfQYmjw2YzWxGuFQ0N0J6tE9oguCBJn9KOvfqYDCMPHIZrBA==",
"dependencies": [
"@quansync/fs",
"defu",
"jiti",
"quansync"
]
}
},
"workspace": {
"dependencies": [
"npm:@bufbuild/protobuf@^2.9.0"
],
"packageJson": {
"dependencies": [
"npm:@bufbuild/protobuf@^2.9.0",
"npm:tsdown@~0.15.6",
"npm:typescript@^5.9.3"
]
}
}
}

View File

@@ -0,0 +1,20 @@
export * as Admin from "./dist/admin_pb.ts";
export * as AppOnly from "./dist/apponly_pb.ts";
export * as ATAK from "./dist/atak_pb.ts";
export * as CannedMessages from "./dist/cannedmessages_pb.ts";
export * as Channel from "./dist/channel_pb.ts";
export * as ClientOnly from "./dist/clientonly_pb.ts";
export * as Config from "./dist/config_pb.ts";
export * as ConnectionStatus from "./dist/connection_status_pb.ts";
export * as LocalOnly from "./dist/localonly_pb.ts";
export * as Mesh from "./dist/mesh_pb.ts";
export * as ModuleConfig from "./dist/module_config_pb.ts";
export * as Mqtt from "./dist/mqtt_pb.ts";
export * as PaxCount from "./dist/paxcount_pb.ts";
export * as Portnums from "./dist/portnums_pb.ts";
export * as PowerMon from "./dist/powermon_pb.ts";
export * as RemoteHardware from "./dist/remote_hardware_pb.ts";
export * as Rtttl from "./dist/rtttl_pb.ts";
export * as StoreForward from "./dist/storeforward_pb.ts";
export * as Telemetry from "./dist/telemetry_pb.ts";
export * as Xmodem from "./dist/xmodem_pb.ts";

View File

@@ -1,15 +1,29 @@
{
"name": "@meshtastic/transport-deno",
"version": "0.1.1",
"description": "Deno-specific transport layer for Meshtastic web applications.",
"exports": {
".": "./mod.ts"
},
"main": "./dist/mod.mjs",
"module": "./dist/mod.mjs",
"types": "./dist/mod.d.mts",
"files": ["dist/*", "mod.ts", "README.md", "../../LICENSE"],
"name": "@meshtastic/transport-deno",
"version": "0.1.1",
"description": "Deno-specific transport layer for Meshtastic web applications.",
"exports": {
".": "./mod.ts"
},
"main": "./dist/mod.js",
"module": "./dist/mod.js",
"types": "./dist/mod.d.ts",
"license": "GPL-3.0-only",
"tsdown": {
"entry": "mod.ts",
"dts": true,
"format": [
"esm"
],
"splitting": false,
"clean": true
},
"files": [
"package.json",
"README.md",
"LICENSE",
"dist"
],
"scripts": {
"preinstall": "npx only-allow pnpm",
"prepack": "cp ../../LICENSE ./LICENSE",
@@ -18,5 +32,8 @@
"publish:npm": "pnpm clean && pnpm build:npm && pnpm publish --access public",
"prepare:jsr": "rm -rf dist && pnpm dlx pkg-to-jsr",
"publish:jsr": "pnpm run prepack && pnpm prepare:jsr && deno publish --allow-dirty --no-check"
},
"dependencies": {
"@meshtastic/core": "workspace:*"
}
}

View File

@@ -13,7 +13,8 @@ await build({
// package.json properties
name: "@meshtastic/transport-deno",
version: Deno.args[0],
description: "A Deno transport layer for your project, enabling seamless integration with npm.",
description:
"A Deno transport layer for your project, enabling seamless integration with npm.",
license: "GPL-3.0-only",
repository: {
type: "git",

View File

@@ -16,9 +16,10 @@ export class TransportDeno implements Types.Transport {
constructor(connection: Deno.Conn) {
this.connection = connection;
Utils.toDeviceStream.readable.pipeTo(this.connection.writable);
const toDeviceStream = Utils.toDeviceStream();
toDeviceStream.readable.pipeTo(this.connection.writable);
this._toDevice = Utils.toDeviceStream.writable;
this._toDevice = toDeviceStream.writable;
this._fromDevice = this.connection.readable.pipeThrough(
Utils.fromDeviceStream(),
);

View File

@@ -1,32 +1,50 @@
{
"name": "@meshtastic/transport-http",
"version": "0.2.3",
"description": "A transport layer for Meshtastic applications using HTTP.",
"exports": {".": "./mod.ts"},
"main": "./dist/mod.mjs",
"module": "./dist/mod.mjs",
"types": "./dist/mod.d.mts",
"license": "GPL-3.0-only",
"tsdown": {
"entry": ["mod.ts"],
"dts": true,
"format": ["esm"],
"splitting": false,
"clean": true
"name": "@meshtastic/transport-http",
"version": "0.2.5",
"description": "A transport layer for Meshtastic applications using HTTP.",
"exports": {
".": "./mod.ts"
},
"type": "module",
"files": [
"package.json",
"README.md",
"LICENSE",
"dist"
],
"main": "./dist/mod.js",
"module": "./dist/mod.js",
"types": "./dist/mod.d.ts",
"license": "GPL-3.0-only",
"tsdown": {
"entry": "mod.ts",
"dts": true,
"format": [
"esm"
],
"splitting": false,
"clean": true
},
"jsrInclude": [
"mod.ts",
"src",
"README.md",
"LICENSE"
],
"jsrExclude": [
"src/**/*.test.ts"
],
"scripts": {
"preinstall": "npx only-allow pnpm",
"preinstall": "npx only-allow pnpm",
"prepack": "cp ../../LICENSE ./LICENSE",
"clean": "rm -rf dist LICENSE",
"build:npm": "tsdown",
"publish:npm": "pnpm clean && pnpm build:npm && pnpm publish --access public",
"publish:npm": "pnpm clean && pnpm build:npm && pnpm publish --access public --no-git-checks",
"prepare:jsr": "rm -rf dist && pnpm dlx pkg-to-jsr",
"publish:jsr": "pnpm run prepack && pnpm prepare:jsr && deno publish --allow-dirty --no-check"
},
"dependencies": {
"@meshtastic/core": "workspace:*"
}
}

View File

@@ -0,0 +1,247 @@
import {
afterEach,
beforeEach,
describe,
expect,
it,
type MockInstance,
vi,
} from "vitest";
import { runTransportContract } from "../../../tests/utils/transportContract.ts";
import { TransportHTTP } from "./transport.ts";
let abortTimeoutSpy: MockInstance | undefined;
beforeEach(() => {
abortTimeoutSpy = vi
.spyOn(
globalThis.AbortSignal as unknown as { timeout(ms: number): AbortSignal },
"timeout",
)
.mockImplementation((ms: number) => {
const ctrl = new AbortController();
const abort = () =>
ctrl.abort(new DOMException("Timeout reached", "TimeoutError"));
// Uses setTimeout so vi.useFakeTimers() can fast-forward it
setTimeout(abort, ms);
return ctrl.signal;
});
});
afterEach(() => {
abortTimeoutSpy?.mockRestore();
});
function stubFetch() {
const inbox: Uint8Array[] = [];
let lastWritten: ArrayBuffer | undefined;
let forceNextReadToHang = false;
let forceNextReadToReturn500 = false;
function makeAbortAwareHang(signal?: AbortSignal): Promise<Response> {
return new Promise((_, reject) => {
const abort = () => reject(new DOMException("Aborted", "AbortError"));
if (signal?.aborted) {
abort();
return;
}
if (signal) {
signal.addEventListener("abort", abort, { once: true });
}
});
}
const mockFetch = vi.fn(async (url: string, init?: RequestInit) => {
const method = (init?.method ?? "GET").toUpperCase();
if (url.includes("/api/v1/toradio") && method === "OPTIONS") {
return { ok: true, status: 204 } as Response;
}
if (url.includes("/api/v1/toradio") && method === "PUT") {
lastWritten = init?.body as ArrayBuffer;
return { ok: true, status: 200 } as Response;
}
if (url.includes("/api/v1/fromradio") && method === "GET") {
if (forceNextReadToHang) {
forceNextReadToHang = false;
return makeAbortAwareHang(init?.signal ?? undefined);
}
if (forceNextReadToReturn500) {
forceNextReadToReturn500 = false;
return {
ok: false,
status: 500,
arrayBuffer: async () => new ArrayBuffer(0),
} as Response;
}
const next = inbox.shift() ?? new Uint8Array();
return {
ok: true,
status: 200,
arrayBuffer: async () => next.buffer,
} as Response;
}
return { ok: true, status: 200 } as Response;
});
vi.stubGlobal("fetch", mockFetch);
return {
pushIncoming: (u8: Uint8Array) => inbox.push(u8),
assertLastWritten: (u8: Uint8Array) => {
const got = new Uint8Array(lastWritten || new ArrayBuffer(0));
expect(got).toEqual(u8);
},
forceReadErrorOnce: () => {
forceNextReadToReturn500 = true;
},
forceReadTimeoutOnce: () => {
forceNextReadToHang = true;
},
getMock: () => mockFetch,
cleanup: () => vi.unstubAllGlobals(),
};
}
async function tickNextTimer() {
try {
await vi.advanceTimersToNextTimerAsync();
} catch {
await new Promise((r) => setTimeout(r, 5));
}
}
describe("TransportHTTP (contract)", () => {
runTransportContract({
name: "TransportHTTP",
setup: () => {
vi.useFakeTimers();
},
teardown: () => {
vi.useRealTimers();
vi.restoreAllMocks();
vi.unstubAllGlobals();
},
create: async () => {
(
globalThis as unknown as { __http: ReturnType<typeof stubFetch> }
).__http = stubFetch();
const transport = await TransportHTTP.create("127.0.0.1:80", false);
await tickNextTimer();
return transport;
},
pushIncoming: async (bytes) => {
(
globalThis as unknown as { __http: ReturnType<typeof stubFetch> }
).__http.pushIncoming(bytes);
await tickNextTimer();
},
assertLastWritten: (bytes) => {
(
globalThis as unknown as { __http: ReturnType<typeof stubFetch> }
).__http.assertLastWritten(bytes);
},
triggerDisconnect: async () => {
(
globalThis as unknown as { __http: ReturnType<typeof stubFetch> }
).__http.forceReadErrorOnce();
await tickNextTimer();
},
});
});
describe("TransportHTTP (extras)", () => {
let httpStub: ReturnType<typeof stubFetch> | undefined;
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
vi.restoreAllMocks();
httpStub?.cleanup();
httpStub = undefined;
});
async function createTransport(): Promise<TransportHTTP> {
httpStub = stubFetch();
const transport = await TransportHTTP.create("127.0.0.1:80", false);
await tickNextTimer();
return transport;
}
async function advanceOnePoll() {
await tickNextTimer();
}
it("emits DeviceDisconnected with reason 'read-timeout' when GET /fromradio hangs", async () => {
const transport = await createTransport();
const reader = transport.fromDevice.getReader();
httpStub!.forceReadTimeoutOnce();
await tickNextTimer();
await vi.advanceTimersByTimeAsync(8000);
let sawReadTimeout = false;
for (let i = 0; i < 6; i++) {
const { value } = await reader.read();
if (value?.type === "status" && value.data.reason === "read-timeout") {
sawReadTimeout = true;
break;
}
}
expect(sawReadTimeout).toBe(true);
reader.releaseLock();
await transport.disconnect();
});
it("stops polling after disconnect()", async () => {
const transport = await createTransport();
const fetchMock = httpStub!.getMock();
const callsBeforeDisconnect = fetchMock.mock.calls.length;
await transport.disconnect();
await advanceOnePoll();
await vi.runOnlyPendingTimersAsync();
const callsAfterDisconnect = fetchMock.mock.calls.length;
expect(callsAfterDisconnect).toBe(callsBeforeDisconnect);
});
it("emits DeviceDisconnected with reason 'read-timeout' when GET /fromradio hangs", async () => {
const transport = await createTransport();
const reader = transport.fromDevice.getReader();
httpStub!.forceReadTimeoutOnce();
await vi.advanceTimersToNextTimerAsync();
await vi.advanceTimersByTimeAsync(8000);
await Promise.resolve();
await Promise.resolve();
let sawReadTimeout = false;
for (let i = 0; i < 6; i++) {
const { value } = await reader.read();
if (value?.type === "status" && value.data.reason === "read-timeout") {
sawReadTimeout = true;
break;
}
}
expect(sawReadTimeout).toBe(true);
reader.releaseLock();
await transport.disconnect();
});
});

View File

@@ -1,14 +1,49 @@
import type { Types } from "@meshtastic/core";
import { Types } from "@meshtastic/core";
const FETCH_INTERVAL_MS = 3000;
const READ_TIMEOUT_MS = 7000;
const WRITE_TIMEOUT_MS = 4000;
function toArrayBuffer(uint8array: Uint8Array): ArrayBuffer {
if (
uint8array.buffer instanceof ArrayBuffer &&
uint8array.byteOffset === 0 &&
uint8array.byteLength === uint8array.buffer.byteLength
) {
return uint8array.buffer;
}
return uint8array.slice().buffer;
}
/**
* Provides HTTP(S) transport for Meshtastic devices.
*
* Implements {@link Types.Transport} using the device's HTTP API.
* Polls `/api/v1/fromradio` for incoming packets and writes to `/api/v1/toradio`.
*/
export class TransportHTTP implements Types.Transport {
private _toDevice: WritableStream<Uint8Array>;
private _fromDevice: ReadableStream<Types.DeviceOutput>;
private fromDeviceController?: ReadableStreamDefaultController<Types.DeviceOutput>;
private url: string;
private receiveBatchRequests: boolean;
private fetchInterval: number;
private fetching: boolean;
private interval: ReturnType<typeof setInterval> | undefined;
private inflightReadController?: AbortController;
private lastStatus: Types.DeviceStatusEnum =
Types.DeviceStatusEnum.DeviceDisconnected;
private closingByUser = false;
/**
* Probe the device and return a connected HTTP transport.
*
* @param address Hostname or IP address (with optional port).
* @param tls Use HTTPS if true, HTTP otherwise.
*/
public static async create(
address: string,
tls?: boolean,
@@ -17,97 +52,194 @@ export class TransportHTTP implements Types.Transport {
await fetch(`${connectionUrl}/api/v1/toradio`, {
method: "OPTIONS",
});
await Promise.resolve();
return new TransportHTTP(connectionUrl);
}
/**
* Construct a new HTTP transport for the given device URL.
*
* @param url Base URL of the device (`http://host:port` or `https://host:port`).
*/
constructor(url: string) {
this.url = url;
this.receiveBatchRequests = false;
this.fetchInterval = 3000;
this.fetchInterval = FETCH_INTERVAL_MS;
this.fetching = false;
this._toDevice = new WritableStream<Uint8Array>({
write: async (chunk) => {
await this.writeToRadio(chunk);
try {
await this.writeToRadio(chunk);
} catch (error) {
if (!this.closingByUser) {
this.emitStatus(
Types.DeviceStatusEnum.DeviceDisconnected,
this.isTimeoutOrAbort(error) ? "write-timeout" : "write-error",
);
return;
}
throw error;
}
},
});
let controller: ReadableStreamDefaultController<Types.DeviceOutput>;
this._fromDevice = new ReadableStream<Types.DeviceOutput>({
start: (ctrl) => {
controller = ctrl;
this.fromDeviceController = ctrl;
this.emitStatus(Types.DeviceStatusEnum.DeviceConnecting);
// Start polling immediately
void this.safePoll();
this.interval = setInterval(
() => void this.safePoll(),
this.fetchInterval,
);
},
cancel: () => {
if (this.interval) {
clearInterval(this.interval);
}
this.interval = undefined;
},
});
this.interval = setInterval(async () => {
if (this.fetching) {
// We still have the previous request open
return;
}
this.fetching = true;
try {
await this.readFromRadio(controller);
} catch {
// TODO: Emit disconnection events for certain types of errors
}
this.fetching = false;
}, this.fetchInterval);
}
private async readFromRadio(
controller: ReadableStreamDefaultController<Types.DeviceOutput>,
): Promise<void> {
/** Poll `/api/v1/fromradio` and enqueue incoming packets. */
private async readFromRadio(): Promise<void> {
let readBuffer = new ArrayBuffer(1);
while (readBuffer.byteLength > 0) {
const response = await fetch(
`${this.url}/api/v1/fromradio?all=${
this.receiveBatchRequests ? "true" : "false"
}`,
{
method: "GET",
headers: {
Accept: "application/x-protobuf",
const inflight = new AbortController();
this.inflightReadController = inflight;
const signal = AbortSignal.any([
inflight.signal,
AbortSignal.timeout(READ_TIMEOUT_MS),
]);
try {
const response = await fetch(
`${this.url}/api/v1/fromradio?all=${this.receiveBatchRequests ? "true" : "false"}`,
{
method: "GET",
headers: { Accept: "application/x-protobuf" },
signal,
},
},
);
);
if (!response.ok) {
throw new Error(
`fromradio ${response.status} ${response.statusText}`,
);
}
readBuffer = await response.arrayBuffer();
this.emitStatus(Types.DeviceStatusEnum.DeviceConnected);
if (readBuffer.byteLength > 0) {
controller.enqueue({
type: "packet",
data: new Uint8Array(readBuffer),
});
readBuffer = await response.arrayBuffer();
if (readBuffer.byteLength > 0) {
this.fromDeviceController?.enqueue({
type: "packet",
data: new Uint8Array(readBuffer),
});
}
} finally {
this.inflightReadController = undefined;
}
}
}
/** Write a protobuf-encoded request to `/api/v1/toradio`. */
private async writeToRadio(data: Uint8Array): Promise<void> {
await fetch(`${this.url}/api/v1/toradio`, {
method: "PUT",
headers: {
"Content-Type": "application/x-protobuf",
},
body: data,
});
try {
const response = await fetch(`${this.url}/api/v1/toradio`, {
method: "PUT",
headers: { "Content-Type": "application/x-protobuf" },
body: toArrayBuffer(data),
signal: AbortSignal.timeout(WRITE_TIMEOUT_MS),
});
if (!response.ok) {
throw new Error(`toradio ${response.status} ${response.statusText}`);
}
} catch (error) {
if (!this.closingByUser) {
this.emitStatus(
Types.DeviceStatusEnum.DeviceDisconnected,
this.isTimeoutOrAbort(error) ? "write-timeout" : "write-error",
);
return;
}
throw error;
}
}
/** Writable stream of bytes to the device. */
get toDevice(): WritableStream<Uint8Array> {
return this._toDevice;
}
/** Readable stream of {@link Types.DeviceOutput} from the device. */
get fromDevice(): ReadableStream<Types.DeviceOutput> {
return this._fromDevice;
}
/**
* Stop polling and emit `DeviceDisconnected("user")`.
*/
disconnect(): Promise<void> {
this.fetching = false;
this.closingByUser = true;
if (this.interval) {
clearInterval(this.interval);
}
this.interval = undefined;
this.fetching = false;
try {
this.inflightReadController?.abort();
} catch {}
this.inflightReadController = undefined;
this.emitStatus(Types.DeviceStatusEnum.DeviceDisconnected, "user");
return Promise.resolve();
}
private emitStatus(next: Types.DeviceStatusEnum, reason?: string): void {
if (next === this.lastStatus) {
return;
}
this.lastStatus = next;
this.fromDeviceController?.enqueue({
type: "status",
data: { status: next, reason },
});
}
private isTimeoutOrAbort(err: unknown): boolean {
return (
(err instanceof DOMException &&
(err.name === "AbortError" || err.name === "TimeoutError")) ||
(err instanceof Error &&
(err.name === "AbortError" || err.name === "TimeoutError"))
);
}
private async safePoll(): Promise<void> {
if (this.fetching) {
return;
}
this.fetching = true;
try {
await this.readFromRadio();
} catch (error) {
if (!this.closingByUser) {
this.emitStatus(
Types.DeviceStatusEnum.DeviceDisconnected,
this.isTimeoutOrAbort(error) ? "read-timeout" : "read-error",
);
}
} finally {
this.fetching = false;
}
}
}

View File

@@ -7,7 +7,7 @@
"outDir": "./dist",
"moduleResolution": "bundler",
"emitDeclarationOnly": false,
"esModuleInterop": true,
"esModuleInterop": true
},
"include": ["src"]
}
}

View File

@@ -0,0 +1,28 @@
# @meshtastic/transport-node-serial
[![JSR](https://jsr.io/badges/@meshtastic/transport-node-serial)](https://jsr.io/@meshtastic/transport-node-serial)
[![CI](https://img.shields.io/github/actions/workflow/status/meshtastic/js/ci.yml?branch=master&label=actions&logo=github&color=yellow)](https://github.com/meshtastic/js/actions/workflows/ci.yml)
[![CLA assistant](https://cla-assistant.io/readme/badge/meshtastic/meshtastic.js)](https://cla-assistant.io/meshtastic/meshtastic.js)
[![Fiscal Contributors](https://opencollective.com/meshtastic/tiers/badge.svg?label=Fiscal%20Contributors&color=deeppink)](https://opencollective.com/meshtastic/)
[![Vercel](https://img.shields.io/static/v1?label=Powered%20by&message=Vercel&style=flat&logo=vercel&color=000000)](https://vercel.com?utm_source=meshtastic&utm_campaign=oss)
## Overview
`@meshtastic/transport-noden-node` Provides Serial transport (Node) for Meshtastic
devices. Installation instructions are available at
[JSR](https://jsr.io/@meshtastic/transport-node-serial)
[NPM](https://www.npmjs.com/package/@meshtastic/transport-node-serial)
## Usage
```ts
import { MeshDevice } from "@meshtastic/core";
import { TransportNodeSerial } from "@meshtastic/transport-node-serial";
const transport = await TransportNodeSerial.create("/dev/cu.usbserial-0001");
const device = new MeshDevice(transport);
```
## Stats
![Alt](https://repobeats.axiom.co/api/embed/5330641586e92a2ec84676fedb98f6d4a7b25d69.svg "Repobeats analytics image")

View File

@@ -0,0 +1 @@
export { TransportNodeSerial } from "./src/transport.ts";

View File

@@ -0,0 +1,50 @@
{
"name": "@meshtastic/transport-node-serial",
"version": "0.0.2",
"description": "NodeJS-specific serial transport layer for Meshtastic web applications.",
"exports": {
".": "./mod.ts"
},
"type": "module",
"main": "./dist/mod.js",
"module": "./dist/mod.js",
"types": "./dist/mod.d.ts",
"files": [
"package.json",
"README.md",
"LICENSE",
"dist"
],
"license": "GPL-3.0-only",
"tsdown": {
"entry": "mod.ts",
"dts": true,
"format": [
"esm"
],
"splitting": false,
"clean": true
},
"jsrInclude": [
"mod.ts",
"src",
"README.md",
"LICENSE"
],
"jsrExclude": [
"src/**/*.test.ts"
],
"scripts": {
"preinstall": "npx only-allow pnpm",
"prepack": "cp ../../LICENSE ./LICENSE",
"clean": "rm -rf dist LICENSE",
"build:npm": "tsdown",
"publish:npm": "pnpm clean && pnpm build:npm && pnpm publish --access public --no-git-checks",
"prepare:jsr": "rm -rf dist && pnpm dlx pkg-to-jsr",
"publish:jsr": "pnpm run prepack && pnpm prepare:jsr && deno publish --allow-dirty --no-check"
},
"dependencies": {
"@meshtastic/core": "workspace:*",
"serialport": "^13.0.0"
}
}

View File

@@ -0,0 +1,189 @@
import { Duplex } from "node:stream";
import { Types, Utils } from "@meshtastic/core";
import type { SerialPort } from "serialport";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { runTransportContract } from "../../../tests/utils/transportContract.ts";
import { TransportNodeSerial } from "./transport.ts";
function isStatusEvent(
output: Types.DeviceOutput | undefined,
): output is Extract<Types.DeviceOutput, { type: "status" }> {
return output !== undefined && output.type === "status";
}
class FakeSerialPort extends Duplex {
public lastWritten: Uint8Array | undefined;
constructor() {
super({ objectMode: false });
}
_read() {}
_write(
chunk: Buffer,
_encoding: BufferEncoding,
callback: (error?: Error | null) => void,
) {
this.lastWritten = new Uint8Array(
chunk.buffer,
chunk.byteOffset,
chunk.byteLength,
);
callback();
}
pushIncoming(data: Uint8Array) {
const buf = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
this.push(buf);
}
emitErrorOnce(message = "simulated serial error") {
this.emit("error", new Error(message));
}
emitClose() {
this.emit("close");
}
close() {
this.destroy();
this.emit("close");
}
}
function stubCoreTransforms() {
const toDevice = () =>
new TransformStream<Uint8Array, Uint8Array>({
transform(chunk, controller) {
controller.enqueue(chunk);
},
});
const fromDeviceFactory = () =>
new TransformStream<Uint8Array, Types.DeviceOutput>({
transform(chunk, controller) {
controller.enqueue({ type: "packet", data: chunk });
},
});
// Utils.toDeviceStream is a getter
const transform = Utils.toDeviceStream;
vi.spyOn(Utils, "toDeviceStream", "get").mockReturnValue(
toDevice as unknown as typeof transform,
);
vi.spyOn(Utils, "fromDeviceStream").mockImplementation(
() =>
fromDeviceFactory() as unknown as TransformStream<
Uint8Array,
Types.DeviceOutput
>,
);
return {
restore: () => vi.restoreAllMocks(),
};
}
describe("TransportNodeSerial (contract)", () => {
let transformsStub: { restore: () => void } | undefined;
beforeEach(() => {
transformsStub = stubCoreTransforms();
});
afterEach(() => {
transformsStub?.restore();
});
runTransportContract({
name: "TransportNodeSerial",
setup: () => {},
teardown: () => {
vi.restoreAllMocks();
},
create: async () => {
const fakePort = new FakeSerialPort();
const transport = new TransportNodeSerial(
fakePort as unknown as SerialPort,
);
await Promise.resolve();
(globalThis as unknown as { __fakePort: FakeSerialPort }).__fakePort =
fakePort;
return transport;
},
pushIncoming: async (bytes) => {
(
globalThis as unknown as { __fakePort: FakeSerialPort }
).__fakePort.pushIncoming(bytes);
await Promise.resolve();
},
assertLastWritten: (bytes) => {
const port = (globalThis as unknown as { __fakePort: FakeSerialPort })
.__fakePort;
expect(port.lastWritten).toBeDefined();
expect(port.lastWritten).toEqual(bytes);
},
triggerDisconnect: async () => {
(
globalThis as unknown as { __fakePort: FakeSerialPort }
).__fakePort.emitErrorOnce("test-disconnect");
await Promise.resolve();
},
});
});
describe("TransportNodeSerial (extras)", () => {
let transformsStub: { restore: () => void } | undefined;
beforeEach(() => {
transformsStub = stubCoreTransforms();
});
afterEach(() => {
transformsStub?.restore();
});
it("emits DeviceDisconnected with reason 'port-closed' on close event", async () => {
const fakePort = new FakeSerialPort();
const transport = new TransportNodeSerial(
fakePort as unknown as SerialPort,
);
const reader = transport.fromDevice.getReader();
await Promise.resolve();
const first = await reader.read();
expect(isStatusEvent(first.value)).toBe(true);
if (isStatusEvent(first.value)) {
expect(first.value.data.status).toBe(
Types.DeviceStatusEnum.DeviceConnecting,
);
}
const second = await reader.read();
expect(isStatusEvent(second.value)).toBe(true);
if (isStatusEvent(second.value)) {
expect(second.value.data.status).toBe(
Types.DeviceStatusEnum.DeviceConnected,
);
}
fakePort.emitClose();
await Promise.resolve();
let sawClosed = false;
for (let i = 0; i < 6; i++) {
const { value } = await reader.read();
if (isStatusEvent(value) && value.data.reason === "port-closed") {
sawClosed = true;
break;
}
}
expect(sawClosed).toBe(true);
reader.releaseLock();
await transport.disconnect();
});
});

View File

@@ -0,0 +1,182 @@
import { Readable, Writable } from "node:stream";
import { Types, Utils } from "@meshtastic/core";
import { SerialPort } from "serialport";
/**
* Node.js Serial transport for Meshtastic.
*
* Implements {@link Types.Transport} on top of a Node `SerialPort`.
* Use {@link TransportNodeSerial.create} for a convenient factory, or
* `new TransportNodeSerial(port)` if you already have an open port.
*/
export class TransportNodeSerial implements Types.Transport {
private readonly _toDevice: WritableStream<Uint8Array>;
private readonly _fromDevice: ReadableStream<Types.DeviceOutput>;
private fromDeviceController?: ReadableStreamDefaultController<Types.DeviceOutput>;
private port: SerialPort | undefined;
private pipePromise?: Promise<void>;
private abortController: AbortController;
private lastStatus: Types.DeviceStatusEnum =
Types.DeviceStatusEnum.DeviceDisconnected;
private closingByUser = false;
/**
* Creates and connects a new TransportNode instance.
* @param path - Path to the serial device
* @param baudRate - Baud rate for the serial connection (default is 115200).
* @returns A promise that resolves with a connected TransportNode instance.
*/
public static create(
path: string,
baudRate = 115200,
): Promise<TransportNodeSerial> {
return new Promise((resolve, reject) => {
const port = new SerialPort({
path,
baudRate,
autoOpen: true,
});
const onError = (err: Error) => {
port.close();
reject(err);
};
port.once("error", onError);
port.on("open", () => {
port.removeListener("error", onError);
resolve(new TransportNodeSerial(port));
});
});
}
/**
* Constructs a new TransportNode.
* @param port - An active Node.js SerialPort connection.
*/
constructor(port: SerialPort) {
this.port = port;
this.port.on("error", (err) => {
console.error("Serial port connection error:", err);
this.emitStatus(Types.DeviceStatusEnum.DeviceDisconnected, "port-error");
});
this.port.on("close", () => {
if (this.closingByUser) {
return;
}
this.emitStatus(Types.DeviceStatusEnum.DeviceDisconnected, "port-closed");
});
const fromDeviceSource = Readable.toWeb(port) as ReadableStream<Uint8Array>;
const transformed = fromDeviceSource.pipeThrough(Utils.fromDeviceStream());
this.abortController = new AbortController();
const controller = this.abortController;
this._fromDevice = new ReadableStream<Types.DeviceOutput>({
start: async (ctrl) => {
this.fromDeviceController = ctrl;
this.emitStatus(Types.DeviceStatusEnum.DeviceConnecting);
this.emitStatus(Types.DeviceStatusEnum.DeviceConnected);
const reader = transformed.getReader();
try {
while (true) {
const { value, done } = await reader.read();
if (done) {
break;
}
ctrl.enqueue(value);
}
ctrl.close();
} catch (error) {
if (this.closingByUser) {
ctrl.close(); // graceful EOF on user
} else {
this.emitStatus(
Types.DeviceStatusEnum.DeviceDisconnected,
"read-error",
);
ctrl.error(
error instanceof Error ? error : new Error(String(error)),
);
}
try {
await transformed.cancel();
} catch {}
} finally {
reader.releaseLock();
}
},
});
// Stream for data going FROM the application TO the Meshtastic device.
const toDeviceTransform = Utils.toDeviceStream();
this._toDevice = toDeviceTransform.writable;
this.pipePromise = toDeviceTransform.readable
.pipeTo(Writable.toWeb(port) as WritableStream<Uint8Array>, {
signal: controller.signal,
})
.catch((error) => {
if (controller.signal.aborted || this.closingByUser) {
return;
}
console.error("Error piping data to serial port:", error);
this.emitStatus(
Types.DeviceStatusEnum.DeviceDisconnected,
"write-error",
);
try {
this.port?.close();
} catch {}
});
}
/**
* The WritableStream to send data to the Meshtastic device.
*/
public get toDevice(): WritableStream<Uint8Array> {
return this._toDevice;
}
/**
* The ReadableStream to receive data from the Meshtastic device.
*/
public get fromDevice(): ReadableStream<Types.DeviceOutput> {
return this._fromDevice;
}
/**
* Disconnect from the serial port and emit `DeviceDisconnected("user")`.
* Safe to call multiple times.
*/
async disconnect() {
try {
this.closingByUser = true;
this.emitStatus(Types.DeviceStatusEnum.DeviceDisconnected, "user");
this.abortController?.abort();
await this.pipePromise?.catch(() => {});
try {
this.port?.close();
} catch {}
} finally {
this.port = undefined;
this.closingByUser = false;
}
}
private emitStatus(next: Types.DeviceStatusEnum, reason?: string): void {
if (next === this.lastStatus) {
return;
}
this.lastStatus = next;
this.fromDeviceController?.enqueue({
type: "status",
data: { status: next, reason },
});
}
}

Some files were not shown because too many files have changed in this diff Show More