Compare commits
360 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e0f8ae611 | |||
| dd7e931c80 | |||
| f475b4cbb1 | |||
| c78d7f2568 | |||
| 177901c974 | |||
| 29bf765947 | |||
| 90311fc8fb | |||
| 3e86f0adaf | |||
| 5b55606415 | |||
| 38bc4f443b | |||
| 324c8098a9 | |||
| ac3212d781 | |||
| 5f6c09b893 | |||
| 5540258abc | |||
| cb06516bea | |||
| 3dea6e7f84 | |||
| 0f6c435f66 | |||
| 3e78cd73bd | |||
| 2b8e1f34b0 | |||
| 467b96a857 | |||
| 7df0080e88 | |||
| 1b48ebd786 | |||
| 03fe29a313 | |||
| 0ec696f64f | |||
| 7b190b78e4 | |||
| b766e8e9b8 | |||
| b603b00110 | |||
| 8a5981644d | |||
| eb9853e889 | |||
| 81b5e6bb15 | |||
| 7a7ac87640 | |||
| 009701e71c | |||
| 2c52a450ae | |||
| f7f4ac3a50 | |||
| 71051062a3 | |||
| af637182dc | |||
| 8d171a749c | |||
| 01e5cff99e | |||
| 0d1ffd3f38 | |||
| 7738eeed81 | |||
| a6cc7f77fe | |||
| 0f2f30f2d2 | |||
| b392c630aa | |||
| 3f4a275aec | |||
| 3f9e5fccae | |||
| b28bfe0f56 | |||
| d60d53fc0b | |||
| 8755a658a8 | |||
| 1874c6206c | |||
| 92422bbf99 | |||
| 277cbd10ac | |||
| e182471f8c | |||
| 331d69b83b | |||
| bcc9b599ca | |||
| a596121acf | |||
| f929f2f142 | |||
| ed871cdeef | |||
| 36200942cc | |||
| b28d3ee448 | |||
| a441b1a205 | |||
| f9cafc5261 | |||
| 9c0a27c8ba | |||
| 589485e31c | |||
| 417f52d759 | |||
| 67bf599149 | |||
| ba27c12494 | |||
| c1d520d700 | |||
| 1847b58935 | |||
| 2be3c2cb1a | |||
| e12e7351d9 | |||
| 20175fa059 | |||
| 8cbbf3f357 | |||
| 79a482a09e | |||
| 73ad50c732 | |||
| 07e2a11253 | |||
| 8865950405 | |||
| b72945a78a | |||
| cde254cdf0 | |||
| b7dc88f6ab | |||
| f78c59a954 | |||
| 49ac8a6da3 | |||
| 2461efdc72 | |||
| e5ebde454e | |||
| d2687956cd | |||
| 87f6faee79 | |||
| 69b1a978e9 | |||
| 6074fdeb71 | |||
| 7dc61300d1 | |||
| 97c0e75eec | |||
| efeb3ad119 | |||
| 36d742e4d7 | |||
| ab2cae5746 | |||
| 7a7c74a747 | |||
| cc7cbe78ba | |||
| 314edc5c51 | |||
| 7e7890183e | |||
| 2c5d7757e5 | |||
| 43b4d35b2e | |||
| 33ed917172 | |||
| 89d51fdc23 | |||
| 022d9848de | |||
| 8afebe560a | |||
| a63f8bc08b | |||
| 4dc1a8c75f | |||
| 121a2d45e7 | |||
| 06b3b7613c | |||
| addf5fcc64 | |||
| a877d95f74 | |||
| 76a5c19000 | |||
| 7d63797dfa | |||
| 0f3199a812 | |||
| 6d51b9dbde | |||
| d340efe58d | |||
| d9f0ef13aa | |||
| 358819bf2e | |||
| cef581d723 | |||
| dbb8e1d409 | |||
| 78008ab9d3 | |||
| cab8a7cecf | |||
| 74fa5413fc | |||
| cc4e12fa74 | |||
| 1c195fea4f | |||
| a4bf13b30d | |||
| 619dd2801c | |||
| 280b2ff856 | |||
| 527cd1fd58 | |||
| 55698b2d74 | |||
| e503a32d84 | |||
| 564e929aea | |||
| bbd5f70c3c | |||
| d35f03134f | |||
| d71841d248 | |||
| e77dedb130 | |||
| 1f37f7806e | |||
| 322466d6c1 | |||
| 1d2b71f27e | |||
| 443a3ba120 | |||
| e37a24bea8 | |||
| 05f1a034ce | |||
| feb61d4e45 | |||
| 332c65238d | |||
| dbecf229af | |||
| 2542074545 | |||
| 2576801a89 | |||
| 025059aadf | |||
| b778d2f0cd | |||
| 71c86ab62b | |||
| 4666ecef82 | |||
| b34a9b19ba | |||
| cf59430991 | |||
| 8e34995944 | |||
| fe66f39ecc | |||
| 87cff80098 | |||
| d545f746bc | |||
| 6be351b5a0 | |||
| d9447ad060 | |||
| 9215a04a7e | |||
| 7b7d22c495 | |||
| b4eada876a | |||
| 49cd6625f4 | |||
| 6f71868bad | |||
| 173eaa7463 | |||
| 2b025c4ea6 | |||
| 37110a9d05 | |||
| 796312db4c | |||
| 9e1724e892 | |||
| 0b3b98eca7 | |||
| abd62648cb | |||
| 0883a36537 | |||
| b6c61fe390 | |||
| f7284a57df | |||
| 96f34ec70f | |||
| aad2f60b87 | |||
| 5b874bc3ad | |||
| 7d515a8345 | |||
| d9f7682b2d | |||
| f6ca040d19 | |||
| 7ad22d49af | |||
| 7fa9056527 | |||
| 62b93f986f | |||
| b9617368a0 | |||
| b217cddbd8 | |||
| 3a52761d30 | |||
| 6b3659c2f5 | |||
| 734b5b410a | |||
| d5ec8f7081 | |||
| 89674f8838 | |||
| ed51705240 | |||
| c9fa5fabc5 | |||
| e3ed350b8b | |||
| d963e120db | |||
| 2ff3796289 | |||
| b48506171b | |||
| 7240ea99f1 | |||
| 1e8fefbede | |||
| 906d819454 | |||
| be88719932 | |||
| 213117ded2 | |||
| 76ff1cf0dc | |||
| 5d44633687 | |||
| 5113a5f4bb | |||
| f501a5f440 | |||
| e9649cfc58 | |||
| ee5587019b | |||
| ebdd58748a | |||
| 99c32d9331 | |||
| cccaf71c3e | |||
| 564018c762 | |||
| 0ccdc082b5 | |||
| 4b582f8074 | |||
| 1f0d2e993b | |||
| b7db68457c | |||
| 11c79a7003 | |||
| f9de29519b | |||
| d7c1aef77a | |||
| 33c2fe90a8 | |||
| e8f68a331f | |||
| 2d15bef298 | |||
| 28db4bd60a | |||
| 01a12b325f | |||
| c87b1eabf4 | |||
| 908c9d5708 | |||
| 860d12a12c | |||
| 511349a7bd | |||
| 764f6e3349 | |||
| 59d08c7612 | |||
| ebbb2fa319 | |||
| d984cda486 | |||
| 52f3134f30 | |||
| 7f332f120c | |||
| ba10b2a2d2 | |||
| 92de37e182 | |||
| 30970abd1f | |||
| 06da25fd26 | |||
| cdb025dd90 | |||
| ce72f196b7 | |||
| 4aa47cc591 | |||
| 1664fda6e4 | |||
| ccee790c0d | |||
| c6815c7ad5 | |||
| 0e51c4c94c | |||
| 221d4e7f69 | |||
| bcddbbde26 | |||
| e39a46c1a0 | |||
| 1f602fa3a0 | |||
| a9413e2d45 | |||
| 6fbc60fa9f | |||
| 43ef2f4e7f | |||
| 3f54330ac9 | |||
| 99a4fc94bf | |||
| c052903ade | |||
| 6a4ec9dbbd | |||
| 7a85430465 | |||
| e8ed16bb42 | |||
| 3176a45ebe | |||
| a601a5cbc1 | |||
| ac03f8097d | |||
| 92238612b6 | |||
| c62ab7b239 | |||
| 992be9a636 | |||
| 60ad1c5afc | |||
| 4854bda302 | |||
| 1ccd5108c3 | |||
| 1d1f9ff1ff | |||
| 8373a26f85 | |||
| 6a28a98c8b | |||
| 549399b795 | |||
| 3ee2195340 | |||
| 2fd0ff0b2a | |||
| 911fee2d85 | |||
| 68b0e225a2 | |||
| dd45d0de26 | |||
| 916ea39fad | |||
| 983d86a334 | |||
| e7fc9ed489 | |||
| e329fcc6b8 | |||
| d3e7fee828 | |||
| 96e39bea1b | |||
| a8687f3736 | |||
| 8d58bdb21b | |||
| 4616fa3486 | |||
| f11f84dfbd | |||
| f418c16b2c | |||
| a1e1f73886 | |||
| d2e99bae34 | |||
| d188415949 | |||
| 5018f6c03a | |||
| b413e3ec03 | |||
| fe7ae99a4d | |||
| 147d71b9f1 | |||
| 384da9fd0f | |||
| ae40f72a39 | |||
| d0ba3a923c | |||
| 76c19a0f2d | |||
| f79506d58b | |||
| 469ee2d92a | |||
| 57c2c64047 | |||
| 4048647ef8 | |||
| bce662d135 | |||
| d04b08133f | |||
| 897239e997 | |||
| 83f6f8c703 | |||
| 3a3dfc003e | |||
| fc355605f5 | |||
| 5d3f597137 | |||
| 3e37551923 | |||
| d09537ed5c | |||
| 86921f4862 | |||
| 7af366b7b2 | |||
| ddbb24942d | |||
| 610a4a0703 | |||
| 7cd37427b2 | |||
| 2d772dad68 | |||
| 156eb15f58 | |||
| c59c515706 | |||
| 44b8d5e4c1 | |||
| cc07ef447e | |||
| cf4afde88e | |||
| d6b1414967 | |||
| 00dd89079b | |||
| cfb635d870 | |||
| 9ae34d3b45 | |||
| 08b8b04c6c | |||
| f890acfc7c | |||
| a4fe4d8d99 | |||
| 593b495139 | |||
| 2a02dd65be | |||
| 845b760db3 | |||
| f86637cf1c | |||
| 363073658a | |||
| da228f2582 | |||
| 36476f05cd | |||
| 90602d3802 | |||
| b2e1c5bb5c | |||
| d58d55cfe7 | |||
| cd23e0f155 | |||
| 26d9ff283f | |||
| fb85019c3c | |||
| 4859d57fea | |||
| 03394fe38d | |||
| e0e12450c5 | |||
| e5ac40aac8 | |||
| 345c8be467 | |||
| def6301292 | |||
| df7cbdfcb6 | |||
| d38763d5ef | |||
| e90e540933 | |||
| 656bf3c7fa | |||
| ca35b0761b | |||
| b031a829c0 | |||
| 3c11cceb43 | |||
| 15f233e773 | |||
| 721b58f8c7 | |||
| eb0bba2c89 | |||
| 0d45c636a8 | |||
| 3e2bc9ee74 | |||
| 6a7a403117 | |||
| d1c047d060 | |||
| a35a344f42 | |||
| efc6f238e7 |
+126
@@ -0,0 +1,126 @@
|
||||
---
|
||||
Language: Cpp
|
||||
BasedOnStyle: Google
|
||||
AccessModifierOffset: -4
|
||||
AlignAfterOpenBracket: Align
|
||||
AlignConsecutiveAssignments: false
|
||||
AlignConsecutiveDeclarations: false
|
||||
AlignEscapedNewlines: Left
|
||||
AlignOperands: true
|
||||
AlignTrailingComments: true
|
||||
AllowAllParametersOfDeclarationOnNextLine: true
|
||||
AllowShortBlocksOnASingleLine: false
|
||||
AllowShortCaseLabelsOnASingleLine: false
|
||||
AllowShortFunctionsOnASingleLine: All
|
||||
AllowShortIfStatementsOnASingleLine: false
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
AlwaysBreakAfterReturnType: ExceptShortType
|
||||
AlwaysBreakBeforeMultilineStrings: true
|
||||
AlwaysBreakTemplateDeclarations: Yes
|
||||
BinPackArguments: true
|
||||
BinPackParameters: true
|
||||
BraceWrapping:
|
||||
AfterClass: false
|
||||
AfterControlStatement: false
|
||||
AfterEnum: false
|
||||
AfterFunction: false
|
||||
AfterNamespace: false
|
||||
AfterObjCDeclaration: false
|
||||
AfterStruct: false
|
||||
AfterUnion: false
|
||||
AfterExternBlock: false
|
||||
BeforeCatch: false
|
||||
BeforeElse: false
|
||||
IndentBraces: false
|
||||
SplitEmptyFunction: false
|
||||
SplitEmptyRecord: false
|
||||
SplitEmptyNamespace: false
|
||||
BreakBeforeBinaryOperators: None
|
||||
BreakBeforeBraces: Attach
|
||||
BreakBeforeInheritanceComma: false
|
||||
BreakInheritanceList: BeforeColon
|
||||
BreakBeforeTernaryOperators: true
|
||||
BreakConstructorInitializersBeforeComma: false
|
||||
BreakConstructorInitializers: BeforeColon
|
||||
BreakAfterJavaFieldAnnotations: false
|
||||
BreakStringLiterals: true
|
||||
ColumnLimit: 100
|
||||
CommentPragmas: '^ IWYU pragma:'
|
||||
CompactNamespaces: false
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: true
|
||||
ConstructorInitializerIndentWidth: 4
|
||||
ContinuationIndentWidth: 4
|
||||
Cpp11BracedListStyle: true
|
||||
DerivePointerAlignment: false
|
||||
DisableFormat: false
|
||||
ExperimentalAutoDetectBinPacking: false
|
||||
FixNamespaceComments: true
|
||||
ForEachMacros:
|
||||
- cJSON_ArrayForEach
|
||||
IncludeBlocks: Preserve
|
||||
IncludeCategories:
|
||||
- Regex: '^<esp_.*\.h>'
|
||||
Priority: 1
|
||||
- Regex: '^<driver/.*\.h>'
|
||||
Priority: 1
|
||||
- Regex: '^<.*\.h>'
|
||||
Priority: 2
|
||||
- Regex: '^<.*'
|
||||
Priority: 3
|
||||
- Regex: '.*'
|
||||
Priority: 4
|
||||
IncludeIsMainRegex: '([-_](test|unittest))?$'
|
||||
IndentCaseLabels: true
|
||||
IndentPPDirectives: None
|
||||
IndentWidth: 4
|
||||
IndentWrappedFunctionNames: false
|
||||
KeepEmptyLinesAtTheStartOfBlocks: false
|
||||
MacroBlockBegin: ''
|
||||
MacroBlockEnd: ''
|
||||
MaxEmptyLinesToKeep: 1
|
||||
NamespaceIndentation: None
|
||||
PenaltyBreakAssignment: 2
|
||||
PenaltyBreakBeforeFirstCallParameter: 1
|
||||
PenaltyBreakComment: 300
|
||||
PenaltyBreakFirstLessLess: 120
|
||||
PenaltyBreakString: 1000
|
||||
PenaltyBreakTemplateDeclaration: 10
|
||||
PenaltyExcessCharacter: 1000000
|
||||
PenaltyReturnTypeOnItsOwnLine: 200
|
||||
PointerAlignment: Left
|
||||
RawStringFormats:
|
||||
- Language: Cpp
|
||||
Delimiters:
|
||||
- cc
|
||||
- CC
|
||||
- cpp
|
||||
- Cpp
|
||||
- CPP
|
||||
- 'c++'
|
||||
- 'C++'
|
||||
CanonicalDelimiter: ''
|
||||
BasedOnStyle: google
|
||||
ReflowComments: true
|
||||
SortIncludes: true
|
||||
SortUsingDeclarations: true
|
||||
SpaceAfterCStyleCast: false
|
||||
SpaceAfterTemplateKeyword: true
|
||||
SpaceBeforeAssignmentOperators: true
|
||||
SpaceBeforeCpp11BracedList: false
|
||||
SpaceBeforeCtorInitializerColon: true
|
||||
SpaceBeforeInheritanceColon: true
|
||||
SpaceBeforeParens: ControlStatements
|
||||
SpaceBeforeRangeBasedForLoopColon: true
|
||||
SpaceInEmptyParentheses: false
|
||||
SpacesBeforeTrailingComments: 2
|
||||
SpacesInAngles: false
|
||||
SpacesInContainerLiterals: true
|
||||
SpacesInCStyleCastParentheses: false
|
||||
SpacesInParentheses: false
|
||||
SpacesInSquareBrackets: false
|
||||
Standard: Latest
|
||||
StatementMacros:
|
||||
- Q_UNUSED
|
||||
- QT_REQUIRE_VERSION
|
||||
TabWidth: 4
|
||||
UseTab: Never
|
||||
@@ -0,0 +1,77 @@
|
||||
name: Build, setup, or flashing problem
|
||||
description: Report a reproducible problem setting up, building, or flashing the firmware
|
||||
title: "[Build] "
|
||||
labels: ['bug']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
This form is for the open-source firmware. For device unbinding, email [xiaozhi.ai@tenclass.com](mailto:xiaozhi.ai@tenclass.com?subject=Device%20unbinding%20request) privately with the device ID and MAC address. Use the [XiaoZhi AI website](https://xiaozhi.me/) for other account and cloud-service requests.
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Before submitting
|
||||
options:
|
||||
- label: I have tested a current source version and searched for an existing issue reporting the same problem.
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: board
|
||||
attributes:
|
||||
label: Board or device
|
||||
description: Enter the physical product/model and the selected board target or build variant.
|
||||
placeholder: ex. Waveshare ESP32-S3-Touch-LCD-3.5B; target waveshare/esp32-s3-touch-lcd-3.5b
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: source_version
|
||||
attributes:
|
||||
label: XiaoZhi source version
|
||||
description: Paste the complete output of `git describe --tags --always --dirty`. Do not enter "latest".
|
||||
placeholder: ex. v2.2.6-15-g1a2b3c4
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: esp_idf_version
|
||||
attributes:
|
||||
label: ESP-IDF version
|
||||
description: Paste the complete output of `idf.py --version`.
|
||||
placeholder: ex. ESP-IDF v6.0.2
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem and reproduction steps
|
||||
description: State what you tried, the exact commands or steps, what you expected, what happened, and any relevant local changes.
|
||||
placeholder: |
|
||||
Operating system:
|
||||
Command or steps:
|
||||
Expected result:
|
||||
Actual result:
|
||||
Local changes, if any:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Complete error output
|
||||
description: Paste the output from the first error through the final failure. Remove credentials, tokens, usernames, and other private information.
|
||||
placeholder: Paste the complete output here, not only the final error line.
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: upload
|
||||
id: attachments
|
||||
attributes:
|
||||
label: Optional files
|
||||
description: Attach logs or a ZIP containing relevant configuration files. Review files for secrets first.
|
||||
validations:
|
||||
required: false
|
||||
accept: ".log,.txt,.json,.zip"
|
||||
@@ -1,103 +0,0 @@
|
||||
name: Installation or build bug report
|
||||
description: Report installation or build bugs
|
||||
labels: ['bug']
|
||||
body:
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Answers checklist.
|
||||
description: Before submitting a new issue, please follow the checklist and try to find the answer.
|
||||
options:
|
||||
- label: I have read the documentation [XiaoZhi AI Programming Guide](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb) and the issue is not addressed there.
|
||||
required: true
|
||||
- label: I have updated my branch (master or release) to the latest version and checked that the issue is present there.
|
||||
required: true
|
||||
- label: I have searched the issue tracker for a similar issue and not found a similar issue.
|
||||
required: true
|
||||
- type: input
|
||||
id: xiaozhi_ai_version
|
||||
attributes:
|
||||
label: XiaoZhi AI version.
|
||||
description: On which XiaoZhi AI version does this issue occur on? Run `git describe --tags` to find it.
|
||||
placeholder: ex. v1.1.0-44-g140aab8
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: operating_system
|
||||
attributes:
|
||||
label: Operating System used.
|
||||
multiple: false
|
||||
options:
|
||||
- Windows
|
||||
- Linux
|
||||
- macOS
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: build
|
||||
attributes:
|
||||
label: How did you build your project?
|
||||
multiple: false
|
||||
options:
|
||||
- Command line with CMake
|
||||
- Command line with idf.py
|
||||
- CLion IDE
|
||||
- VS Code IDE/Cursor
|
||||
- Other (please specify in More Information)
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: windows_comand_line
|
||||
attributes:
|
||||
label: If you are using Windows, please specify command line type.
|
||||
multiple: false
|
||||
options:
|
||||
- PowerShell
|
||||
- CMD
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: What is the expected behavior?
|
||||
description: Please provide a clear and concise description of the expected behavior.
|
||||
placeholder: I expected it to...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: What is the actual behavior?
|
||||
description: Please describe actual behavior.
|
||||
placeholder: Instead it...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: Steps to reproduce.
|
||||
description: 'How do you trigger this bug? Please walk us through it step by step. If this is build bug, please attach sdkconfig file (from your project folder). Please attach your code here.'
|
||||
value: |
|
||||
1. Step
|
||||
2. Step
|
||||
3. Step
|
||||
...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: debug_logs
|
||||
attributes:
|
||||
label: Build or installation Logs.
|
||||
description: Build or installation log goes here, should contain the backtrace, as well as the reset source if it is a crash.
|
||||
placeholder: Your log goes here.
|
||||
render: plain
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: more-info
|
||||
attributes:
|
||||
label: More Information.
|
||||
description: Do you have any other information from investigating this?
|
||||
placeholder: ex. Any more.
|
||||
validations:
|
||||
required: false
|
||||
@@ -1,115 +1,77 @@
|
||||
name: Runtime bug report
|
||||
description: Report runtime bugs
|
||||
name: Device runtime bug
|
||||
description: Report a reproducible firmware problem while a device is running
|
||||
title: "[Bug] "
|
||||
labels: ['bug']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
This form is for the open-source device firmware. For device unbinding, email [xiaozhi.ai@tenclass.com](mailto:xiaozhi.ai@tenclass.com?subject=Device%20unbinding%20request) privately with the device ID and MAC address. Use the [XiaoZhi AI website](https://xiaozhi.me/) for other account and cloud-service requests.
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Answers checklist.
|
||||
description: Before submitting a new issue, please follow the checklist and try to find the answer.
|
||||
label: Before submitting
|
||||
options:
|
||||
- label: I have read the documentation [XiaoZhi AI Programming Guide](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb) and the issue is not addressed there.
|
||||
required: true
|
||||
- label: I have updated my firmware to the latest version and checked that the issue is present there.
|
||||
required: true
|
||||
- label: I have searched the issue tracker for a similar issue and not found a similar issue.
|
||||
- label: I have tested the latest applicable firmware and searched for an existing issue reporting the same problem.
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: xiaozhi_ai_firmware_version
|
||||
id: device
|
||||
attributes:
|
||||
label: XiaoZhi AI firmware version.
|
||||
description: On which firmware version does this issue occur on?
|
||||
placeholder: ex. v1.2.1_bread-compact-wifi
|
||||
label: Device or development board
|
||||
description: Enter the manufacturer, exact product/model, and hardware revision or SKU when known.
|
||||
placeholder: ex. LilyGO T-Circle-S3 V1.1 or EchoEar N16R8
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: operating_system
|
||||
|
||||
- type: input
|
||||
id: firmware_version
|
||||
attributes:
|
||||
label: Operating System used.
|
||||
multiple: false
|
||||
options:
|
||||
- Windows
|
||||
- Linux
|
||||
- macOS
|
||||
label: Firmware version
|
||||
description: Paste the `App version:` value from the startup log. Do not enter a board name or "latest".
|
||||
placeholder: ex. 2.2.6
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: build
|
||||
|
||||
- type: input
|
||||
id: firmware_source
|
||||
attributes:
|
||||
label: How did you build your project?
|
||||
multiple: false
|
||||
options:
|
||||
- Command line with CMake
|
||||
- Command line with idf.py
|
||||
- CLion IDE
|
||||
- VS Code IDE/Cursor
|
||||
- Other (please specify in More Information)
|
||||
label: Firmware source or filename
|
||||
description: State where the firmware came from and include its filename or board target when available.
|
||||
placeholder: ex. xiaozhi.me, GitHub release echoear.bin, self-built target df-k10, or vendor firmware
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: windows_comand_line
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: If you are using Windows, please specify command line type.
|
||||
multiple: false
|
||||
options:
|
||||
- PowerShell
|
||||
- CMD
|
||||
label: Problem and reproduction steps
|
||||
description: Describe the starting state, exact actions, expected result, actual result, frequency, and any relevant modifications.
|
||||
placeholder: |
|
||||
Steps:
|
||||
Expected result:
|
||||
Actual result:
|
||||
Frequency:
|
||||
Modifications, if any:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: evidence
|
||||
attributes:
|
||||
label: Logs or other evidence
|
||||
description: Paste relevant startup and failure logs, including a backtrace or reset reason when applicable. If logs are unavailable, briefly say why. Remove credentials, device IDs, MAC addresses, UUIDs, and other private information.
|
||||
placeholder: Paste relevant logs here, or explain why logs cannot be collected.
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: upload
|
||||
id: attachments
|
||||
attributes:
|
||||
label: Optional photos, videos, or log files
|
||||
description: Attach files that help identify the hardware or demonstrate the failure. Review files for private information first.
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: power_supply
|
||||
attributes:
|
||||
label: Power Supply used.
|
||||
multiple: false
|
||||
options:
|
||||
- USB
|
||||
- External 5V
|
||||
- External 3.3V
|
||||
- Battery
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: What is the expected behavior?
|
||||
description: Please provide a clear and concise description of the expected behavior.
|
||||
placeholder: I expected it to...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: What is the actual behavior?
|
||||
description: Please describe actual behavior.
|
||||
placeholder: Instead it...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: Steps to reproduce.
|
||||
description: 'How do you trigger this bug? Please walk us through it step by step. Please attach your code here.'
|
||||
value: |
|
||||
1. Step
|
||||
2. Step
|
||||
3. Step
|
||||
...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: debug_logs
|
||||
attributes:
|
||||
label: Debug Logs.
|
||||
description: Debug log goes here, should contain the backtrace, as well as the reset source if it is a crash.
|
||||
placeholder: Your log goes here.
|
||||
render: plain
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: more-info
|
||||
attributes:
|
||||
label: More Information.
|
||||
description: Do you have any other information from investigating this?
|
||||
placeholder: ex. Any more.
|
||||
validations:
|
||||
required: false
|
||||
accept: ".png,.jpg,.jpeg,.gif,.mp4,.mov,.webm,.log,.txt,.zip"
|
||||
|
||||
@@ -1,34 +1,50 @@
|
||||
name: Feature request
|
||||
description: Suggest an idea for this project.
|
||||
description: Propose an improvement to a supported device or the common firmware
|
||||
title: "[Feature] "
|
||||
labels: ['enhancement']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
* We welcome any ideas or feature requests! It’s helpful if you can explain exactly why the feature would be useful.
|
||||
* There are usually some outstanding feature requests in the [existing issues list](https://github.com/78/xiaozhi-esp32/labels/enhancement), feel free to add comments to them.
|
||||
* If you would like to contribute, please read the [contributions guide](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb).
|
||||
- type: textarea
|
||||
id: problem-related
|
||||
Use the **Board support request** form for a new board, hardware revision, or product variant. Account and cloud-product requests belong on the [XiaoZhi AI website](https://xiaozhi.me/).
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Is your feature request related to a problem?
|
||||
description: Please provide a clear and concise description of what the problem is.
|
||||
placeholder: ex. I'm always frustrated when ...
|
||||
label: Before submitting
|
||||
options:
|
||||
- label: I have searched the existing feature requests and did not find the same proposal.
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: scope
|
||||
attributes:
|
||||
label: Affected device or area
|
||||
description: Enter the device, board target, or firmware area. Enter "All devices" for a general request.
|
||||
placeholder: ex. df-k10, audio, display, protocol, or All devices
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem or use case
|
||||
description: Explain the current limitation and why the requested change would be useful.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Describe the solution you'd like.
|
||||
description: Please provide a clear and concise description of what you want to happen.
|
||||
placeholder: ex. When using XiaoZhi ...
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Describe alternatives you've considered.
|
||||
description: Please provide a clear and concise description of any alternative solutions or features you've considered.
|
||||
placeholder: ex. Choosing other approach wouldn't work, because ...
|
||||
label: Requested behavior
|
||||
description: Describe the result or behavior you would like to see.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Additional context.
|
||||
description: Please add any other context or screenshots about the feature request here.
|
||||
placeholder: ex. This would work only when ...
|
||||
label: Additional context
|
||||
description: Optionally add alternatives, screenshots, diagrams, links, or testing information.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
name: Board support request
|
||||
description: Request support for a new board, product, hardware revision, or SKU
|
||||
title: "[Board Support] "
|
||||
labels: ['enhancement']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Similar-looking products may use different pins and components. Please identify the exact hardware rather than only naming the closest supported board.
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Before submitting
|
||||
options:
|
||||
- label: I have searched the repository and existing issues for this exact model and hardware revision.
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: hardware
|
||||
attributes:
|
||||
label: Exact hardware
|
||||
description: Provide the manufacturer, model, revision/SKU, main chip and memory, and an official product or hardware documentation link.
|
||||
placeholder: |
|
||||
Manufacturer and model:
|
||||
Revision or SKU:
|
||||
Chip, flash, and PSRAM:
|
||||
Official link:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: request
|
||||
attributes:
|
||||
label: Requested support
|
||||
description: Describe what should work and any known differences from the closest supported board.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: attempted
|
||||
attributes:
|
||||
label: What have you tried?
|
||||
description: Include the closest board target or firmware, its version, and the result. Enter "None" if no existing target is appropriate.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: testing
|
||||
attributes:
|
||||
label: Testing available
|
||||
options:
|
||||
- I can build, flash, collect logs, and test changes
|
||||
- I can flash binaries and test changes
|
||||
- I can provide hardware information only
|
||||
- I cannot test changes
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: upload
|
||||
id: attachments
|
||||
attributes:
|
||||
label: Optional hardware files, photos, or logs
|
||||
description: Attach public schematics, board photos, pinouts, or serial logs. Do not upload confidential material or secrets.
|
||||
validations:
|
||||
required: false
|
||||
accept: ".png,.jpg,.jpeg,.pdf,.log,.txt,.json,.zip"
|
||||
@@ -0,0 +1,42 @@
|
||||
name: Firmware development question
|
||||
description: Ask a focused technical question about this repository or supported hardware
|
||||
title: "[Question] "
|
||||
labels: ['question']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
This form is for technical questions about the open-source firmware. For device unbinding, email [xiaozhi.ai@tenclass.com](mailto:xiaozhi.ai@tenclass.com?subject=Device%20unbinding%20request) privately with the device ID and MAC address. Other account and cloud-service requests belong on the [XiaoZhi AI website](https://xiaozhi.me/).
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Before submitting
|
||||
options:
|
||||
- label: I have searched the repository and existing issues and did not find an answer.
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: context
|
||||
attributes:
|
||||
label: Relevant device, version, or code area
|
||||
description: Enter the device/model, board target, source or firmware version, file, or subsystem that matters to the question.
|
||||
placeholder: ex. zhengchen-cam on v2.2.6, or main/audio on current main
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: question
|
||||
attributes:
|
||||
label: Goal and question
|
||||
description: Explain what you are trying to accomplish and state the specific technical question.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: attempted
|
||||
attributes:
|
||||
label: What have you tried?
|
||||
description: Optionally include relevant files, commands, experiments, logs, screenshots, or a minimal example.
|
||||
validations:
|
||||
required: false
|
||||
@@ -1,8 +1,8 @@
|
||||
blank_issues_enabled: true
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 小智 AI 官方网站
|
||||
- name: Unbind a previously owned device
|
||||
url: https://github.com/78/xiaozhi-esp32/blob/main/.github/SUPPORT.md#unbind-a-previously-owned-device
|
||||
about: If the device is bound to another account, email its device ID and MAC address privately. Do not post these identifiers in a public issue.
|
||||
- name: Other cloud services and account requests
|
||||
url: https://xiaozhi.me/
|
||||
about: 激活设备、配置 AI、声纹识别、声音克隆等应有尽有,DIY 属于你自己的小智
|
||||
- name: 小智 AI 聊天机器人百科全书
|
||||
url: https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb
|
||||
about: 开发文档、硬件制作、烧录教程、FAQ尽在小智百科
|
||||
about: Use the XiaoZhi AI website for activation, agent configuration, voiceprint, voice cloning, and other cloud services
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# Account and Device Support
|
||||
|
||||
This GitHub repository tracks the open-source XiaoZhi device firmware. Account recovery, device ownership, and cloud-service requests are handled outside the public issue tracker.
|
||||
|
||||
## Unbind a Previously Owned Device
|
||||
|
||||
If you purchased or received a used device that is still bound to another person's account:
|
||||
|
||||
1. Collect the device ID and MAC address using the instructions below.
|
||||
2. Email [xiaozhi.ai@tenclass.com](mailto:xiaozhi.ai@tenclass.com?subject=Device%20unbinding%20request) with both identifiers.
|
||||
3. Use an email subject such as `Device unbinding / 解绑设备 - Device ID XXX - MAC address XXX`.
|
||||
|
||||
For multiple devices, attach a list containing the device ID and MAC address for each device.
|
||||
|
||||
> **Privacy:** Send device IDs and MAC addresses only by email. Do not post them in a public GitHub issue, discussion, screenshot, or log.
|
||||
|
||||
### Why Both Identifiers Are Required
|
||||
|
||||
- The **MAC address** uniquely identifies the physical device hardware.
|
||||
- The **device ID** is obtained by asking the AI running on the device. Providing the value reported by the device helps demonstrate that the requester has the device in hand and can operate it.
|
||||
|
||||
Together, these identifiers help the support team locate the correct binding record and avoid unbinding the wrong device.
|
||||
|
||||
### Find the Device ID
|
||||
|
||||
If the device can connect and have a conversation, ask it:
|
||||
|
||||
> What is my device ID?
|
||||
|
||||
Record the complete value reported by the device. Both the device ID and MAC address are required for the standard unbinding process. If the device cannot have a conversation or does not return a device ID, explain this in the email; the support team may require other proof of possession. Do not guess or substitute a temporary activation code.
|
||||
|
||||
### Find the MAC Address
|
||||
|
||||
1. Connect the powered-on device to a computer with a data-capable USB cable.
|
||||
2. Open the device's serial port with a serial terminal. Developers with an ESP-IDF environment can use `idf.py monitor`.
|
||||
3. Keep the serial terminal open and restart the device so that the complete startup log is captured.
|
||||
4. Search the log for a line similar to:
|
||||
|
||||
```text
|
||||
wifi:mode : sta (aa:bb:cc:dd:ee:ff)
|
||||
```
|
||||
|
||||
5. The value inside the parentheses is the Wi-Fi MAC address to include in the email.
|
||||
|
||||
If that line does not appear, search the startup log for `MAC` or `mac_address`. If you still cannot identify it, attach the exact device model and explain the situation in the private email. Do not upload the unredacted startup log to a public GitHub issue because it may contain the MAC address, UUID, Wi-Fi name, and other device information.
|
||||
|
||||
## Other Account and Cloud-Service Requests
|
||||
|
||||
For verification codes, password recovery, activation, agent configuration, voiceprint, voice cloning, and other cloud services, use the [XiaoZhi AI website](https://xiaozhi.me/).
|
||||
+124
-19
@@ -1,32 +1,137 @@
|
||||
name: Build and Test
|
||||
name: Build Boards
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- ci/* # for ci test
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
prepare:
|
||||
name: Determine variants to build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
container:
|
||||
image: espressif/idf:v6.0.1
|
||||
outputs:
|
||||
variants: ${{ steps.select.outputs.variants }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Espressif IoT Development Framework (ESP-IDF)
|
||||
# You may pin to the exact commit or the version.
|
||||
# uses: espressif/esp-idf-ci-action@8cd22ae10042fadc37890e81e9988a9113e7b506
|
||||
uses: espressif/esp-idf-ci-action@v1.1.0
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
# Relative path under $GITHUB_WORKSPACE to place the repository
|
||||
#path: # optional, default is
|
||||
# Version of ESP-IDF docker image to use
|
||||
esp_idf_version: release-v5.4
|
||||
# ESP32 variant to build for
|
||||
target: esp32s3
|
||||
# Command to run inside the docker container (default: builds the project)
|
||||
# command: # optional, default is idf.py build
|
||||
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Test build tooling
|
||||
run: python -m unittest discover -s scripts/tests -v
|
||||
|
||||
- id: list
|
||||
name: Get all variant list
|
||||
run: |
|
||||
echo "all_variants=$(python scripts/build.py --list-boards --json)" >> $GITHUB_OUTPUT
|
||||
|
||||
- id: select
|
||||
name: Select variants based on changes
|
||||
shell: bash
|
||||
env:
|
||||
ALL_VARIANTS: ${{ steps.list.outputs.all_variants }}
|
||||
run: |
|
||||
EVENT_NAME="${{ github.event_name }}"
|
||||
|
||||
# push 到 main 分支,编译全部变体
|
||||
if [[ "$EVENT_NAME" == "push" ]]; then
|
||||
echo "variants=$ALL_VARIANTS" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# pull_request 场景
|
||||
# actions/checkout checks out GitHub's synthetic PR merge commit. Its
|
||||
# first and second parents are the exact base and head commits used
|
||||
# for this run, even when the event SHAs are not exposed as refs in
|
||||
# the job container.
|
||||
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
BASE_SHA="HEAD^1"
|
||||
HEAD_SHA="HEAD^2"
|
||||
echo "Base: $(git rev-parse "$BASE_SHA"), Head: $(git rev-parse "$HEAD_SHA")"
|
||||
|
||||
CHANGED=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA")
|
||||
echo -e "Changed files:\n$CHANGED"
|
||||
|
||||
SELECTED=$(printf '%s\n' "$CHANGED" | python scripts/build.py --select-changed)
|
||||
echo "variants=$SELECTED" >> $GITHUB_OUTPUT
|
||||
|
||||
build:
|
||||
name: Build ${{ matrix.full_name }}
|
||||
needs: prepare
|
||||
if: ${{ needs.prepare.outputs.variants != '[]' }}
|
||||
strategy:
|
||||
fail-fast: false # 单个变体失败不影响其它变体
|
||||
matrix:
|
||||
include: ${{ fromJson(needs.prepare.outputs.variants) }}
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: espressif/idf:v6.0.1
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build current variant
|
||||
shell: bash
|
||||
run: |
|
||||
source $IDF_PATH/export.sh
|
||||
python scripts/build.py ${{ matrix.board }} --name ${{ matrix.name }}
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: xiaozhi_${{ matrix.full_name }}_${{ github.sha }}
|
||||
path: build/merged-binary.bin
|
||||
if-no-files-found: error
|
||||
|
||||
esp32s31-idf61:
|
||||
name: Build ESP32-S31 with IDF 6.1
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: espressif/idf:release-v6.1
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build ESP32-S31 preview variant
|
||||
shell: bash
|
||||
run: |
|
||||
source $IDF_PATH/export.sh
|
||||
python scripts/build.py espressif/esp32-s31-function-coreboard-1 \
|
||||
--name esp32-s31-function-coreboard-1
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: xiaozhi_espressif-esp32-s31-function-coreboard-1_${{ github.sha }}
|
||||
path: build/merged-binary.bin
|
||||
if-no-files-found: error
|
||||
|
||||
blufi-idf6:
|
||||
name: IDF 6 BluFi option
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: espressif/idf:v6.0.1
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build BluFi-enabled firmware
|
||||
shell: bash
|
||||
run: |
|
||||
source $IDF_PATH/export.sh
|
||||
idf.py -B build-blufi \
|
||||
-DSDKCONFIG=build-blufi/sdkconfig \
|
||||
-DSDKCONFIG_DEFAULTS="sdkconfig.defaults;scripts/ci/blufi.sdkconfig.defaults" \
|
||||
-DIDF_TARGET=esp32s3 \
|
||||
-DBOARD_NAME=bread-compact-wifi \
|
||||
-DBOARD_TYPE=bread-compact-wifi \
|
||||
build
|
||||
|
||||
+4
-2
@@ -2,6 +2,7 @@ tmp/
|
||||
components/
|
||||
managed_components/
|
||||
build/
|
||||
dist/
|
||||
.vscode/
|
||||
.devcontainer/
|
||||
sdkconfig.old
|
||||
@@ -13,6 +14,7 @@ main/assets/lang_config.h
|
||||
main/mmap_generate_emoji.h
|
||||
.DS_Store
|
||||
.cache
|
||||
main/mmap_generate_emoji.h
|
||||
*.pyc
|
||||
*.bin
|
||||
*.bin
|
||||
mmap_generate_*.h
|
||||
.clangd
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# AGENTS.md
|
||||
|
||||
## Project
|
||||
|
||||
XiaoZhi is an ESP-IDF C/C++ voice-assistant firmware supporting many chips, boards, displays, audio devices, and network transports. A build selects exactly one board implementation.
|
||||
|
||||
Use ESP-IDF v6.0.2 when possible. IDF 5.5.x is retained only for documented legacy boards.
|
||||
|
||||
## Architecture
|
||||
|
||||
- `main/application.*`: main event loop, protocol lifecycle, and high-level behavior.
|
||||
- `main/device_state_machine.*`: legal runtime state transitions.
|
||||
- `main/boards/common/`: board interfaces and reusable hardware/network helpers.
|
||||
- `main/boards/**/`: board-specific pins, initialization, and build variants.
|
||||
- `main/audio/`: codecs, audio tasks, engines, wake words, and queues.
|
||||
- `main/protocols/`: transport-neutral API plus WebSocket and MQTT/UDP.
|
||||
- `main/display/` and `main/led/`: reusable UI implementations.
|
||||
- `main/mcp_server.*`: common device-side MCP tools and dispatch.
|
||||
- `main/Kconfig.projbuild`: board and feature configuration.
|
||||
- `main/CMakeLists.txt`: source, board, locale, font, and asset selection.
|
||||
- `scripts/build.py`: canonical board/variant build entry point.
|
||||
|
||||
Read the closest existing implementation before adding a new one. Prefer the narrowest owning layer; do not put board-specific behavior into core modules.
|
||||
|
||||
## Required Rules
|
||||
|
||||
- Preserve unrelated worktree changes and keep patches focused.
|
||||
- A build must export exactly one board factory through `DECLARE_BOARD(...)`.
|
||||
- Never alter an existing board's pins to support different hardware. Add a uniquely named board or release variant; board identity affects OTA compatibility.
|
||||
- Core code depends on `Board` interfaces, never a concrete board class or board `config.h`.
|
||||
- Treat camera, backlight, display, LED, battery, and similar capabilities as optional.
|
||||
- Change runtime state through `Application::SetDeviceState()` and the state machine.
|
||||
- Callbacks may run outside the main task. Schedule application mutations with `Application::Schedule()` or event bits.
|
||||
- Do not block the main event loop or audio tasks. Avoid unbounded queues and repeated large allocations in audio paths.
|
||||
- Keep shared message semantics in `Protocol`; verify both transports when changing its contract.
|
||||
- Validate network input and preserve `cJSON` ownership. NVS keys are persistent API and require migration when changed.
|
||||
- Guard target-specific features with Kconfig/component rules. Do not assume every target has PSRAM or S3/P4 resources.
|
||||
- Do not manually edit generated/vendor output: `build/`, `releases/`, `managed_components/`, `components/`, `sdkconfig*`, `main/assets/lang_config.h`, or generated mmap headers.
|
||||
- Format only touched C/C++ files with the repository `.clang-format`; avoid unrelated mass formatting.
|
||||
|
||||
## Boards and Configuration
|
||||
|
||||
Board selection is a coupled chain:
|
||||
|
||||
`config.json` -> `scripts/build.py` -> `main/Kconfig.projbuild` -> `main/CMakeLists.txt` -> board source and `config.h`.
|
||||
|
||||
When adding a board or variant, update every relevant link in that chain. Include a unique board identity, correct chip target, flash/partition settings, exactly one `DECLARE_BOARD`, and board documentation. Follow `docs/custom-board.md`.
|
||||
|
||||
## Commands
|
||||
|
||||
Source the intended ESP-IDF environment first:
|
||||
|
||||
```sh
|
||||
source /path/to/esp-idf/export.sh
|
||||
idf.py --version
|
||||
```
|
||||
|
||||
```sh
|
||||
# Discover exact board and variant names
|
||||
python3 scripts/build.py --list-boards
|
||||
|
||||
# Canonical variant build
|
||||
python3 scripts/build.py <board-directory> --name <variant-name>
|
||||
|
||||
# Host-side build tests
|
||||
python3 -m unittest discover -s scripts/tests -v
|
||||
|
||||
# Format/check touched files
|
||||
clang-format -i <files>
|
||||
clang-format --dry-run -Werror <files>
|
||||
```
|
||||
|
||||
The build script changes local `sdkconfig` and build state. Do not assume the build directory still represents a previous target.
|
||||
|
||||
## Validation
|
||||
|
||||
- Board-only change: build affected variants and smoke-test changed hardware.
|
||||
- Core, common-board, audio, protocol, display, dependency, Kconfig, or CMake change: run host tests and build representative affected chip/network paths.
|
||||
- Protocol changes: verify WebSocket and MQTT/UDP when shared behavior changes.
|
||||
- Audio changes: verify capture, playback, wake/VAD, interruption, reconnect, and applicable AEC modes.
|
||||
- UI/assets changes: verify applicable no-display/OLED/LVGL paths and partition size.
|
||||
- Always report what was tested and what still needs physical hardware. A successful build is not hardware validation.
|
||||
|
||||
## Authoritative Documentation
|
||||
|
||||
- Overview and SDK policy: `README.md`
|
||||
- SDK compatibility: `docs/esp-idf-6-migration.md`
|
||||
- Board guide: `docs/custom-board.md`
|
||||
- Audio design: `main/audio/README.md`
|
||||
- Code style: `docs/code_style.md`
|
||||
- Protocols: `docs/websocket.md`, `docs/mqtt-udp.md`, `docs/mcp-protocol.md`
|
||||
- CI matrix: `.github/workflows/build.yml`
|
||||
|
||||
Keep detailed or fast-changing information in those files, not here. Add a nested `AGENTS.md` only when a subsystem needs specialized instructions.
|
||||
+5
-6
@@ -1,14 +1,13 @@
|
||||
# For more information about build system see
|
||||
# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html
|
||||
# The following five lines of boilerplate have to be in your project's
|
||||
# The following lines of boilerplate have to be in your project's
|
||||
# CMakeLists in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
set(PROJECT_VER "1.8.0")
|
||||
|
||||
# Add this line to disable the specific warning
|
||||
add_compile_options(-Wno-missing-field-initializers)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(xiaozhi)
|
||||
# "Trim" the build. Include the minimal set of components, main, and anything it depends on.
|
||||
idf_build_set_property(MINIMAL_BUILD ON)
|
||||
|
||||
set(PROJECT_VER "2.4.0")
|
||||
project(xiaozhi)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Xiaoxia
|
||||
Copyright (c) 2025 Shenzhen Xinzhi Future Technology Co., Ltd.
|
||||
Copyright (c) 2025 Project Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,74 +1,72 @@
|
||||
# An MCP-based Chatbot | 一个基于 MCP 的聊天机器人
|
||||
# An MCP-based Chatbot
|
||||
|
||||
(中文 | [English](README_en.md) | [日本語](README_ja.md))
|
||||
(English | [中文](README_zh.md) | [日本語](README_ja.md))
|
||||
|
||||
## 视频
|
||||
## Introduction
|
||||
|
||||
👉 [人类:给 AI 装摄像头 vs AI:当场发现主人三天没洗头【bilibili】](https://www.bilibili.com/video/BV1bpjgzKEhd/)
|
||||
👉 [Human: Give AI a camera vs AI: Instantly finds out the owner hasn't washed hair for three days【bilibili】](https://www.bilibili.com/video/BV1bpjgzKEhd/)
|
||||
|
||||
👉 [手工打造你的 AI 女友,新手入门教程【bilibili】](https://www.bilibili.com/video/BV1XnmFYLEJN/)
|
||||
👉 [Handcraft your AI girlfriend, beginner's guide【bilibili】](https://www.bilibili.com/video/BV1XnmFYLEJN/)
|
||||
|
||||
## 介绍
|
||||
As a voice interaction entry, the XiaoZhi AI chatbot leverages the AI capabilities of large models like Qwen / DeepSeek, and achieves multi-terminal control via the MCP protocol.
|
||||
|
||||
这是一个由虾哥开源的 ESP32 项目,以 MIT 许可证发布,允许任何人免费使用,或用于商业用途。
|
||||
<img src="docs/mcp-based-graph.jpg" alt="Control everything via MCP" width="320">
|
||||
|
||||
我们希望通过这个项目,能够帮助大家了解 AI 硬件开发,将当下飞速发展的大语言模型应用到实际的硬件设备中。
|
||||
## Recent Updates
|
||||
|
||||
如果你有任何想法或建议,请随时提出 Issues 或加入 QQ 群:1011329060
|
||||
- The mainline now targets ESP-IDF v6.0 or later, with v6.0.2 as the preferred stable SDK. The previous 157-variant baseline was validated on ESP-IDF v6.0.1; the current matrix contains 171 variants, of which 170 support IDF 6.0.x and the ESP32-S31 variant requires IDF 6.1 or later.
|
||||
- MQTT and BluFi cryptographic code has migrated to PSA Crypto. IDF 6 component splits and third-party dependency compatibility have also been addressed.
|
||||
- Audio pipeline concurrency, MQTT/UDP packet validation, and release-matrix selection have been hardened.
|
||||
- ESP-IDF v5.5 is retained only for documented legacy boards. ESP32-P4 Rev1 and Rev3 are both supported on IDF 6 with ESP-SR 2.4.7; see the [ESP-IDF 6.0 Migration Guide](docs/esp-idf-6-migration.md) for full compatibility and board-validation details.
|
||||
|
||||
### 基于 MCP 控制万物
|
||||
### Features Implemented
|
||||
|
||||
小智 AI 聊天机器人作为一个语音交互入口,利用 Qwen / DeepSeek 等大模型的 AI 能力,通过 MCP 协议实现多端控制。
|
||||
- Wi-Fi, wired Ethernet, USB RNDIS, and ML307/EC801E or NT26 Cat.1 4G networking; supported boards can switch between Wi-Fi and 4G
|
||||
- Offline voice wake-up with [ESP-SR](https://github.com/espressif/esp-sr), including customizable wake words
|
||||
- Two communication transports: [WebSocket](docs/websocket.md) and [MQTT + UDP](docs/mqtt-udp.md)
|
||||
- Opus audio streaming with conventional streaming ASR + LLM + TTS pipelines and Realtime end-to-end voice models; AEC-capable hardware supports realtime full-duplex interaction
|
||||
- Speaker recognition, identifies the current speaker [3D Speaker](https://github.com/modelscope/3D-Speaker)
|
||||
- OLED / LCD displays with emoji and rich expression support, plus camera vision input on supported boards
|
||||
- Battery display and power management
|
||||
- 38 interface languages, with localized voice prompts where available and English fallback
|
||||
- ESP32, ESP32-C3, ESP32-C5, ESP32-C6, ESP32-S3, and ESP32-P4 chip platforms
|
||||
- Wi-Fi provisioning through hotspot or BluFi
|
||||
- Device-side MCP for device control (Speaker, LED, Servo, GPIO, etc.)
|
||||
- Cloud-side MCP to extend large model capabilities (smart home control, PC desktop operation, knowledge search, email, etc.)
|
||||
- Customizable wake words, fonts, emojis, and chat backgrounds with online web-based editing ([Custom Assets Generator](https://github.com/78/xiaozhi-assets-generator))
|
||||
|
||||

|
||||
## Hardware
|
||||
|
||||
### 已实现功能
|
||||
### Breadboard DIY Practice
|
||||
|
||||
- Wi-Fi / ML307 Cat.1 4G
|
||||
- 离线语音唤醒 [ESP-SR](https://github.com/espressif/esp-sr)
|
||||
- 支持两种通信协议([Websocket](docs/websocket.md) 或 MQTT+UDP)
|
||||
- 采用 OPUS 音频编解码
|
||||
- 基于流式 ASR + LLM + TTS 架构的语音交互
|
||||
- 声纹识别,识别当前说话人的身份 [3D Speaker](https://github.com/modelscope/3D-Speaker)
|
||||
- OLED / LCD 显示屏,支持表情显示
|
||||
- 电量显示与电源管理
|
||||
- 支持多语言(中文、英文、日文)
|
||||
- 支持 ESP32-C3、ESP32-S3、ESP32-P4 芯片平台
|
||||
- 通过设备端 MCP 实现设备控制(音量、灯光、电机、GPIO 等)
|
||||
- 通过云端 MCP 扩展大模型能力(智能家居控制、PC桌面操作、知识搜索、邮件收发等)
|
||||
See the Feishu document tutorial:
|
||||
|
||||
## 硬件
|
||||
👉 ["XiaoZhi AI Chatbot Encyclopedia"](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb?from=from_copylink)
|
||||
|
||||
### 面包板手工制作实践
|
||||
Breadboard demo:
|
||||
|
||||
详见飞书文档教程:
|
||||

|
||||
|
||||
👉 [《小智 AI 聊天机器人百科全书》](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb?from=from_copylink)
|
||||
### Supports 138 Board Directories and 171 Release Variants (Partial List)
|
||||
|
||||
面包板效果图如下:
|
||||
|
||||

|
||||
|
||||
### 支持 70 多个开源硬件(仅展示部分)
|
||||
|
||||
- <a href="https://oshwhub.com/li-chuang-kai-fa-ban/li-chuang-shi-zhan-pai-esp32-s3-kai-fa-ban" target="_blank" title="立创·实战派 ESP32-S3 开发板">立创·实战派 ESP32-S3 开发板</a>
|
||||
- <a href="https://github.com/espressif/esp-box" target="_blank" title="乐鑫 ESP32-S3-BOX3">乐鑫 ESP32-S3-BOX3</a>
|
||||
- <a href="https://oshwhub.com/li-chuang-kai-fa-ban/li-chuang-shi-zhan-pai-esp32-s3-kai-fa-ban" target="_blank" title="LiChuang ESP32-S3 Development Board">LiChuang ESP32-S3 Development Board</a>
|
||||
- <a href="https://github.com/espressif/esp-box" target="_blank" title="Espressif ESP32-S3-BOX-3">Espressif ESP32-S3-BOX-3</a>
|
||||
- <a href="https://docs.m5stack.com/zh_CN/core/CoreS3" target="_blank" title="M5Stack CoreS3">M5Stack CoreS3</a>
|
||||
- <a href="https://docs.m5stack.com/en/atom/Atomic%20Echo%20Base" target="_blank" title="AtomS3R + Echo Base">M5Stack AtomS3R + Echo Base</a>
|
||||
- <a href="https://gf.bilibili.com/item/detail/1108782064" target="_blank" title="神奇按钮 2.4">神奇按钮 2.4</a>
|
||||
- <a href="https://www.waveshare.net/shop/ESP32-S3-Touch-AMOLED-1.8.htm" target="_blank" title="微雪电子 ESP32-S3-Touch-AMOLED-1.8">微雪电子 ESP32-S3-Touch-AMOLED-1.8</a>
|
||||
- <a href="https://gf.bilibili.com/item/detail/1108782064" target="_blank" title="Magic Button 2.4">Magic Button 2.4</a>
|
||||
- <a href="https://www.waveshare.net/shop/ESP32-S3-Touch-AMOLED-1.8.htm" target="_blank" title="Waveshare ESP32-S3-Touch-AMOLED-1.8">Waveshare ESP32-S3-Touch-AMOLED-1.8</a>
|
||||
- <a href="https://github.com/Xinyuan-LilyGO/T-Circle-S3" target="_blank" title="LILYGO T-Circle-S3">LILYGO T-Circle-S3</a>
|
||||
- <a href="https://oshwhub.com/tenclass01/xmini_c3" target="_blank" title="虾哥 Mini C3">虾哥 Mini C3</a>
|
||||
- <a href="https://oshwhub.com/movecall/cuican-ai-pendant-lights-up-y" target="_blank" title="Movecall CuiCan ESP32S3">璀璨·AI 吊坠</a>
|
||||
- <a href="https://github.com/WMnologo/xingzhi-ai" target="_blank" title="无名科技Nologo-星智-1.54">无名科技 Nologo-星智-1.54TFT</a>
|
||||
- <a href="https://oshwhub.com/tenclass01/xmini_c3" target="_blank" title="XiaGe Mini C3">XiaGe Mini C3</a>
|
||||
- <a href="https://oshwhub.com/movecall/cuican-ai-pendant-lights-up-y" target="_blank" title="Movecall CuiCan ESP32S3">CuiCan AI Pendant</a>
|
||||
- <a href="https://github.com/WMnologo/xingzhi-ai" target="_blank" title="WMnologo-Xingzhi-1.54">WMnologo-Xingzhi-1.54TFT</a>
|
||||
- <a href="https://www.seeedstudio.com/SenseCAP-Watcher-W1-A-p-5979.html" target="_blank" title="SenseCAP Watcher">SenseCAP Watcher</a>
|
||||
- <a href="https://www.bilibili.com/video/BV1BHJtz6E2S/" target="_blank" title="ESP-HI 超低成本机器狗">ESP-HI 超低成本机器狗</a>
|
||||
- <a href="https://www.bilibili.com/video/BV1BHJtz6E2S/" target="_blank" title="ESP-HI Low Cost Robot Dog">ESP-HI Low Cost Robot Dog</a>
|
||||
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<a href="docs/v1/lichuang-s3.jpg" target="_blank" title="立创·实战派 ESP32-S3 开发板">
|
||||
<a href="docs/v1/lichuang-s3.jpg" target="_blank" title="LiChuang ESP32-S3 Development Board">
|
||||
<img src="docs/v1/lichuang-s3.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/espbox3.jpg" target="_blank" title="乐鑫 ESP32-S3-BOX3">
|
||||
<a href="docs/v1/espbox3.jpg" target="_blank" title="Espressif ESP32-S3-BOX3">
|
||||
<img src="docs/v1/espbox3.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/m5cores3.jpg" target="_blank" title="M5Stack CoreS3">
|
||||
@@ -77,78 +75,92 @@
|
||||
<a href="docs/v1/atoms3r.jpg" target="_blank" title="AtomS3R + Echo Base">
|
||||
<img src="docs/v1/atoms3r.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/magiclick.jpg" target="_blank" title="神奇按钮 2.4">
|
||||
<a href="docs/v1/magiclick.jpg" target="_blank" title="Magic Button 2.4">
|
||||
<img src="docs/v1/magiclick.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/waveshare.jpg" target="_blank" title="微雪电子 ESP32-S3-Touch-AMOLED-1.8">
|
||||
<a href="docs/v1/waveshare.jpg" target="_blank" title="Waveshare ESP32-S3-Touch-AMOLED-1.8">
|
||||
<img src="docs/v1/waveshare.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/lilygo-t-circle-s3.jpg" target="_blank" title="LILYGO T-Circle-S3">
|
||||
<img src="docs/v1/lilygo-t-circle-s3.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/xmini-c3.jpg" target="_blank" title="虾哥 Mini C3">
|
||||
<a href="docs/v1/xmini-c3.jpg" target="_blank" title="XiaGe Mini C3">
|
||||
<img src="docs/v1/xmini-c3.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/movecall-cuican-esp32s3.jpg" target="_blank" title="CuiCan">
|
||||
<img src="docs/v1/movecall-cuican-esp32s3.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/wmnologo_xingzhi_1.54.jpg" target="_blank" title="无名科技Nologo-星智-1.54">
|
||||
<a href="docs/v1/wmnologo_xingzhi_1.54.jpg" target="_blank" title="WMnologo-Xingzhi-1.54">
|
||||
<img src="docs/v1/wmnologo_xingzhi_1.54.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/sensecap_watcher.jpg" target="_blank" title="SenseCAP Watcher">
|
||||
<img src="docs/v1/sensecap_watcher.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/esp-hi.jpg" target="_blank" title="ESP-HI 超低成本机器狗">
|
||||
<a href="docs/v1/esp-hi.jpg" target="_blank" title="ESP-HI Low Cost Robot Dog">
|
||||
<img src="docs/v1/esp-hi.jpg" width="240" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## 软件
|
||||
## Software
|
||||
|
||||
### 固件烧录
|
||||
### Firmware Flashing
|
||||
|
||||
新手第一次操作建议先不要搭建开发环境,直接使用免开发环境烧录的固件。
|
||||
For beginners, it is recommended to use the firmware that can be flashed without setting up a development environment.
|
||||
|
||||
固件默认接入 [xiaozhi.me](https://xiaozhi.me) 官方服务器,个人用户注册账号可以免费使用 Qwen 实时模型。
|
||||
The firmware connects to the official [xiaozhi.me](https://xiaozhi.me) server by default. Personal users can register an account to use the Qwen real-time model for free.
|
||||
|
||||
👉 [新手烧录固件教程](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS)
|
||||
👉 [Beginner's Firmware Flashing Guide](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS)
|
||||
|
||||
### 开发环境
|
||||
### Development Environment
|
||||
|
||||
- Cursor 或 VSCode
|
||||
- 安装 ESP-IDF 插件,选择 SDK 版本 5.4 或以上
|
||||
- Linux 比 Windows 更好,编译速度快,也免去驱动问题的困扰
|
||||
- 本项目使用 Google C++ 代码风格,提交代码时请确保符合规范
|
||||
- Cursor or VSCode
|
||||
- Install the ESP-IDF plugin. [ESP-IDF v6.0.2](https://github.com/espressif/esp-idf/releases/tag/v6.0.2) is preferred; use a stable v6.0 or later release. ESP-IDF v5.5.2 is retained only for legacy board compatibility
|
||||
- Linux is better than Windows for faster compilation and fewer driver issues
|
||||
- This project uses Google C++ code style, please ensure compliance when submitting code
|
||||
|
||||
### 开发者文档
|
||||
### Developer Documentation
|
||||
|
||||
- [自定义开发板指南](main/boards/README.md) - 学习如何为小智 AI 创建自定义开发板
|
||||
- [MCP 协议物联网控制用法说明](docs/mcp-usage.md) - 了解如何通过 MCP 协议控制物联网设备
|
||||
- [MCP 协议交互流程](docs/mcp-protocol.md) - 设备端 MCP 协议的实现方式
|
||||
- [MQTT + UDP 混合通信协议文档](docs/mqtt-udp.md)
|
||||
- [一份详细的 WebSocket 通信协议文档](docs/websocket.md)
|
||||
- [ESP-IDF 6.0 Migration Guide](docs/esp-idf-6-migration.md) - SDK compatibility, component changes, legacy hardware support, and board validation status
|
||||
- [Custom Board Guide](docs/custom-board.md) - Learn how to create custom boards for XiaoZhi AI
|
||||
- [MCP Protocol IoT Control Usage](docs/mcp-usage.md) - Learn how to control IoT devices via MCP protocol
|
||||
- [MCP Protocol Interaction Flow](docs/mcp-protocol.md) - Device-side MCP protocol implementation
|
||||
- [MQTT + UDP Hybrid Communication Protocol Document](docs/mqtt-udp.md)
|
||||
- [A detailed WebSocket communication protocol document](docs/websocket.md)
|
||||
|
||||
## 大模型配置
|
||||
## Large Model Configuration
|
||||
|
||||
如果你已经拥有一个的小智 AI 聊天机器人设备,并且已接入官方服务器,可以登录 [xiaozhi.me](https://xiaozhi.me) 控制台进行配置。
|
||||
If you already have a XiaoZhi AI chatbot device and have connected to the official server, you can log in to the [xiaozhi.me](https://xiaozhi.me) console for configuration.
|
||||
|
||||
👉 [后台操作视频教程(旧版界面)](https://www.bilibili.com/video/BV1jUCUY2EKM/)
|
||||
👉 [Backend Operation Video Tutorial (Old Interface)](https://www.bilibili.com/video/BV1jUCUY2EKM/)
|
||||
|
||||
## 相关开源项目
|
||||
## Related Open Source Projects
|
||||
|
||||
在个人电脑上部署服务器,可以参考以下第三方开源的项目:
|
||||
For server deployment on personal computers, refer to the following open-source projects:
|
||||
|
||||
- [xinnan-tech/xiaozhi-esp32-server](https://github.com/xinnan-tech/xiaozhi-esp32-server) Python 服务器
|
||||
- [joey-zhou/xiaozhi-esp32-server-java](https://github.com/joey-zhou/xiaozhi-esp32-server-java) Java 服务器
|
||||
- [AnimeAIChat/xiaozhi-server-go](https://github.com/AnimeAIChat/xiaozhi-server-go) Golang 服务器
|
||||
- [xinnan-tech/xiaozhi-esp32-server](https://github.com/xinnan-tech/xiaozhi-esp32-server) Python server
|
||||
- [joey-zhou/xiaozhi-esp32-server-java](https://github.com/joey-zhou/xiaozhi-esp32-server-java) Java server
|
||||
- [AnimeAIChat/xiaozhi-server-go](https://github.com/AnimeAIChat/xiaozhi-server-go) Golang server
|
||||
- [hackers365/xiaozhi-esp32-server-golang](https://github.com/hackers365/xiaozhi-esp32-server-golang) Golang server
|
||||
|
||||
使用小智通信协议的第三方客户端项目:
|
||||
Other client projects using the XiaoZhi communication protocol:
|
||||
|
||||
- [huangjunsen0406/py-xiaozhi](https://github.com/huangjunsen0406/py-xiaozhi) Python 客户端
|
||||
- [TOM88812/xiaozhi-android-client](https://github.com/TOM88812/xiaozhi-android-client) Android 客户端
|
||||
- [100askTeam/xiaozhi-linux](http://github.com/100askTeam/xiaozhi-linux) 百问科技提供的 Linux 客户端
|
||||
- [78/xiaozhi-sf32](https://github.com/78/xiaozhi-sf32) 思澈科技的蓝牙芯片固件
|
||||
- [QuecPython/solution-xiaozhiAI](https://github.com/QuecPython/solution-xiaozhiAI) 移远提供的 QuecPython 固件
|
||||
- [huangjunsen0406/py-xiaozhi](https://github.com/huangjunsen0406/py-xiaozhi) Python client
|
||||
- [TOM88812/xiaozhi-android-client](https://github.com/TOM88812/xiaozhi-android-client) Android client
|
||||
- [100askTeam/xiaozhi-linux](http://github.com/100askTeam/xiaozhi-linux) Linux client by 100ask
|
||||
- [78/xiaozhi-sf32](https://github.com/78/xiaozhi-sf32) Bluetooth chip firmware by Sichuan
|
||||
- [QuecPython/solution-xiaozhiAI](https://github.com/QuecPython/solution-xiaozhiAI) QuecPython firmware by Quectel
|
||||
|
||||
Custom Assets Tools:
|
||||
|
||||
- [78/xiaozhi-assets-generator](https://github.com/78/xiaozhi-assets-generator) Custom Assets Generator (Wake words, fonts, emojis, backgrounds)
|
||||
|
||||
## About the Project
|
||||
|
||||
This is an open-source ESP32 project, released under the MIT license, allowing anyone to use it for free, including for commercial purposes.
|
||||
|
||||
We hope this project helps everyone understand AI hardware development and apply rapidly evolving large language models to real hardware devices.
|
||||
|
||||
If you have any ideas or suggestions, please feel free to raise Issues or join our [Discord](https://discord.gg/C759fGMBcZ) or QQ group: 1095994019
|
||||
|
||||
## Star History
|
||||
|
||||
|
||||
-157
@@ -1,157 +0,0 @@
|
||||
# An MCP-based Chatbot
|
||||
|
||||
(English | [中文](README.md) | [日本語](README_ja.md))
|
||||
|
||||
## Video
|
||||
|
||||
👉 [Human: Give AI a camera vs AI: Instantly finds out the owner hasn't washed hair for three days【bilibili】](https://www.bilibili.com/video/BV1bpjgzKEhd/)
|
||||
|
||||
👉 [Handcraft your AI girlfriend, beginner's guide【bilibili】](https://www.bilibili.com/video/BV1XnmFYLEJN/)
|
||||
|
||||
## Introduction
|
||||
|
||||
This is an open-source ESP32 project, released under the MIT license, allowing anyone to use it for free, including for commercial purposes.
|
||||
|
||||
We hope this project helps everyone understand AI hardware development and apply rapidly evolving large language models to real hardware devices.
|
||||
|
||||
If you have any ideas or suggestions, please feel free to raise Issues or join the QQ group: 1011329060
|
||||
|
||||
### Control Everything with MCP
|
||||
|
||||
As a voice interaction entry, the XiaoZhi AI chatbot leverages the AI capabilities of large models like Qwen / DeepSeek, and achieves multi-terminal control via the MCP protocol.
|
||||
|
||||

|
||||
|
||||
### Features Implemented
|
||||
|
||||
- Wi-Fi / ML307 Cat.1 4G
|
||||
- Offline voice wake-up [ESP-SR](https://github.com/espressif/esp-sr)
|
||||
- Supports two communication protocols ([Websocket](docs/websocket.md) or MQTT+UDP)
|
||||
- Uses OPUS audio codec
|
||||
- Voice interaction based on streaming ASR + LLM + TTS architecture
|
||||
- Speaker recognition, identifies the current speaker [3D Speaker](https://github.com/modelscope/3D-Speaker)
|
||||
- OLED / LCD display, supports emoji display
|
||||
- Battery display and power management
|
||||
- Multi-language support (Chinese, English, Japanese)
|
||||
- Supports ESP32-C3, ESP32-S3, ESP32-P4 chip platforms
|
||||
- Device-side MCP for device control (Speaker, LED, Servo, GPIO, etc.)
|
||||
- Cloud-side MCP to extend large model capabilities (smart home control, PC desktop operation, knowledge search, email, etc.)
|
||||
|
||||
## Hardware
|
||||
|
||||
### Breadboard DIY Practice
|
||||
|
||||
See the Feishu document tutorial:
|
||||
|
||||
👉 ["XiaoZhi AI Chatbot Encyclopedia"](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb?from=from_copylink)
|
||||
|
||||
Breadboard demo:
|
||||
|
||||

|
||||
|
||||
### Supports 70+ Open Source Hardware (Partial List)
|
||||
|
||||
- <a href="https://oshwhub.com/li-chuang-kai-fa-ban/li-chuang-shi-zhan-pai-esp32-s3-kai-fa-ban" target="_blank" title="LiChuang ESP32-S3 Development Board">LiChuang ESP32-S3 Development Board</a>
|
||||
- <a href="https://github.com/espressif/esp-box" target="_blank" title="Espressif ESP32-S3-BOX3">Espressif ESP32-S3-BOX3</a>
|
||||
- <a href="https://docs.m5stack.com/zh_CN/core/CoreS3" target="_blank" title="M5Stack CoreS3">M5Stack CoreS3</a>
|
||||
- <a href="https://docs.m5stack.com/en/atom/Atomic%20Echo%20Base" target="_blank" title="AtomS3R + Echo Base">M5Stack AtomS3R + Echo Base</a>
|
||||
- <a href="https://gf.bilibili.com/item/detail/1108782064" target="_blank" title="Magic Button 2.4">Magic Button 2.4</a>
|
||||
- <a href="https://www.waveshare.net/shop/ESP32-S3-Touch-AMOLED-1.8.htm" target="_blank" title="Waveshare ESP32-S3-Touch-AMOLED-1.8">Waveshare ESP32-S3-Touch-AMOLED-1.8</a>
|
||||
- <a href="https://github.com/Xinyuan-LilyGO/T-Circle-S3" target="_blank" title="LILYGO T-Circle-S3">LILYGO T-Circle-S3</a>
|
||||
- <a href="https://oshwhub.com/tenclass01/xmini_c3" target="_blank" title="XiaGe Mini C3">XiaGe Mini C3</a>
|
||||
- <a href="https://oshwhub.com/movecall/cuican-ai-pendant-lights-up-y" target="_blank" title="Movecall CuiCan ESP32S3">CuiCan AI Pendant</a>
|
||||
- <a href="https://github.com/WMnologo/xingzhi-ai" target="_blank" title="WMnologo-Xingzhi-1.54">WMnologo-Xingzhi-1.54TFT</a>
|
||||
- <a href="https://www.seeedstudio.com/SenseCAP-Watcher-W1-A-p-5979.html" target="_blank" title="SenseCAP Watcher">SenseCAP Watcher</a>
|
||||
- <a href="https://www.bilibili.com/video/BV1BHJtz6E2S/" target="_blank" title="ESP-HI Low Cost Robot Dog">ESP-HI Low Cost Robot Dog</a>
|
||||
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<a href="docs/v1/lichuang-s3.jpg" target="_blank" title="LiChuang ESP32-S3 Development Board">
|
||||
<img src="docs/v1/lichuang-s3.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/espbox3.jpg" target="_blank" title="Espressif ESP32-S3-BOX3">
|
||||
<img src="docs/v1/espbox3.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/m5cores3.jpg" target="_blank" title="M5Stack CoreS3">
|
||||
<img src="docs/v1/m5cores3.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/atoms3r.jpg" target="_blank" title="AtomS3R + Echo Base">
|
||||
<img src="docs/v1/atoms3r.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/magiclick.jpg" target="_blank" title="Magic Button 2.4">
|
||||
<img src="docs/v1/magiclick.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/waveshare.jpg" target="_blank" title="Waveshare ESP32-S3-Touch-AMOLED-1.8">
|
||||
<img src="docs/v1/waveshare.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/lilygo-t-circle-s3.jpg" target="_blank" title="LILYGO T-Circle-S3">
|
||||
<img src="docs/v1/lilygo-t-circle-s3.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/xmini-c3.jpg" target="_blank" title="XiaGe Mini C3">
|
||||
<img src="docs/v1/xmini-c3.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/movecall-cuican-esp32s3.jpg" target="_blank" title="CuiCan">
|
||||
<img src="docs/v1/movecall-cuican-esp32s3.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/wmnologo_xingzhi_1.54.jpg" target="_blank" title="WMnologo-Xingzhi-1.54">
|
||||
<img src="docs/v1/wmnologo_xingzhi_1.54.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/sensecap_watcher.jpg" target="_blank" title="SenseCAP Watcher">
|
||||
<img src="docs/v1/sensecap_watcher.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/esp-hi.jpg" target="_blank" title="ESP-HI Low Cost Robot Dog">
|
||||
<img src="docs/v1/esp-hi.jpg" width="240" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## Software
|
||||
|
||||
### Firmware Flashing
|
||||
|
||||
For beginners, it is recommended to use the firmware that can be flashed without setting up a development environment.
|
||||
|
||||
The firmware connects to the official [xiaozhi.me](https://xiaozhi.me) server by default. Personal users can register an account to use the Qwen real-time model for free.
|
||||
|
||||
👉 [Beginner's Firmware Flashing Guide](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS)
|
||||
|
||||
### Development Environment
|
||||
|
||||
- Cursor or VSCode
|
||||
- Install ESP-IDF plugin, select SDK version 5.4 or above
|
||||
- Linux is better than Windows for faster compilation and fewer driver issues
|
||||
- This project uses Google C++ code style, please ensure compliance when submitting code
|
||||
|
||||
### Developer Documentation
|
||||
|
||||
- [Custom Board Guide](main/boards/README.md) - Learn how to create custom boards for XiaoZhi AI
|
||||
- [MCP Protocol IoT Control Usage](docs/mcp-usage.md) - Learn how to control IoT devices via MCP protocol
|
||||
- [MCP Protocol Interaction Flow](docs/mcp-protocol.md) - Device-side MCP protocol implementation
|
||||
- [A detailed WebSocket communication protocol document](docs/websocket.md)
|
||||
|
||||
## Large Model Configuration
|
||||
|
||||
If you already have a XiaoZhi AI chatbot device and have connected to the official server, you can log in to the [xiaozhi.me](https://xiaozhi.me) console for configuration.
|
||||
|
||||
👉 [Backend Operation Video Tutorial (Old Interface)](https://www.bilibili.com/video/BV1jUCUY2EKM/)
|
||||
|
||||
## Related Open Source Projects
|
||||
|
||||
For server deployment on personal computers, refer to the following open-source projects:
|
||||
|
||||
- [xinnan-tech/xiaozhi-esp32-server](https://github.com/xinnan-tech/xiaozhi-esp32-server) Python server
|
||||
- [joey-zhou/xiaozhi-esp32-server-java](https://github.com/joey-zhou/xiaozhi-esp32-server-java) Java server
|
||||
- [AnimeAIChat/xiaozhi-server-go](https://github.com/AnimeAIChat/xiaozhi-server-go) Golang server
|
||||
|
||||
Other client projects using the XiaoZhi communication protocol:
|
||||
|
||||
- [huangjunsen0406/py-xiaozhi](https://github.com/huangjunsen0406/py-xiaozhi) Python client
|
||||
- [TOM88812/xiaozhi-android-client](https://github.com/TOM88812/xiaozhi-android-client) Android client
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://star-history.com/#78/xiaozhi-esp32&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
+37
-25
@@ -1,41 +1,39 @@
|
||||
# MCP ベースのチャットボット
|
||||
|
||||
(日本語 | [中文](README.md) | [English](README_en.md))
|
||||
(日本語 | [中文](README_zh.md) | [English](README.md))
|
||||
|
||||
## 動画
|
||||
## はじめに
|
||||
|
||||
👉 [人間:AIにカメラを装着 vs AI:その場で飼い主が3日間髪を洗っていないことを発見【bilibili】](https://www.bilibili.com/video/BV1bpjgzKEhd/)
|
||||
|
||||
👉 [手作りでAIガールフレンドを作る、初心者入門チュートリアル【bilibili】](https://www.bilibili.com/video/BV1XnmFYLEJN/)
|
||||
|
||||
## イントロダクション
|
||||
|
||||
これはエビ兄さんがオープンソースで公開しているESP32プロジェクトで、MITライセンスのもと、誰でも無料で、商用利用も可能です。
|
||||
|
||||
このプロジェクトを通じて、AIハードウェア開発を理解し、急速に進化する大規模言語モデルを実際のハードウェアデバイスに応用できるようになることを目指しています。
|
||||
|
||||
ご意見やご提案があれば、いつでもIssueを提出するか、QQグループ:1011329060 にご参加ください。
|
||||
|
||||
### MCPであらゆるものを制御
|
||||
|
||||
シャオジーAIチャットボットは音声インタラクションの入口として、Qwen / DeepSeekなどの大規模モデルのAI能力を活用し、MCPプロトコルを通じてマルチエンド制御を実現します。
|
||||
|
||||

|
||||
<img src="docs/mcp-based-graph.jpg" alt="MCPであらゆるものを制御" width="320">
|
||||
|
||||
## 最近の更新
|
||||
|
||||
- メインラインはESP-IDF v6.0以降へ移行し、推奨安定版はv6.0.2です。従来の157リリースバリアントはESP-IDF v6.0.1でビルド検証済みです。現在のマトリクスは171バリアントで、そのうち170はIDF 6.0.xに対応し、ESP32-S31バリアントにはIDF 6.1以降が必要です。
|
||||
- MQTTとBluFiの暗号処理をPSA Cryptoへ移行し、IDF 6のコンポーネント分割およびサードパーティ依存関係にも対応しました。
|
||||
- オーディオパイプラインの並行処理、MQTT/UDPパケット検証、リリースマトリクス選択処理を強化しました。
|
||||
- ESP-IDF v5.5は、文書で明記された旧式ボード向けにのみ残しています。ESP-SR 2.4.7を使用すると、ESP32-P4 Rev1とRev3の両方がIDF 6に対応します。詳細な互換性とボード検証状況は、[ESP-IDF 6.0移行ガイド](docs/esp-idf-6-migration.md)を参照してください。
|
||||
|
||||
### 実装済み機能
|
||||
|
||||
- Wi-Fi / ML307 Cat.1 4G
|
||||
- オフライン音声ウェイクアップ [ESP-SR](https://github.com/espressif/esp-sr)
|
||||
- 2種類の通信プロトコルに対応([Websocket](docs/websocket.md) または MQTT+UDP)
|
||||
- OPUSオーディオコーデックを採用
|
||||
- ストリーミングASR + LLM + TTSアーキテクチャに基づく音声インタラクション
|
||||
- Wi-Fi、有線Ethernet、USB RNDIS、およびML307/EC801EまたはNT26 Cat.1 4Gに対応し、一部のボードではWi-Fiと4Gを切り替え可能
|
||||
- [ESP-SR](https://github.com/espressif/esp-sr)によるオフライン音声ウェイクアップとカスタムウェイクワード
|
||||
- 2種類の通信方式:[WebSocket](docs/websocket.md)と[MQTT + UDP](docs/mqtt-udp.md)
|
||||
- Opusオーディオストリーミングにより、従来のストリーミングASR + LLM + TTS構成とRealtimeエンドツーエンド音声モデルの両方に対応。AEC対応ハードウェアではリアルタイム全二重対話が可能
|
||||
- 話者認識、現在話している人を識別 [3D Speaker](https://github.com/modelscope/3D-Speaker)
|
||||
- OLED / LCDディスプレイ、表情表示対応
|
||||
- OLED / LCDディスプレイで絵文字や豊かな感情表現を表示し、一部のボードではカメラによる視覚入力にも対応
|
||||
- バッテリー表示と電源管理
|
||||
- 多言語対応(中国語、英語、日本語)
|
||||
- ESP32-C3、ESP32-S3、ESP32-P4チッププラットフォーム対応
|
||||
- 38言語の画面表示に対応し、音声プロンプトはローカライズ済みリソースを優先して、未収録時は英語へフォールバック
|
||||
- ESP32、ESP32-C3、ESP32-C5、ESP32-C6、ESP32-S3、ESP32-P4チッププラットフォーム
|
||||
- ホットスポットまたはBluFiによるWi-Fiプロビジョニング
|
||||
- デバイス側MCPによるデバイス制御(音量・明るさ調整、アクション制御など)
|
||||
- クラウド側MCPで大規模モデル能力を拡張(スマートホーム制御、PCデスクトップ操作、知識検索、メール送受信など)
|
||||
- カスタマイズ可能なウェイクワード、フォント、絵文字、チャット背景、オンラインWeb編集に対応 ([カスタムアセットジェネレーター](https://github.com/78/xiaozhi-assets-generator))
|
||||
|
||||
## ハードウェア
|
||||
|
||||
@@ -49,10 +47,10 @@ Feishuドキュメントチュートリアルをご覧ください:
|
||||
|
||||

|
||||
|
||||
### 70種類以上のオープンソースハードウェアに対応(一部のみ表示)
|
||||
### 138のボードディレクトリと171のリリースバリアントに対応(一部のみ表示)
|
||||
|
||||
- <a href="https://oshwhub.com/li-chuang-kai-fa-ban/li-chuang-shi-zhan-pai-esp32-s3-kai-fa-ban" target="_blank" title="立創・実戦派 ESP32-S3 開発ボード">立創・実戦派 ESP32-S3 開発ボード</a>
|
||||
- <a href="https://github.com/espressif/esp-box" target="_blank" title="楽鑫 ESP32-S3-BOX3">楽鑫 ESP32-S3-BOX3</a>
|
||||
- <a href="https://github.com/espressif/esp-box" target="_blank" title="楽鑫 ESP32-S3-BOX-3">楽鑫 ESP32-S3-BOX-3</a>
|
||||
- <a href="https://docs.m5stack.com/zh_CN/core/CoreS3" target="_blank" title="M5Stack CoreS3">M5Stack CoreS3</a>
|
||||
- <a href="https://docs.m5stack.com/en/atom/Atomic%20Echo%20Base" target="_blank" title="AtomS3R + Echo Base">M5Stack AtomS3R + Echo Base</a>
|
||||
- <a href="https://gf.bilibili.com/item/detail/1108782064" target="_blank" title="マジックボタン2.4">マジックボタン2.4</a>
|
||||
@@ -116,15 +114,17 @@ Feishuドキュメントチュートリアルをご覧ください:
|
||||
### 開発環境
|
||||
|
||||
- Cursor または VSCode
|
||||
- ESP-IDFプラグインをインストールし、SDKバージョン5.4以上を選択
|
||||
- ESP-IDFプラグインをインストールし、[ESP-IDF v6.0.2](https://github.com/espressif/esp-idf/releases/tag/v6.0.2)を優先して使用してください。v6.0以降の安定版を推奨し、ESP-IDF v5.5.2は旧ハードウェアとの互換性維持にのみ使用します
|
||||
- LinuxはWindowsよりも優れており、コンパイルが速く、ドライバの問題も少ない
|
||||
- 本プロジェクトはGoogle C++コードスタイルを採用、コード提出時は準拠を確認してください
|
||||
|
||||
### 開発者ドキュメント
|
||||
|
||||
- [カスタム開発ボードガイド](main/boards/README.md) - シャオジーAI用のカスタム開発ボード作成方法
|
||||
- [ESP-IDF 6.0移行ガイド](docs/esp-idf-6-migration.md) - SDK互換性、コンポーネント変更、旧ハードウェア対応、ボード検証状況
|
||||
- [カスタム開発ボードガイド](docs/custom-board.md) - シャオジーAI用のカスタム開発ボード作成方法
|
||||
- [MCPプロトコルIoT制御使用法](docs/mcp-usage.md) - MCPプロトコルでIoTデバイスを制御する方法
|
||||
- [MCPプロトコルインタラクションフロー](docs/mcp-protocol.md) - デバイス側MCPプロトコルの実装方法
|
||||
- [MQTT + UDP ハイブリッド通信プロトコルドキュメント](docs/mqtt-udp.md)
|
||||
- [詳細なWebSocket通信プロトコルドキュメント](docs/websocket.md)
|
||||
|
||||
## 大規模モデル設定
|
||||
@@ -140,11 +140,23 @@ Feishuドキュメントチュートリアルをご覧ください:
|
||||
- [xinnan-tech/xiaozhi-esp32-server](https://github.com/xinnan-tech/xiaozhi-esp32-server) Pythonサーバー
|
||||
- [joey-zhou/xiaozhi-esp32-server-java](https://github.com/joey-zhou/xiaozhi-esp32-server-java) Javaサーバー
|
||||
- [AnimeAIChat/xiaozhi-server-go](https://github.com/AnimeAIChat/xiaozhi-server-go) Golangサーバー
|
||||
- [hackers365/xiaozhi-esp32-server-golang](https://github.com/hackers365/xiaozhi-esp32-server-golang) Golangサーバー
|
||||
|
||||
シャオジー通信プロトコルを利用した他のクライアントプロジェクト:
|
||||
|
||||
- [huangjunsen0406/py-xiaozhi](https://github.com/huangjunsen0406/py-xiaozhi) Pythonクライアント
|
||||
- [TOM88812/xiaozhi-android-client](https://github.com/TOM88812/xiaozhi-android-client) Androidクライアント
|
||||
- [100askTeam/xiaozhi-linux](http://github.com/100askTeam/xiaozhi-linux) 百問科技提供のLinuxクライアント
|
||||
- [78/xiaozhi-sf32](https://github.com/78/xiaozhi-sf32) 思澈科技のBluetoothチップファームウェア
|
||||
- [QuecPython/solution-xiaozhiAI](https://github.com/QuecPython/solution-xiaozhiAI) 移遠提供のQuecPythonファームウェア
|
||||
|
||||
## プロジェクトについて
|
||||
|
||||
これはエビ兄さんがオープンソースで公開しているESP32プロジェクトで、MITライセンスのもと、誰でも無料で、商用利用も可能です。
|
||||
|
||||
このプロジェクトを通じて、AIハードウェア開発を理解し、急速に進化する大規模言語モデルを実際のハードウェアデバイスに応用できるようになることを目指しています。
|
||||
|
||||
ご意見やご提案があれば、いつでもIssueを提出するか、[Discord](https://discord.gg/C759fGMBcZ) または QQグループ:1011329060 にご参加ください。
|
||||
|
||||
## スター履歴
|
||||
|
||||
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
# An MCP-based Chatbot
|
||||
|
||||
(中文 | [English](README.md) | [日本語](README_ja.md))
|
||||
|
||||
## 介绍
|
||||
|
||||
👉 [人类:给 AI 装摄像头 vs AI:当场发现主人三天没洗头【bilibili】](https://www.bilibili.com/video/BV1bpjgzKEhd/)
|
||||
|
||||
👉 [手工打造你的 AI 女友,新手入门教程【bilibili】](https://www.bilibili.com/video/BV1XnmFYLEJN/)
|
||||
|
||||
小智 AI 聊天机器人作为一个语音交互入口,利用 Qwen / DeepSeek 等大模型的 AI 能力,通过 MCP 协议实现多端控制。
|
||||
|
||||
<img src="docs/mcp-based-graph.jpg" alt="通过MCP控制万物" width="320">
|
||||
|
||||
## 近期更新
|
||||
|
||||
- 项目主线现已迁移到 ESP-IDF v6.0 或以上版本,首选稳定版为 v6.0.2;此前的 157 个发布变体已在 ESP-IDF v6.0.1 上通过构建验证。当前矩阵包含 171 个变体,其中 170 个支持 IDF 6.0.x,ESP32-S31 变体需要 IDF 6.1 或以上版本。
|
||||
- MQTT 和 BluFi 加密已迁移到 PSA Crypto,同时完成了 IDF 6 组件拆分及第三方依赖兼容处理。
|
||||
- 加固了音频流水线并发、MQTT/UDP 数据包校验和发布矩阵选择逻辑。
|
||||
- ESP-IDF v5.5 仅保留用于文档明确标注的旧版板卡;使用 ESP-SR 2.4.7 时,ESP32-P4 Rev1 和 Rev3 均支持 IDF 6。完整兼容性和板卡验证状态请参阅 [ESP-IDF 6.0 迁移文档](docs/esp-idf-6-migration.md)。
|
||||
|
||||
### 已实现功能
|
||||
|
||||
- 支持 Wi-Fi、有线以太网、USB RNDIS,以及 ML307/EC801E 或 NT26 Cat.1 4G 网络;部分硬件支持 Wi-Fi 与 4G 切换
|
||||
- 基于 [ESP-SR](https://github.com/espressif/esp-sr) 的离线语音唤醒,支持自定义唤醒词
|
||||
- 支持两种通信传输方式:[WebSocket](docs/websocket_zh.md) 和 [MQTT + UDP](docs/mqtt-udp_zh.md)
|
||||
- 采用 Opus 音频流,既支持传统的流式 ASR + LLM + TTS 方案,也支持 Realtime 端到端语音模型;具备 AEC 的硬件可实现实时全双工交互
|
||||
- 声纹识别,识别当前说话人的身份 [3D Speaker](https://github.com/modelscope/3D-Speaker)
|
||||
- OLED / LCD 显示屏,支持表情和丰富的情绪呈现;部分硬件支持摄像头视觉输入
|
||||
- 电量显示与电源管理
|
||||
- 提供 38 种界面语言;语音提示优先使用本地化资源,缺失时自动回退到英文
|
||||
- 支持 ESP32、ESP32-C3、ESP32-C5、ESP32-C6、ESP32-S3、ESP32-P4 芯片平台
|
||||
- 支持热点和 BluFi 两种 Wi-Fi 配网方式
|
||||
- 通过设备端 MCP 实现设备控制(音量、灯光、电机、GPIO 等)
|
||||
- 通过云端 MCP 扩展大模型能力(智能家居控制、PC桌面操作、知识搜索、邮件收发等)
|
||||
- 自定义唤醒词、字体、表情与聊天背景,支持网页端在线修改 ([自定义Assets生成器](https://github.com/78/xiaozhi-assets-generator))
|
||||
|
||||
## 硬件
|
||||
|
||||
### 面包板手工制作实践
|
||||
|
||||
详见飞书文档教程:
|
||||
|
||||
👉 [《小智 AI 聊天机器人百科全书》](https://ccnphfhqs21z.feishu.cn/wiki/F5krwD16viZoF0kKkvDcrZNYnhb?from=from_copylink)
|
||||
|
||||
面包板效果图如下:
|
||||
|
||||

|
||||
|
||||
### 支持 138 个板卡目录、171 个固件发布变体(仅展示部分)
|
||||
|
||||
- <a href="https://oshwhub.com/li-chuang-kai-fa-ban/li-chuang-shi-zhan-pai-esp32-s3-kai-fa-ban" target="_blank" title="立创·实战派 ESP32-S3 开发板">立创·实战派 ESP32-S3 开发板</a>
|
||||
- <a href="https://github.com/espressif/esp-box" target="_blank" title="乐鑫 ESP32-S3-BOX-3">乐鑫 ESP32-S3-BOX-3</a>
|
||||
- <a href="https://docs.m5stack.com/zh_CN/core/CoreS3" target="_blank" title="M5Stack CoreS3">M5Stack CoreS3</a>
|
||||
- <a href="https://docs.m5stack.com/en/atom/Atomic%20Echo%20Base" target="_blank" title="AtomS3R + Echo Base">M5Stack AtomS3R + Echo Base</a>
|
||||
- <a href="https://gf.bilibili.com/item/detail/1108782064" target="_blank" title="神奇按钮 2.4">神奇按钮 2.4</a>
|
||||
- <a href="https://www.waveshare.net/shop/ESP32-S3-Touch-AMOLED-1.8.htm" target="_blank" title="微雪电子 ESP32-S3-Touch-AMOLED-1.8">微雪电子 ESP32-S3-Touch-AMOLED-1.8</a>
|
||||
- <a href="https://github.com/Xinyuan-LilyGO/T-Circle-S3" target="_blank" title="LILYGO T-Circle-S3">LILYGO T-Circle-S3</a>
|
||||
- <a href="https://oshwhub.com/tenclass01/xmini_c3" target="_blank" title="虾哥 Mini C3">虾哥 Mini C3</a>
|
||||
- <a href="https://oshwhub.com/movecall/cuican-ai-pendant-lights-up-y" target="_blank" title="Movecall CuiCan ESP32S3">璀璨·AI 吊坠</a>
|
||||
- <a href="https://github.com/WMnologo/xingzhi-ai" target="_blank" title="无名科技Nologo-星智-1.54">无名科技 Nologo-星智-1.54TFT</a>
|
||||
- <a href="https://www.seeedstudio.com/SenseCAP-Watcher-W1-A-p-5979.html" target="_blank" title="SenseCAP Watcher">SenseCAP Watcher</a>
|
||||
- <a href="https://www.bilibili.com/video/BV1BHJtz6E2S/" target="_blank" title="ESP-HI 超低成本机器狗">ESP-HI 超低成本机器狗</a>
|
||||
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<a href="docs/v1/lichuang-s3.jpg" target="_blank" title="立创·实战派 ESP32-S3 开发板">
|
||||
<img src="docs/v1/lichuang-s3.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/espbox3.jpg" target="_blank" title="乐鑫 ESP32-S3-BOX3">
|
||||
<img src="docs/v1/espbox3.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/m5cores3.jpg" target="_blank" title="M5Stack CoreS3">
|
||||
<img src="docs/v1/m5cores3.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/atoms3r.jpg" target="_blank" title="AtomS3R + Echo Base">
|
||||
<img src="docs/v1/atoms3r.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/magiclick.jpg" target="_blank" title="神奇按钮 2.4">
|
||||
<img src="docs/v1/magiclick.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/waveshare.jpg" target="_blank" title="微雪电子 ESP32-S3-Touch-AMOLED-1.8">
|
||||
<img src="docs/v1/waveshare.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/lilygo-t-circle-s3.jpg" target="_blank" title="LILYGO T-Circle-S3">
|
||||
<img src="docs/v1/lilygo-t-circle-s3.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/xmini-c3.jpg" target="_blank" title="虾哥 Mini C3">
|
||||
<img src="docs/v1/xmini-c3.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/movecall-cuican-esp32s3.jpg" target="_blank" title="CuiCan">
|
||||
<img src="docs/v1/movecall-cuican-esp32s3.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/wmnologo_xingzhi_1.54.jpg" target="_blank" title="无名科技Nologo-星智-1.54">
|
||||
<img src="docs/v1/wmnologo_xingzhi_1.54.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/sensecap_watcher.jpg" target="_blank" title="SenseCAP Watcher">
|
||||
<img src="docs/v1/sensecap_watcher.jpg" width="240" />
|
||||
</a>
|
||||
<a href="docs/v1/esp-hi.jpg" target="_blank" title="ESP-HI 超低成本机器狗">
|
||||
<img src="docs/v1/esp-hi.jpg" width="240" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## 软件
|
||||
|
||||
### 固件烧录
|
||||
|
||||
新手第一次操作建议先不要搭建开发环境,直接使用免开发环境烧录的固件。
|
||||
|
||||
固件默认接入 [xiaozhi.me](https://xiaozhi.me) 官方服务器,个人用户注册账号可以免费使用 Qwen 实时模型。
|
||||
|
||||
👉 [新手烧录固件教程](https://ccnphfhqs21z.feishu.cn/wiki/Zpz4wXBtdimBrLk25WdcXzxcnNS)
|
||||
|
||||
### 开发环境
|
||||
|
||||
- Cursor 或 VSCode
|
||||
- 安装 ESP-IDF 插件,首选 [ESP-IDF v6.0.2](https://github.com/espressif/esp-idf/releases/tag/v6.0.2),建议使用 v6.0 或以上的稳定版本;ESP-IDF v5.5.2 仅保留用于旧版硬件兼容
|
||||
- Linux 比 Windows 更好,编译速度快,也免去驱动问题的困扰
|
||||
- 本项目使用 Google C++ 代码风格,提交代码时请确保符合规范
|
||||
|
||||
### 开发者文档
|
||||
|
||||
- [ESP-IDF 6.0 迁移文档](docs/esp-idf-6-migration.md) - SDK 兼容性、组件变更、旧版硬件支持和板卡验证状态
|
||||
- [自定义开发板指南](docs/custom-board_zh.md) - 学习如何为小智 AI 创建自定义开发板
|
||||
- [MCP 协议物联网控制用法说明](docs/mcp-usage_zh.md) - 了解如何通过 MCP 协议控制物联网设备
|
||||
- [MCP 协议交互流程](docs/mcp-protocol_zh.md) - 设备端 MCP 协议的实现方式
|
||||
- [MQTT + UDP 混合通信协议文档](docs/mqtt-udp_zh.md)
|
||||
- [一份详细的 WebSocket 通信协议文档](docs/websocket_zh.md)
|
||||
|
||||
## 大模型配置
|
||||
|
||||
如果你已经拥有一个小智 AI 聊天机器人设备,并且已接入官方服务器,可以登录 [xiaozhi.me](https://xiaozhi.me) 控制台进行配置。
|
||||
|
||||
👉 [后台操作视频教程(旧版界面)](https://www.bilibili.com/video/BV1jUCUY2EKM/)
|
||||
|
||||
## 相关开源项目
|
||||
|
||||
在个人电脑上部署服务器,可以参考以下第三方开源的项目:
|
||||
|
||||
- [xinnan-tech/xiaozhi-esp32-server](https://github.com/xinnan-tech/xiaozhi-esp32-server) Python 服务器
|
||||
- [joey-zhou/xiaozhi-esp32-server-java](https://github.com/joey-zhou/xiaozhi-esp32-server-java) Java 服务器
|
||||
- [AnimeAIChat/xiaozhi-server-go](https://github.com/AnimeAIChat/xiaozhi-server-go) Golang 服务器
|
||||
- [hackers365/xiaozhi-esp32-server-golang](https://github.com/hackers365/xiaozhi-esp32-server-golang) Golang 服务器
|
||||
|
||||
使用小智通信协议的第三方客户端项目:
|
||||
|
||||
- [huangjunsen0406/py-xiaozhi](https://github.com/huangjunsen0406/py-xiaozhi) Python 客户端
|
||||
- [TOM88812/xiaozhi-android-client](https://github.com/TOM88812/xiaozhi-android-client) Android 客户端
|
||||
- [100askTeam/xiaozhi-linux](http://github.com/100askTeam/xiaozhi-linux) 百问科技提供的 Linux 客户端
|
||||
- [78/xiaozhi-sf32](https://github.com/78/xiaozhi-sf32) 思澈科技的蓝牙芯片固件
|
||||
- [QuecPython/solution-xiaozhiAI](https://github.com/QuecPython/solution-xiaozhiAI) 移远提供的 QuecPython 固件
|
||||
|
||||
## 关于项目
|
||||
|
||||
这是一个由虾哥开源的 ESP32 项目,以 MIT 许可证发布,允许任何人免费使用,修改或用于商业用途。
|
||||
|
||||
我们希望通过这个项目,能够帮助大家了解 AI 硬件开发,将当下飞速发展的大语言模型应用到实际的硬件设备中。
|
||||
|
||||
如果你有任何想法或建议,请随时提出 Issues 或加入 [Discord](https://discord.gg/C759fGMBcZ) 或 QQ 群:1011329060
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://star-history.com/#78/xiaozhi-esp32&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=78/xiaozhi-esp32&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
@@ -0,0 +1,34 @@
|
||||
# BluFi Provisioning (with `esp-wifi-connect`)
|
||||
|
||||
This document explains how to enable and use BluFi (BLE-based WiFi provisioning) in the XiaoZhi firmware, together with the in-tree `esp-wifi-connect` component that handles WiFi connection and credential storage. See the official [Espressif BluFi documentation](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/ble/blufi.html) for the protocol details.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A chip and firmware configuration that support BLE.
|
||||
- In `idf.py menuconfig`, enable `WiFi Configuration Method -> ESP-BluFi` (`CONFIG_USE_ESP_BLUFI_WIFI_PROVISIONING=y`). If you want to use BluFi, disable the Hotspot option in the same menu; otherwise hotspot provisioning wins by default.
|
||||
- Keep the default NVS and event-loop initialization provided by the project's `app_main`.
|
||||
- Exactly one of `CONFIG_BT_BLUEDROID_ENABLED` / `CONFIG_BT_NIMBLE_ENABLED` must be selected; they are mutually exclusive.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. A phone (using the official EspBlufi app or another BluFi client) connects to the device over BLE and sends the target WiFi SSID / password. The phone can also request the list of WiFi networks scanned by the device through the BluFi protocol.
|
||||
2. In `ESP_BLUFI_EVENT_REQ_CONNECT_TO_AP`, the device stores the credentials into `SsidManager` (persisted in NVS by the `esp-wifi-connect` component).
|
||||
3. The device then launches `WifiStation` to scan and connect; progress is reported back over BluFi.
|
||||
4. If provisioning succeeds, the device connects to the new WiFi automatically. If it fails, an error status is sent back.
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Configure**: turn on `ESP-BluFi` in menuconfig, then build and flash the firmware.
|
||||
2. **Trigger provisioning**: at first boot with no stored WiFi credentials the device enters provisioning automatically.
|
||||
3. **Phone side**: open the EspBlufi app (or another BluFi client), scan and connect to the device, optionally enable encryption, then enter the WiFi SSID / password and send them.
|
||||
4. **Observe the result**:
|
||||
- Success: BluFi reports success and the device connects to WiFi.
|
||||
- Failure: BluFi reports failure; retry or check the router.
|
||||
|
||||
## Notes
|
||||
|
||||
- BluFi cannot be used at the same time as hotspot provisioning. If hotspot provisioning is already enabled, the device will use it. Keep only one provisioning method in menuconfig.
|
||||
- When running repeated tests, clear or overwrite the stored SSID (`wifi` NVS namespace) to avoid stale credentials interfering with the next run.
|
||||
- If you write your own BluFi client, follow the official protocol frame format linked above.
|
||||
- The EspBlufi app download links are listed in the official documentation.
|
||||
- Because the BluFi API changed in IDF 5.5.2, firmware built with 5.5.2 advertises the Bluetooth name as `"Xiaozhi-Blufi"`, while 5.5.1 uses `"BLUFI_DEVICE"`.
|
||||
@@ -0,0 +1,37 @@
|
||||
# BluFi 配网(集成 esp-wifi-connect)
|
||||
|
||||
本文档说明如何在小智固件中启用和使用 BluFi(BLE Wi‑Fi 配网),并结合项目内置的 `esp-wifi-connect` 组件完成 Wi‑Fi 连接与存储。官方
|
||||
BluFi
|
||||
协议说明请参考 [Espressif 文档](https://docs.espressif.com/projects/esp-idf/zh_CN/stable/esp32/api-guides/ble/blufi.html)。
|
||||
|
||||
## 前置条件
|
||||
|
||||
- 需要支持 BLE 的芯片与固件配置。
|
||||
- 在 `idf.py menuconfig` 中启用 `WiFi Configuration Method -> ESP-BluFi`(`CONFIG_USE_ESP_BLUFI_WIFI_PROVISIONING=y`
|
||||
)。如果想使用 BluFi,必须关闭同一菜单下的 Hotspot 选项,否则默认使用 Hotspot 配网模式。
|
||||
|
||||
- 保持默认的 NVS 与事件循环初始化(项目的 `app_main` 已处理)。
|
||||
- CONFIG_BT_BLUEDROID_ENABLED、CONFIG_BT_NIMBLE_ENABLED这两个宏应二选一,不能同时启用。
|
||||
## 工作流程
|
||||
|
||||
1) 手机端通过 BluFi(如官方 EspBlufi App 或自研客户端)连接设备,发送 Wi‑Fi SSID/密码,手机端可以通过blufi协议获取设备端扫描到的WiFi列表。
|
||||
2) 设备侧在 `ESP_BLUFI_EVENT_REQ_CONNECT_TO_AP` 中将凭据写入 `SsidManager`(存储到 NVS,属于 `esp-wifi-connect` 组件)。
|
||||
3) 随后启动 `WifiStation` 扫描并连接;状态通过 BluFi 返回。
|
||||
4) 配网成功后设备会自动连接新 Wi‑Fi;失败则返回失败状态。
|
||||
|
||||
## 使用步骤
|
||||
|
||||
1. 配置:在 menuconfig 开启 `ESP-BluFi`。编译并烧录固件。
|
||||
2. 触发配网:设备首次启动且没有已保存的 Wi‑Fi 时会自动进入配网。
|
||||
3. 手机端操作:打开 EspBlufi App(或其他 BluFi 客户端),搜索并连接设备,可以选择是否加密,按提示输入 Wi‑Fi SSID/密码并发送。
|
||||
4. 观察结果:
|
||||
- 成功:BluFi 报告连接成功,设备自动连接 Wi‑Fi。
|
||||
- 失败:BluFi 返回失败状态,可重新发送或检查路由器。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- BluFi 配网不支持与热点配网同时开启。如果热点配网已经启动,则默认使用热点配网。请在 menuconfig 中只保留一种配网方式。
|
||||
- 若多次测试,建议清除或覆盖存储的 SSID(`wifi` 命名空间),避免旧配置干扰。
|
||||
- 如果使用自定义 BluFi 客户端,需遵循官方协议帧格式,参考上文官方文档链接。
|
||||
- 官方文档中已提供EspBlufi APP下载地址
|
||||
- 由于IDF5.5.2的blufi接口发生变化,5.5.2版本编译后蓝牙名称为"Xiaozhi-Blufi",5.5.1版本中蓝牙名称为"BLUFI_DEVICE"
|
||||
@@ -0,0 +1,90 @@
|
||||
# Code Style Guide
|
||||
|
||||
## Formatting Tool
|
||||
|
||||
This project uses `clang-format` to keep the code style consistent. The `.clang-format` file in the project root is based on the Google C++ style guide with a few project-specific tweaks.
|
||||
|
||||
### Installing clang-format
|
||||
|
||||
Make sure `clang-format` is available before you use it:
|
||||
|
||||
- **Windows**:
|
||||
```powershell
|
||||
winget install LLVM
|
||||
# or with Chocolatey
|
||||
choco install llvm
|
||||
```
|
||||
|
||||
- **Linux**:
|
||||
```bash
|
||||
sudo apt install clang-format # Ubuntu/Debian
|
||||
sudo dnf install clang-tools-extra # Fedora
|
||||
```
|
||||
|
||||
- **macOS**:
|
||||
```bash
|
||||
brew install clang-format
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
1. **Format a single file**:
|
||||
```bash
|
||||
clang-format -i path/to/your/file.cpp
|
||||
```
|
||||
|
||||
2. **Format the entire project**:
|
||||
```bash
|
||||
# Run from the project root
|
||||
find main -iname '*.h' -o -iname '*.cc' | xargs clang-format -i
|
||||
```
|
||||
|
||||
3. **Check formatting without modifying files (useful in CI / pre-commit)**:
|
||||
```bash
|
||||
clang-format --dry-run -Werror path/to/your/file.cpp
|
||||
```
|
||||
|
||||
### IDE Integration
|
||||
|
||||
- **Visual Studio Code**:
|
||||
1. Install the C/C++ extension.
|
||||
2. Set `C_Cpp.formatting` to `clangFormat` in settings.
|
||||
3. Optionally enable `editor.formatOnSave`.
|
||||
|
||||
- **CLion**:
|
||||
1. Open `Editor > Code Style > C/C++` in the settings.
|
||||
2. Set `Formatter` to `clang-format`.
|
||||
3. Choose "use the .clang-format file in the project".
|
||||
|
||||
### Main Rules
|
||||
|
||||
- Indent with 4 spaces.
|
||||
- Line width capped at 100 characters.
|
||||
- Attach-style braces (`{` on the same line as the control statement).
|
||||
- Pointers and references bind to the type (left alignment).
|
||||
- Includes are sorted automatically.
|
||||
- Access specifiers are indented by -4 spaces.
|
||||
|
||||
### Notes
|
||||
|
||||
1. Make sure the code has been formatted before committing.
|
||||
2. Do not fix up alignment by hand after running clang-format.
|
||||
3. To exclude a block from formatting, wrap it with:
|
||||
```cpp
|
||||
// clang-format off
|
||||
your code
|
||||
// clang-format on
|
||||
```
|
||||
|
||||
### FAQ
|
||||
|
||||
1. **Formatting fails**:
|
||||
- Check whether `clang-format` is too old.
|
||||
- Make sure the file is UTF-8 encoded.
|
||||
- Validate the syntax of your `.clang-format` file.
|
||||
|
||||
2. **Output differs from what you expected**:
|
||||
- Verify that the `.clang-format` in the project root is actually picked up.
|
||||
- Make sure no other `.clang-format` higher in the tree is winning.
|
||||
|
||||
Questions and suggestions are welcome - please open an issue or a pull request.
|
||||
@@ -0,0 +1,91 @@
|
||||
# 代码风格指南
|
||||
|
||||
## 代码格式化工具
|
||||
|
||||
本项目使用 clang-format 工具来统一代码风格。我们已经在项目根目录下提供了 `.clang-format` 配置文件,该配置基于 Google C++ 风格指南,并做了一些自定义调整。
|
||||
|
||||
### 安装 clang-format
|
||||
|
||||
在使用之前,请确保你已经安装了 clang-format 工具:
|
||||
|
||||
- **Windows**:
|
||||
```powershell
|
||||
winget install LLVM
|
||||
# 或者使用 Chocolatey
|
||||
choco install llvm
|
||||
```
|
||||
|
||||
- **Linux**:
|
||||
```bash
|
||||
sudo apt install clang-format # Ubuntu/Debian
|
||||
sudo dnf install clang-tools-extra # Fedora
|
||||
```
|
||||
|
||||
- **macOS**:
|
||||
```bash
|
||||
brew install clang-format
|
||||
```
|
||||
|
||||
### 使用方法
|
||||
|
||||
1. **格式化单个文件**:
|
||||
```bash
|
||||
clang-format -i path/to/your/file.cpp
|
||||
```
|
||||
|
||||
2. **格式化整个项目**:
|
||||
```bash
|
||||
# 在项目根目录下执行
|
||||
find main -iname *.h -o -iname *.cc | xargs clang-format -i
|
||||
```
|
||||
|
||||
3. **在提交代码前检查格式**:
|
||||
```bash
|
||||
# 检查文件格式是否符合规范(不修改文件)
|
||||
clang-format --dry-run -Werror path/to/your/file.cpp
|
||||
```
|
||||
|
||||
### IDE 集成
|
||||
|
||||
- **Visual Studio Code**:
|
||||
1. 安装 C/C++ 扩展
|
||||
2. 在设置中启用 `C_Cpp.formatting` 为 `clang-format`
|
||||
3. 可以设置保存时自动格式化:`editor.formatOnSave: true`
|
||||
|
||||
- **CLion**:
|
||||
1. 在设置中选择 `Editor > Code Style > C/C++`
|
||||
2. 将 `Formatter` 设置为 `clang-format`
|
||||
3. 选择使用项目中的 `.clang-format` 配置文件
|
||||
|
||||
### 主要格式规则
|
||||
|
||||
- 缩进使用 4 个空格
|
||||
- 行宽限制为 100 字符
|
||||
- 大括号采用 Attach 风格(与控制语句在同一行)
|
||||
- 指针和引用符号靠左对齐
|
||||
- 自动排序头文件包含
|
||||
- 类访问修饰符缩进为 -4 空格
|
||||
|
||||
### 注意事项
|
||||
|
||||
1. 提交代码前请确保代码已经过格式化
|
||||
2. 不要手动调整已格式化的代码对齐
|
||||
3. 如果某段代码不希望被格式化,可以使用以下注释包围:
|
||||
```cpp
|
||||
// clang-format off
|
||||
// 你的代码
|
||||
// clang-format on
|
||||
```
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **格式化失败**:
|
||||
- 检查 clang-format 版本是否过低
|
||||
- 确认文件编码为 UTF-8
|
||||
- 验证 .clang-format 文件语法是否正确
|
||||
|
||||
2. **与期望格式不符**:
|
||||
- 检查是否使用了项目根目录下的 .clang-format 配置
|
||||
- 确认没有其他位置的 .clang-format 文件被优先使用
|
||||
|
||||
如有任何问题或建议,欢迎提出 issue 或 pull request。
|
||||
@@ -0,0 +1,515 @@
|
||||
# Custom Board Guide
|
||||
|
||||
This guide describes how to add a new board to the XiaoZhi AI voice assistant project. XiaoZhi AI supports 70+ ESP32-series boards; each one lives in its own directory under `main/boards/`.
|
||||
|
||||
## Important
|
||||
|
||||
> **Warning**: for a custom board whose IO configuration differs from an existing board, never overwrite the original board's configuration. Always create a new board type - or use the `builds` array in `config.json` to produce a distinct firmware name with different `sdkconfig` macros. Use `python scripts/build.py [board-directory]` to build the firmware.
|
||||
>
|
||||
> Overwriting an existing board's configuration is dangerous because OTA updates may replace your custom firmware with the stock firmware for the original board. Every board must have a unique identity and its own firmware update channel.
|
||||
|
||||
## Directory Layout
|
||||
|
||||
A board directory typically contains:
|
||||
|
||||
- `xxx_board.cc` - board-level initialization and glue code.
|
||||
- `config.h` - pin assignments and board-level settings.
|
||||
- `config.json` - reported board type and release configuration consumed by CMake and `scripts/build.py`.
|
||||
- `README.md` - board-specific notes.
|
||||
|
||||
Boards can live directly under `main/boards/` or be grouped by manufacturer under `main/boards/<manufacturer>/<board>/` (see [Manufacturer Sub-directories](#manufacturer-sub-directories) below).
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Create the Board Directory
|
||||
|
||||
Create a new directory under `main/boards/` using the `[vendor]-[model]` naming style (e.g. `m5stack-tab5`):
|
||||
|
||||
```bash
|
||||
mkdir main/boards/my-custom-board
|
||||
```
|
||||
|
||||
### 2. Create the Configuration Files
|
||||
|
||||
#### config.h
|
||||
|
||||
Define all hardware settings in `config.h`:
|
||||
|
||||
- Audio sample rates and I2S pin mapping.
|
||||
- Audio codec I2C address and pins.
|
||||
- Button and LED pins.
|
||||
- Display parameters and pins.
|
||||
|
||||
Example (from `lichuang-c3-dev`):
|
||||
|
||||
```c
|
||||
#ifndef _BOARD_CONFIG_H_
|
||||
#define _BOARD_CONFIG_H_
|
||||
|
||||
#include <driver/gpio.h>
|
||||
|
||||
// Audio
|
||||
#define AUDIO_INPUT_SAMPLE_RATE 24000
|
||||
#define AUDIO_OUTPUT_SAMPLE_RATE 24000
|
||||
|
||||
#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_10
|
||||
#define AUDIO_I2S_GPIO_WS GPIO_NUM_12
|
||||
#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_8
|
||||
#define AUDIO_I2S_GPIO_DIN GPIO_NUM_7
|
||||
#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_11
|
||||
|
||||
#define AUDIO_CODEC_PA_PIN GPIO_NUM_13
|
||||
#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_0
|
||||
#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_1
|
||||
#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR
|
||||
|
||||
// Buttons
|
||||
#define BOOT_BUTTON_GPIO GPIO_NUM_9
|
||||
|
||||
// Display
|
||||
#define DISPLAY_SPI_SCK_PIN GPIO_NUM_3
|
||||
#define DISPLAY_SPI_MOSI_PIN GPIO_NUM_5
|
||||
#define DISPLAY_DC_PIN GPIO_NUM_6
|
||||
#define DISPLAY_SPI_CS_PIN GPIO_NUM_4
|
||||
|
||||
#define DISPLAY_WIDTH 320
|
||||
#define DISPLAY_HEIGHT 240
|
||||
#define DISPLAY_MIRROR_X true
|
||||
#define DISPLAY_MIRROR_Y false
|
||||
#define DISPLAY_SWAP_XY true
|
||||
|
||||
#define DISPLAY_OFFSET_X 0
|
||||
#define DISPLAY_OFFSET_Y 0
|
||||
|
||||
#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_2
|
||||
#define DISPLAY_BACKLIGHT_OUTPUT_INVERT true
|
||||
|
||||
#endif // _BOARD_CONFIG_H_
|
||||
```
|
||||
|
||||
#### config.json
|
||||
|
||||
`config.json` defines the compatibility-sensitive reported type and drives `scripts/build.py`:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "my-custom-board",
|
||||
"target": "esp32s3",
|
||||
"builds": [
|
||||
{
|
||||
"name": "my-custom-board",
|
||||
"sdkconfig_append": [
|
||||
"CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y",
|
||||
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/8m.csv\""
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Fields**:
|
||||
- `type`: compatibility-sensitive board family reported by the firmware. Keep it stable after release.
|
||||
- `target`: target chip, must match the real hardware (`esp32`, `esp32s3`, `esp32c3`, `esp32c6`, `esp32p4`, ...).
|
||||
- `name`: compatibility-sensitive firmware variant name reported by release builds; typically matches `type`.
|
||||
- `sdkconfig_append`: extra sdkconfig lines merged into the defaults.
|
||||
|
||||
Both `type` and `name` must contain only lowercase letters, digits, periods
|
||||
(`.`), and hyphens (`-`). Underscores, spaces, and uppercase letters are not
|
||||
allowed.
|
||||
|
||||
**Common `sdkconfig_append` entries**:
|
||||
|
||||
```json
|
||||
// Flash size
|
||||
"CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y"
|
||||
"CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y"
|
||||
|
||||
// Partition table
|
||||
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/4m.csv\""
|
||||
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/8m.csv\""
|
||||
|
||||
// Audio pipeline
|
||||
"CONFIG_USE_DEVICE_AEC=y" // enable on-device AEC
|
||||
```
|
||||
|
||||
The project defaults to 16MB flash and `partitions/v2/16m.csv` on applicable
|
||||
targets. Do not repeat values that already match the effective project and
|
||||
target defaults; use `sdkconfig_append` only for actual board-specific
|
||||
overrides.
|
||||
|
||||
Do not select a language or a specific wake word in a board `config.json`.
|
||||
Those are user build options and must be configured consistently through
|
||||
`menuconfig` or build-script parameters so CLI, agent, and online builds can
|
||||
share the same interface.
|
||||
|
||||
### 3. Implement the Board Class
|
||||
|
||||
Create `my_custom_board.cc` containing the board-level implementation.
|
||||
|
||||
A basic board class has:
|
||||
|
||||
1. **Class declaration**: derive from `WifiBoard` or `Ml307Board`.
|
||||
2. **Initialization helpers**: I2C, display, buttons, IoT/MCP tools, etc.
|
||||
3. **Virtual overrides**: `GetAudioCodec()`, `GetDisplay()`, `GetBacklight()`, ...
|
||||
4. **Board registration**: `DECLARE_BOARD(ClassName)`.
|
||||
|
||||
```cpp
|
||||
#include "wifi_board.h"
|
||||
#include "codecs/es8311_audio_codec.h"
|
||||
#include "display/lcd_display.h"
|
||||
#include "application.h"
|
||||
#include "button.h"
|
||||
#include "config.h"
|
||||
#include "mcp_server.h"
|
||||
|
||||
#include <esp_log.h>
|
||||
#include <driver/i2c_master.h>
|
||||
#include <driver/spi_common.h>
|
||||
|
||||
#define TAG "MyCustomBoard"
|
||||
|
||||
class MyCustomBoard : public WifiBoard {
|
||||
private:
|
||||
i2c_master_bus_handle_t codec_i2c_bus_;
|
||||
Button boot_button_;
|
||||
LcdDisplay* display_;
|
||||
|
||||
void InitializeI2c() {
|
||||
i2c_master_bus_config_t i2c_bus_cfg = {
|
||||
.i2c_port = I2C_NUM_0,
|
||||
.sda_io_num = AUDIO_CODEC_I2C_SDA_PIN,
|
||||
.scl_io_num = AUDIO_CODEC_I2C_SCL_PIN,
|
||||
.clk_source = I2C_CLK_SRC_DEFAULT,
|
||||
.glitch_ignore_cnt = 7,
|
||||
.intr_priority = 0,
|
||||
.trans_queue_depth = 0,
|
||||
.flags = {
|
||||
.enable_internal_pullup = 1,
|
||||
},
|
||||
};
|
||||
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &codec_i2c_bus_));
|
||||
}
|
||||
|
||||
void InitializeSpi() {
|
||||
spi_bus_config_t buscfg = {};
|
||||
buscfg.mosi_io_num = DISPLAY_SPI_MOSI_PIN;
|
||||
buscfg.miso_io_num = GPIO_NUM_NC;
|
||||
buscfg.sclk_io_num = DISPLAY_SPI_SCK_PIN;
|
||||
buscfg.quadwp_io_num = GPIO_NUM_NC;
|
||||
buscfg.quadhd_io_num = GPIO_NUM_NC;
|
||||
buscfg.max_transfer_sz = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t);
|
||||
ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO));
|
||||
}
|
||||
|
||||
void InitializeButtons() {
|
||||
boot_button_.OnClick([this]() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
app.ToggleChatState();
|
||||
});
|
||||
}
|
||||
|
||||
void InitializeDisplay() {
|
||||
esp_lcd_panel_io_handle_t panel_io = nullptr;
|
||||
esp_lcd_panel_handle_t panel = nullptr;
|
||||
|
||||
esp_lcd_panel_io_spi_config_t io_config = {};
|
||||
io_config.cs_gpio_num = DISPLAY_SPI_CS_PIN;
|
||||
io_config.dc_gpio_num = DISPLAY_DC_PIN;
|
||||
io_config.spi_mode = 2;
|
||||
io_config.pclk_hz = 80 * 1000 * 1000;
|
||||
io_config.trans_queue_depth = 10;
|
||||
io_config.lcd_cmd_bits = 8;
|
||||
io_config.lcd_param_bits = 8;
|
||||
ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI2_HOST, &io_config, &panel_io));
|
||||
|
||||
esp_lcd_panel_dev_config_t panel_config = {};
|
||||
panel_config.reset_gpio_num = GPIO_NUM_NC;
|
||||
panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB;
|
||||
panel_config.bits_per_pixel = 16;
|
||||
ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel));
|
||||
|
||||
esp_lcd_panel_reset(panel);
|
||||
esp_lcd_panel_init(panel);
|
||||
esp_lcd_panel_invert_color(panel, true);
|
||||
esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY);
|
||||
esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
|
||||
|
||||
display_ = new SpiLcdDisplay(panel_io, panel,
|
||||
DISPLAY_WIDTH, DISPLAY_HEIGHT,
|
||||
DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y,
|
||||
DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY);
|
||||
}
|
||||
|
||||
void InitializeTools() {
|
||||
// Register MCP tools here; see docs/mcp-usage.md.
|
||||
}
|
||||
|
||||
public:
|
||||
MyCustomBoard() : boot_button_(BOOT_BUTTON_GPIO) {
|
||||
InitializeI2c();
|
||||
InitializeSpi();
|
||||
InitializeDisplay();
|
||||
InitializeButtons();
|
||||
InitializeTools();
|
||||
GetBacklight()->SetBrightness(100);
|
||||
}
|
||||
|
||||
virtual AudioCodec* GetAudioCodec() override {
|
||||
static Es8311AudioCodec audio_codec(
|
||||
codec_i2c_bus_,
|
||||
I2C_NUM_0,
|
||||
AUDIO_INPUT_SAMPLE_RATE,
|
||||
AUDIO_OUTPUT_SAMPLE_RATE,
|
||||
AUDIO_I2S_GPIO_MCLK,
|
||||
AUDIO_I2S_GPIO_BCLK,
|
||||
AUDIO_I2S_GPIO_WS,
|
||||
AUDIO_I2S_GPIO_DOUT,
|
||||
AUDIO_I2S_GPIO_DIN,
|
||||
AUDIO_CODEC_PA_PIN,
|
||||
AUDIO_CODEC_ES8311_ADDR);
|
||||
return &audio_codec;
|
||||
}
|
||||
|
||||
virtual Display* GetDisplay() override {
|
||||
return display_;
|
||||
}
|
||||
|
||||
virtual Backlight* GetBacklight() override {
|
||||
static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT);
|
||||
return &backlight;
|
||||
}
|
||||
};
|
||||
|
||||
DECLARE_BOARD(MyCustomBoard);
|
||||
```
|
||||
|
||||
### 4. Hook Up the Build System
|
||||
|
||||
#### Add a Kconfig entry
|
||||
|
||||
In `main/Kconfig.projbuild`, add an entry to the `choice BOARD_TYPE` block:
|
||||
|
||||
```kconfig
|
||||
choice BOARD_TYPE
|
||||
prompt "Board Type"
|
||||
default BOARD_TYPE_BREAD_COMPACT_WIFI
|
||||
help
|
||||
Board type.
|
||||
|
||||
# ... other entries ...
|
||||
|
||||
config BOARD_TYPE_MY_CUSTOM_BOARD
|
||||
bool "My Custom Board"
|
||||
depends on IDF_TARGET_ESP32S3 # pick the matching target
|
||||
endchoice
|
||||
```
|
||||
|
||||
Notes:
|
||||
- The identifier must be uppercase and underscore-separated.
|
||||
- `depends on` restricts the entry to the correct target (`IDF_TARGET_ESP32S3`, `IDF_TARGET_ESP32C3`, ...).
|
||||
- The label can be localized.
|
||||
|
||||
#### Add a branch in CMakeLists.txt
|
||||
|
||||
Open `main/CMakeLists.txt` and extend the board-type chain:
|
||||
|
||||
```cmake
|
||||
elseif(CONFIG_BOARD_TYPE_MY_CUSTOM_BOARD)
|
||||
set(BOARD_DIR "my-custom-board")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) # pick a font for the display
|
||||
set(BUILTIN_ICON_FONT font_awesome_20_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64) // optional, for emoji display
|
||||
```
|
||||
|
||||
**Font and emoji guidance**:
|
||||
|
||||
Pick a font size that matches the display resolution:
|
||||
- Small (128x64 OLED): `font_puhui_basic_14_1` / `font_awesome_14_1`
|
||||
- Small-medium (240x240): `font_puhui_basic_16_4` / `font_awesome_16_4`
|
||||
- Medium (240x320): `font_puhui_basic_20_4` / `font_awesome_20_4`
|
||||
- Large (480x320+): `font_puhui_basic_30_4` / `font_awesome_30_4`
|
||||
|
||||
Emoji collections:
|
||||
- `twemoji_32` - 32x32 pixels (small screens).
|
||||
- `twemoji_64` - 64x64 pixels (large screens).
|
||||
|
||||
### 5. Build and Flash
|
||||
|
||||
#### Option A - use `idf.py` manually
|
||||
|
||||
1. Set the target chip (first time, or when switching targets):
|
||||
```bash
|
||||
idf.py set-target esp32s3 # ESP32-S3
|
||||
idf.py set-target esp32c3 # ESP32-C3
|
||||
idf.py set-target esp32 # ESP32
|
||||
```
|
||||
|
||||
2. Clean stale configuration:
|
||||
```bash
|
||||
idf.py fullclean
|
||||
```
|
||||
|
||||
3. Select the board via menuconfig:
|
||||
```bash
|
||||
idf.py menuconfig
|
||||
```
|
||||
Navigate to `Xiaozhi Assistant -> Board Type` and choose your board.
|
||||
|
||||
4. Build and flash:
|
||||
```bash
|
||||
idf.py build
|
||||
idf.py flash monitor
|
||||
```
|
||||
|
||||
#### Option B - use `build.py` (recommended)
|
||||
|
||||
If the board directory contains a `config.json`, you can configure and build it automatically:
|
||||
|
||||
```bash
|
||||
python scripts/build.py my-custom-board
|
||||
```
|
||||
|
||||
Language and wake-word selection are user build options:
|
||||
|
||||
```bash
|
||||
python scripts/build.py my-custom-board \
|
||||
--language en-US \
|
||||
--wake-word wn9_jarvis_tts
|
||||
```
|
||||
|
||||
`--language` accepts a locale listed under `main/assets/locales/`.
|
||||
`--wake-word` accepts an ESP-SR model name, `nihaoxiaozhi` (which selects the
|
||||
compatible model for the target), or `disabled`. ESP32-C3/C5/C6 targets support
|
||||
WakeNet9s (`wn9s_*`) models; ESP32-S3/P4/S31 builds automatically use the AFE
|
||||
wake-word engine.
|
||||
|
||||
Query the accepted values in text or machine-readable form:
|
||||
|
||||
```bash
|
||||
python scripts/build.py --list-languages
|
||||
python scripts/build.py --list-languages --json
|
||||
python scripts/build.py --list-wake-words
|
||||
python scripts/build.py --list-wake-words --json
|
||||
```
|
||||
|
||||
The wake-word list is read from the currently resolved ESP-SR component, so
|
||||
run `idf.py reconfigure` first if `managed_components/` has not been populated.
|
||||
|
||||
The script:
|
||||
- Prints help when run without arguments. Use `--list-boards` to list board
|
||||
types and variants.
|
||||
- Prompts for a variant when the selected board has multiple builds. In
|
||||
non-interactive environments, pass `--name <variant>`.
|
||||
- Reads `target` from `config.json`. It calls `idf.py set-target` only when the
|
||||
target changes, then regenerates `sdkconfig` from defaults and the selected
|
||||
build's `sdkconfig_append`.
|
||||
- Passes the selected build's `name` as the reported firmware variant name.
|
||||
- Builds `build/merged-binary.bin` without creating a ZIP by default. Pass
|
||||
`--zip` to recreate `releases/v<version>_<name>.zip`.
|
||||
|
||||
### 6. Write the README
|
||||
|
||||
In `README.md`, describe the board, hardware requirements, build instructions, and any special notes.
|
||||
|
||||
## Manufacturer Sub-directories
|
||||
|
||||
Boards can be grouped by manufacturer under `main/boards/<manufacturer>/<board>/`. This is the recommended layout when a single vendor ships several variants - for example `main/boards/waveshare/esp32-p4-nano/` or `main/boards/lceda-course-examples/eda-tv-pro/`.
|
||||
|
||||
For a board in a manufacturer sub-directory, add the same value to `config.json`, for example `"manufacturer": "waveshare"`. The firmware reports it as `board.manufacturer` together with `board.type` and `board.name`. Flat community boards without this field report an empty manufacturer string.
|
||||
|
||||
Set `BOARD_DIR` to the complete path relative to `main/boards/`:
|
||||
|
||||
```cmake
|
||||
elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_P4_NANO)
|
||||
set(BOARD_DIR "waveshare/esp32-p4-nano")
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_30_4)
|
||||
set(BUILTIN_ICON_FONT font_awesome_30_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
|
||||
```
|
||||
|
||||
The build system loads sources from `main/boards/${BOARD_DIR}/` and reads the reported board type from that directory's `config.json`. If `config.json` or its top-level `type` is absent, the full `BOARD_DIR` with `/` replaced by `-` is used as the fallback type.
|
||||
|
||||
Rules of thumb:
|
||||
- Use the manufacturer layout when you have two or more boards from the same vendor that share drivers, assets, or documentation.
|
||||
- Use the flat layout for one-off boards and community examples.
|
||||
- Directory names use lowercase with dashes (e.g. `waveshare`, `lceda-course-examples`).
|
||||
|
||||
## Common Board Components
|
||||
|
||||
Several reusable components live in `main/boards/common/`. You can include them directly from your board class:
|
||||
|
||||
### Display drivers
|
||||
|
||||
Supported LCD families include:
|
||||
- ST7789 (SPI)
|
||||
- ILI9341 (SPI)
|
||||
- SH8601 (QSPI)
|
||||
- and many more.
|
||||
|
||||
### Audio codecs
|
||||
|
||||
- `Es8311AudioCodec` (most common)
|
||||
- `Es8374AudioCodec`
|
||||
- `Es8388AudioCodec`
|
||||
- `Es8389AudioCodec`
|
||||
- `BoxAudioCodec` (ES7210 mic array + codec combo used on ESP-Box boards)
|
||||
- `NoAudioCodec` (direct I2S without external codec)
|
||||
- `DummyAudioCodec` (placeholder for boards without audio)
|
||||
|
||||
### Power management
|
||||
|
||||
- `Axp2101` power management IC helpers.
|
||||
- `Sy6970` battery charger helpers.
|
||||
- `AdcBatteryMonitor` - simple ADC-based battery voltage monitor.
|
||||
- `PowerSaveTimer` / `SleepTimer` - helpers for light-sleep scheduling.
|
||||
|
||||
### Networking
|
||||
|
||||
- `WifiBoard` - WiFi-only base class.
|
||||
- `Ml307Board` / `Nt26Board` - 4G modem base classes.
|
||||
- `DualNetworkBoard` - switchable WiFi / 4G base class.
|
||||
- `RndisBoard` - RNDIS-over-USB networking (ESP32-S3 / ESP32-P4).
|
||||
- `EspVideo` helpers for ESP-Video on ESP32-S3 / ESP32-P4.
|
||||
|
||||
### Input helpers
|
||||
|
||||
- `Button` - standard push buttons (click, long-press, multi-click).
|
||||
- `Knob` - rotary encoder wrapper.
|
||||
- `PressToTalkMcpTool` - push-to-talk tool that registers itself through MCP.
|
||||
- `SystemReset` - helper that performs a safe factory reset when a button is held at boot.
|
||||
|
||||
### MCP integration
|
||||
|
||||
Any board can register custom tools - speaker control, screen brightness, battery readout, light control, etc. See [MCP IoT control usage](./mcp-usage.md).
|
||||
|
||||
## Board Class Hierarchy
|
||||
|
||||
- `Board` - base class
|
||||
- `WifiBoard` - WiFi-connected board
|
||||
- `Ml307Board` / `Nt26Board` - 4G modem boards
|
||||
- `DualNetworkBoard` - WiFi + 4G switchable board
|
||||
- `RndisBoard` - RNDIS-over-USB board
|
||||
|
||||
## Tips
|
||||
|
||||
1. **Start from a similar board** - copying and tweaking an existing board is usually faster than starting from scratch.
|
||||
2. **Bring up incrementally** - get the display up first, then audio, then the full stack.
|
||||
3. **Double check pin assignments** - every pin defined in `config.h` must match your schematic.
|
||||
4. **Check hardware compatibility** - especially codec / PMIC / touch controller combinations.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
1. **Display looks wrong** - verify SPI configuration, mirroring, and color inversion.
|
||||
2. **No audio** - check I2S wiring, PA enable pin, and codec I2C address.
|
||||
3. **Cannot connect to WiFi** - re-check WiFi credentials and provisioning method.
|
||||
4. **Cannot reach the server** - verify the WebSocket / MQTT endpoint configuration.
|
||||
|
||||
## References
|
||||
|
||||
- ESP-IDF documentation: https://docs.espressif.com/projects/esp-idf/
|
||||
- LVGL documentation: https://docs.lvgl.io/
|
||||
- ESP-SR documentation: https://github.com/espressif/esp-sr
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
## 重要提示
|
||||
|
||||
> **警告**: 对于自定义开发板,当IO配置与原有开发板不同时,切勿直接覆盖原有开发板的配置编译固件。必须创建新的开发板类型,或者通过config.json文件中的builds配置不同的name和sdkconfig宏定义来区分。使用 `python scripts/release.py [开发板目录名字]` 来编译打包固件。
|
||||
> **警告**: 对于自定义开发板,当IO配置与原有开发板不同时,切勿直接覆盖原有开发板的配置编译固件。必须创建新的开发板类型,或者通过config.json文件中的builds配置不同的name和sdkconfig宏定义来区分。使用 `python scripts/build.py [开发板目录名字]` 来编译固件。
|
||||
>
|
||||
> 如果直接覆盖原有配置,将来OTA升级时,您的自定义固件可能会被原有开发板的标准固件覆盖,导致您的设备无法正常工作。每个开发板有唯一的标识和对应的固件升级通道,保持开发板标识的唯一性非常重要。
|
||||
|
||||
@@ -14,14 +14,14 @@
|
||||
|
||||
- `xxx_board.cc` - 主要的板级初始化代码,实现了板子相关的初始化和功能
|
||||
- `config.h` - 板级配置文件,定义了硬件管脚映射和其他配置项
|
||||
- `config.json` - 编译配置,指定目标芯片和特殊的编译选项
|
||||
- `config.json` - 上报开发板类型及发布配置,供 CMake 和 `scripts/build.py` 使用
|
||||
- `README.md` - 开发板相关的说明文档
|
||||
|
||||
## 定制开发板步骤
|
||||
|
||||
### 1. 创建新的开发板目录
|
||||
|
||||
首先在`boards/`目录下创建一个新的目录,例如`my-custom-board/`:
|
||||
首先在`boards/`目录下创建一个新的目录,命名方式应使用 `[品牌名]-[开发板类型]` 的形式,例如 `m5stack-tab5`:
|
||||
|
||||
```bash
|
||||
mkdir main/boards/my-custom-board
|
||||
@@ -87,24 +87,58 @@ mkdir main/boards/my-custom-board
|
||||
|
||||
#### config.json
|
||||
|
||||
在`config.json`中定义编译配置:
|
||||
在`config.json`中定义兼容性相关的上报类型和自动化编译配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"target": "esp32s3", // 目标芯片型号: esp32, esp32s3, esp32c3等
|
||||
"type": "my-custom-board", // 固件上报的开发板类型,发布后应保持稳定
|
||||
"target": "esp32s3", // 目标芯片型号: esp32, esp32s3, esp32c3, esp32c6, esp32p4等
|
||||
"builds": [
|
||||
{
|
||||
"name": "my-custom-board", // 开发板名称
|
||||
"name": "my-custom-board", // 开发板名称,用于生成固件包
|
||||
"sdkconfig_append": [
|
||||
// 额外需要的编译配置
|
||||
// 特别 Flash 大小配置
|
||||
"CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y",
|
||||
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v1/8m.csv\""
|
||||
// 特别分区表配置
|
||||
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/8m.csv\""
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**配置项说明:**
|
||||
- `manufacturer`: 厂商子目录名称;使用厂商目录时必须填写,并作为 `board.manufacturer` 上报。平铺社区板可以省略,上报为空字符串
|
||||
- `type`: 固件上报的开发板系列类型,发布后应保持稳定
|
||||
- `target`: 目标芯片型号,必须与硬件匹配
|
||||
- `name`: release 构建上报的固件变体名称,通常与 `type` 一致
|
||||
- `sdkconfig_append`: 额外的 sdkconfig 配置项数组,会追加到默认配置中
|
||||
|
||||
`type` 和 `name` 只能包含小写字母、数字、点(`.`)和连字符(`-`),
|
||||
不允许使用下划线、空格或大写字母。
|
||||
|
||||
**常用的 sdkconfig_append 配置:**
|
||||
```json
|
||||
// Flash 大小
|
||||
"CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y" // 4MB Flash
|
||||
"CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y" // 8MB Flash
|
||||
|
||||
// 分区表
|
||||
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/4m.csv\"" // 4MB 分区表
|
||||
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/8m.csv\"" // 8MB 分区表
|
||||
|
||||
// 音频处理
|
||||
"CONFIG_USE_DEVICE_AEC=y" // 启用设备端 AEC
|
||||
```
|
||||
|
||||
对于适用的目标芯片,项目默认使用 16MB Flash 和
|
||||
`partitions/v2/16m.csv`。如果板子配置与项目及目标芯片的有效默认值相同,
|
||||
不要在 `sdkconfig_append` 中重复填写;这里应只保留板子确实需要的覆盖项。
|
||||
|
||||
不要在板子的 `config.json` 中选择语言或具体唤醒词。这些属于用户构建选项,
|
||||
应统一通过 `menuconfig` 或构建脚本参数配置,方便 CLI、Agent 和在线编译接口
|
||||
共用同一套参数。
|
||||
|
||||
### 3. 编写板级初始化代码
|
||||
|
||||
创建一个`my_custom_board.cc`文件,实现开发板的所有初始化逻辑。
|
||||
@@ -131,10 +165,6 @@ mkdir main/boards/my-custom-board
|
||||
|
||||
#define TAG "MyCustomBoard"
|
||||
|
||||
// 声明字体
|
||||
LV_FONT_DECLARE(font_puhui_16_4);
|
||||
LV_FONT_DECLARE(font_awesome_16_4);
|
||||
|
||||
class MyCustomBoard : public WifiBoard {
|
||||
private:
|
||||
i2c_master_bus_handle_t codec_i2c_bus_;
|
||||
@@ -174,8 +204,9 @@ private:
|
||||
void InitializeButtons() {
|
||||
boot_button_.OnClick([this]() {
|
||||
auto& app = Application::GetInstance();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting && !WifiStation::GetInstance().IsConnected()) {
|
||||
ResetWifiConfiguration();
|
||||
if (app.GetDeviceState() == kDeviceStateStarting) {
|
||||
EnterWifiConfigMode();
|
||||
return;
|
||||
}
|
||||
app.ToggleChatState();
|
||||
});
|
||||
@@ -212,12 +243,7 @@ private:
|
||||
display_ = new SpiLcdDisplay(panel_io, panel,
|
||||
DISPLAY_WIDTH, DISPLAY_HEIGHT,
|
||||
DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y,
|
||||
DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY,
|
||||
{
|
||||
.text_font = &font_puhui_16_4,
|
||||
.icon_font = &font_awesome_16_4,
|
||||
.emoji_font = font_emoji_32_init(),
|
||||
});
|
||||
DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY);
|
||||
}
|
||||
|
||||
// MCP Tools 初始化
|
||||
@@ -269,7 +295,136 @@ public:
|
||||
DECLARE_BOARD(MyCustomBoard);
|
||||
```
|
||||
|
||||
### 4. 创建README.md
|
||||
### 4. 添加构建系统配置
|
||||
|
||||
#### 在 Kconfig.projbuild 中添加开发板选项
|
||||
|
||||
打开 `main/Kconfig.projbuild` 文件,在 `choice BOARD_TYPE` 部分添加新的开发板配置项:
|
||||
|
||||
```kconfig
|
||||
choice BOARD_TYPE
|
||||
prompt "Board Type"
|
||||
default BOARD_TYPE_BREAD_COMPACT_WIFI
|
||||
help
|
||||
Board type. 开发板类型
|
||||
|
||||
# ... 其他开发板选项 ...
|
||||
|
||||
config BOARD_TYPE_MY_CUSTOM_BOARD
|
||||
bool "My Custom Board (我的自定义开发板)"
|
||||
depends on IDF_TARGET_ESP32S3 # 根据你的目标芯片修改
|
||||
endchoice
|
||||
```
|
||||
|
||||
**注意事项:**
|
||||
- `BOARD_TYPE_MY_CUSTOM_BOARD` 是配置项名称,需要全大写,使用下划线分隔
|
||||
- `depends on` 指定了目标芯片类型(如 `IDF_TARGET_ESP32S3`、`IDF_TARGET_ESP32C3` 等)
|
||||
- 描述文字可以使用中英文
|
||||
|
||||
#### 在 CMakeLists.txt 中添加开发板配置
|
||||
|
||||
打开 `main/CMakeLists.txt` 文件,在开发板类型判断部分添加新的配置:
|
||||
|
||||
```cmake
|
||||
# 在 elseif 链中添加你的开发板配置
|
||||
elseif(CONFIG_BOARD_TYPE_MY_CUSTOM_BOARD)
|
||||
set(BOARD_DIR "my-custom-board") # 相对于 main/boards 的完整路径
|
||||
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) # 根据屏幕大小选择合适的字体
|
||||
set(BUILTIN_ICON_FONT font_awesome_20_4)
|
||||
set(DEFAULT_EMOJI_COLLECTION twemoji_64) # 可选,如果需要表情显示
|
||||
endif()
|
||||
```
|
||||
|
||||
**字体和表情配置说明:**
|
||||
|
||||
根据屏幕分辨率选择合适的字体大小:
|
||||
- 小屏幕(128x64 OLED):`font_puhui_basic_14_1` / `font_awesome_14_1`
|
||||
- 中小屏幕(240x240):`font_puhui_basic_16_4` / `font_awesome_16_4`
|
||||
- 中等屏幕(240x320):`font_puhui_basic_20_4` / `font_awesome_20_4`
|
||||
- 大屏幕(480x320+):`font_puhui_basic_30_4` / `font_awesome_30_4`
|
||||
|
||||
表情集合选项:
|
||||
- `twemoji_32` - 32x32 像素表情(小屏幕)
|
||||
- `twemoji_64` - 64x64 像素表情(大屏幕)
|
||||
|
||||
### 5. 配置和编译
|
||||
|
||||
#### 方法一:使用 idf.py 手动配置
|
||||
|
||||
1. **设置目标芯片**(首次配置或更换芯片时):
|
||||
```bash
|
||||
# 对于 ESP32-S3
|
||||
idf.py set-target esp32s3
|
||||
|
||||
# 对于 ESP32-C3
|
||||
idf.py set-target esp32c3
|
||||
|
||||
# 对于 ESP32
|
||||
idf.py set-target esp32
|
||||
```
|
||||
|
||||
2. **清理旧配置**:
|
||||
```bash
|
||||
idf.py fullclean
|
||||
```
|
||||
|
||||
3. **进入配置菜单**:
|
||||
```bash
|
||||
idf.py menuconfig
|
||||
```
|
||||
|
||||
在菜单中导航到:`Xiaozhi Assistant` -> `Board Type`,选择你的自定义开发板。
|
||||
|
||||
4. **编译和烧录**:
|
||||
```bash
|
||||
idf.py build
|
||||
idf.py flash monitor
|
||||
```
|
||||
|
||||
#### 方法二:使用 build.py 脚本(推荐)
|
||||
|
||||
如果你的开发板目录下有 `config.json` 文件,可以使用此脚本自动完成配置和编译:
|
||||
|
||||
```bash
|
||||
python scripts/build.py my-custom-board
|
||||
```
|
||||
|
||||
语言和唤醒词属于用户构建参数:
|
||||
|
||||
```bash
|
||||
python scripts/build.py my-custom-board \
|
||||
--language en-US \
|
||||
--wake-word wn9_jarvis_tts
|
||||
```
|
||||
|
||||
`--language` 接受 `main/assets/locales/` 下已有的 locale;`--wake-word`
|
||||
接受 ESP-SR 模型名、`nihaoxiaozhi`(自动选择与目标芯片兼容的模型)或
|
||||
`disabled`。ESP32-C3/C5/C6 仅支持 WakeNet9s(`wn9s_*`)模型;
|
||||
ESP32-S3/P4/S31 构建会自动使用 AFE 唤醒引擎。
|
||||
|
||||
可以用文本或 JSON 格式查询可用值:
|
||||
|
||||
```bash
|
||||
python scripts/build.py --list-languages
|
||||
python scripts/build.py --list-languages --json
|
||||
python scripts/build.py --list-wake-words
|
||||
python scripts/build.py --list-wake-words --json
|
||||
```
|
||||
|
||||
唤醒词列表从当前已解析的 ESP-SR 组件读取。如果尚未生成
|
||||
`managed_components/`,请先运行 `idf.py reconfigure`。
|
||||
|
||||
此脚本会自动:
|
||||
- 不传参数时打印帮助;使用 `--list-boards` 列出所有开发板类型和变体
|
||||
- 如果开发板有多个变体,交互式提示选择;非交互环境使用 `--name <变体>`
|
||||
- 读取 `config.json` 中的 `target`,仅在目标芯片变化时调用
|
||||
`idf.py set-target`,然后根据 defaults 和所选变体的
|
||||
`sdkconfig_append` 重新生成 `sdkconfig`
|
||||
- 将所选 build 的 `name` 作为固件上报的变体名称
|
||||
- 默认生成 `build/merged-binary.bin`,不创建 ZIP;指定 `--zip` 时会重新生成
|
||||
`releases/v<版本>_<名称>.zip`
|
||||
|
||||
### 6. 创建README.md
|
||||
|
||||
在README.md中说明开发板的特性、硬件要求、编译和烧录步骤:
|
||||
|
||||
@@ -332,4 +487,4 @@ DECLARE_BOARD(MyCustomBoard);
|
||||
|
||||
- ESP-IDF 文档: https://docs.espressif.com/projects/esp-idf/
|
||||
- LVGL 文档: https://docs.lvgl.io/
|
||||
- ESP-SR 文档: https://github.com/espressif/esp-sr
|
||||
- ESP-SR 文档: https://github.com/espressif/esp-sr
|
||||
@@ -0,0 +1,257 @@
|
||||
# ESP-IDF 6.0 Migration and Board Compatibility Status
|
||||
|
||||
> Last updated: 2026-07-24
|
||||
> Validated SDK: ESP-IDF v6.0.1
|
||||
> Scope: 138 board directories and 171 supported build variants defined by `main/boards/**/config.json`.
|
||||
|
||||
## Current Status
|
||||
|
||||
The current release matrix contains 171 variants: 170 select on IDF 6.0.x, while the ESP32-S31 variant requires IDF 6.1 or later. The previous 157-variant baseline passed the latest complete GitHub Actions matrix on ESP-IDF 6.0.1. ESP-SR 2.4.7 adds IDF 6 support for ESP32-P4 Rev < 3, so the 14 unsuffixed Rev1 variants now join their existing `-p4x` Rev3 counterparts in the IDF 6 matrix. Component versions that use ranges are resolved from the Component Registry at build time; until per-target lock snapshots are committed, this is a source-reproducible build rather than a bit-for-bit dependency-reproducible build.
|
||||
|
||||
| Status | Variants | Meaning |
|
||||
|---|---:|---|
|
||||
| ✅ Previously matrix-validated | 157 | All 157 baseline builds completed in GitHub Actions with ESP-IDF 6.0.1; this does not imply hardware or complete peripheral validation |
|
||||
| 🟡 Net variants added since full-matrix validation | 14 | Includes the 14 newly enabled P4 Rev1 variants and the IDF 6.1-only ESP32-S31 variant, after retired low-use variants were removed; a complete current-matrix CI run remains pending |
|
||||
| 🟡 Feature-degraded subset | 1 | `esp-vocat` builds on IDF 6, but its PCB capacitive slider/button support is disabled pending compatible touch-sensor components |
|
||||
| 🔴 Build-blocked | 0 | No supported release variant remains blocked at compile or link time |
|
||||
|
||||
There are no known remaining IDF 6 build blockers. [`78/esp_lcd_nv3023 1.0.1`](https://components.espressif.com/components/78/esp_lcd_nv3023/versions/1.0.1) and [`wvirgil123/sscma_client 1.0.3`](https://components.espressif.com/components/wvirgil123/sscma_client/versions/1.0.3/readme) are consumed directly from the Component Registry, so local copies under the ignored `components/` directory are not required. ESP32-P4 Rev1 and Rev3 now share the IDF 6 build path while retaining separate artifact names and silicon-selection settings.
|
||||
|
||||
Representative local full-build results are shown below. Firmware size and free space are reported by ESP-IDF 6.0.1 `check_sizes.py`. The authoritative per-variant compatibility result is the full GitHub Actions matrix in the next section.
|
||||
|
||||
| Chip | Build variant | Application size | Free space in smallest app partition |
|
||||
|---|---|---:|---:|
|
||||
| ESP32 | `bread-compact-esp32` | `0x240560` | 23% |
|
||||
| ESP32-C3 | `espressif-esp-hi` | `0x20ef10` | 30% |
|
||||
| ESP32-C3 | `xmini-c3` | `0x2370f0` | 44% |
|
||||
| ESP32-C5 | `waveshare-esp32-c5-touch-lcd-1.69` | `0x27cd30` | 37% |
|
||||
| ESP32-C6 | `waveshare-esp32-c6-lcd-0.85` | `0x280ee0` | 15% |
|
||||
| ESP32-S3 | `doit-s3-aibox` | `0x21edf0` | 46% |
|
||||
| ESP32-S3 | `lilygo-t-display-s3-pro-mvsrlora` | `0x2adce0` | 32% |
|
||||
| ESP32-S3 | `lilygo-t-circle-s3` | `0x2a32f0` | 33% |
|
||||
| ESP32-S3 | `zhengchen-1.54tft-wifi` | `0x2abdc0` | 32% |
|
||||
| ESP32-S3 | `kevin-yuying-313lcd` | `0x2c4990` | 30% |
|
||||
| ESP32-S3 | `otto-robot` | `0x37ff70` | 11% |
|
||||
| ESP32-S3 | `espressif-esp-vocat` | `0x2742f0` | 38% |
|
||||
| ESP32-S3 | `sensecap-watcher` | `0x2faff0` | 25% |
|
||||
| ESP32-P4 Rev1 | `espressif-esp32-p4-function-ev-board` | `0x345a90` | 17% |
|
||||
| ESP32-P4 Rev3 | `espressif-esp32-p4x-function-ev-board` | `0x34a150` | 16% |
|
||||
| ESP32-P4 v3.x | `m5stack-tab5-p4x` | `0x380790` | 11% |
|
||||
|
||||
The old negative result for `esp32-p4-function-ev-board` came from ESP-SR 2.4.6. ESP-SR 2.4.7 supplies the missing ESP32-P4 Rev < 3 libraries for IDF 6, so the project no longer version-gates the P4 variants.
|
||||
|
||||
For backward-compatibility regression coverage, `xmini-c3` also completed a full build with ESP-IDF 5.5.4 (application size `0x234920`, 44% free in the smallest app partition). The legacy `esp32-p4-function-ev-board` release variant subsequently completed the same 5.5.4 release flow (application size `0x38a130`, 10% free), including merged-binary packaging. This confirms that the compatibility changes for I2S port numbering, LCD I2C configuration, the UHCI DMA dependency, and the pre-v3 P4 selection did not break the existing 5.5 build chain.
|
||||
|
||||
## Full-Matrix CI Validation
|
||||
|
||||
GitHub Actions run [29534954031](https://github.com/78/xiaozhi-esp32/actions/runs/29534954031) built the then-current matrix with the `espressif/idf:v6.0.1` container. The matrix-generation job and all 157 board builds passed. This historical run predates the current 170-variant IDF 6.0.x matrix.
|
||||
|
||||
Results by chip target:
|
||||
|
||||
| Chip target | Variants | Passed | Blocked |
|
||||
|---|---:|---:|---:|
|
||||
| ESP32 | 7 | 7 | 0 |
|
||||
| ESP32-C3 | 9 | 9 | 0 |
|
||||
| ESP32-C5 | 4 | 4 | 0 |
|
||||
| ESP32-C6 | 9 | 9 | 0 |
|
||||
| ESP32-S3 | 114 | 114 | 0 |
|
||||
| ESP32-P4 v3.x | 14 | 14 | 0 |
|
||||
| **Total** | **157** | **157** | **0** |
|
||||
|
||||
`esp-vocat` remains feature-degraded because IDF 6 builds omit the PCB capacitive slider/button path; its CST816 display touch remains enabled.
|
||||
|
||||
## ESP32-P4 Silicon Scope and Naming
|
||||
|
||||
The IDF 6 release matrix supports both ESP32-P4 Rev < 3 and Rev >= 3 silicon. The two silicon families keep distinct artifact names and sdkconfig settings:
|
||||
|
||||
| SDK | ESP32-P4 Rev < 3 | ESP32-P4 Rev >= 3 |
|
||||
|---|---|---|
|
||||
| ESP-IDF < 6 | Original `esp32-p4-` name; adds `CONFIG_ESP32P4_SELECTS_REV_LESS_V3=y` and `CONFIG_ESP32P4_REV_MIN_100=y` | Uses `esp32-p4x-` when the variant starts with the chip name; otherwise retains the established `-p4x` suffix |
|
||||
| ESP-IDF >= 6 | Original `esp32-p4-` name; adds `CONFIG_ESP32P4_SELECTS_REV_LESS_V3=y` and `CONFIG_ESP32P4_REV_MIN_100=y` | Uses `esp32-p4x-` when the variant starts with the chip name; otherwise retains the established `-p4x` suffix |
|
||||
|
||||
ESP-IDF 5.5 and 6.0.x each select 170 variants: 14 P4X artifacts, 14 Rev < 3 P4 artifacts with their original names, and 142 non-P4 variants. ESP-IDF 6.1 or later additionally selects the ESP32-S31 build, for 171 variants in total.
|
||||
|
||||
Espressif's current chip-identification table lists v0.0, v1.0, v1.3, v3.0, v3.1, and v3.2, with no v4.0 revision. The public errata history added v3.0/v3.1 information on 2026-02-12 and v3.2 information on 2026-04-20. ESP-IDF 6.0 release notes explicitly describe support for "ESP32-P4 Version3 silicon." See the official [chip revision identification](https://docs.espressif.com/projects/esp-chip-errata/en/latest/esp32p4/01-chip-identification/index.html), [errata revision history](https://docs.espressif.com/projects/esp-chip-errata/en/latest/esp32p4/revision-history/index.html), and [ESP-IDF releases](https://github.com/espressif/esp-idf/releases).
|
||||
|
||||
## Component Compatibility
|
||||
|
||||
| Component/module | Version or approach for IDF 6 | Status | Notes |
|
||||
|---|---|---|---|
|
||||
| `78/uart-uhci` | `0.3.2` | ✅ Upstream support | The registry release compiles under IDF 6.0.1 in GitHub Actions |
|
||||
| `78/uart-eth-modem` | `0.6.0` | ✅ Upstream support | Pinned because it has the required RF-test event API while supporting both ESP-IDF 5.5.2+ and 6.0.1; version 0.6.1 requires IDF 6.0.1+ |
|
||||
| `espressif/mqtt` | `1.0.0` | ✅ Upstream support | MQTT moved from a built-in SDK component to a Component Manager dependency in IDF 6 |
|
||||
| `78/esp-ml307` | `3.6.6` + project-level `espressif/mqtt` | ✅ Builds in CI | The upstream source compiles under IDF 6.0.1 when the project supplies the MQTT component moved out of IDF |
|
||||
| `espressif/esp_hosted` / `esp_wifi_remote` | `2.12.11` / `1.6.2` | ✅ Upstream support | Used for ESP32-P4 Hosted Wi-Fi |
|
||||
| `espressif/esp_video` | `^2.0.1` | ✅ Upstream support | Currently resolves to 2.3.0 on S3 and 2.0.1 on P4 due to BSP constraints |
|
||||
| P4 BSP / LCD drivers | BSP `5.2.3`, EK79007/ST7701 `2.0.x` | ✅ Upstream support | Resolves IDF 6 DPI, color-field, and split-driver-component issues |
|
||||
| `espressif/esp_lcd_st77916` | `2.0.2` | ✅ Upstream support | Major version 2 uses the IDF 6 panel I/O definitions |
|
||||
| `espressif/esp_lcd_spd2010` | `^2.0.0` (resolved `2.0.0~1`) | ✅ Upstream support | Major version 2 declares ESP-IDF 6 compatibility |
|
||||
| `espressif/esp_lcd_co5300` | `2.1.0` | ✅ Upstream support | Used with explicit IDF 6-compatible QSPI I/O configuration in board code |
|
||||
| `esp_emote_expression` | `1.0.2` | ✅ Upstream support | Replaces the previous dependency on the built-in `json` component |
|
||||
| `wvirgil123/sscma_client` | [`1.0.3`](https://components.espressif.com/components/wvirgil123/sscma_client/versions/1.0.3/readme) | ✅ Upstream support | Uses `espressif/cjson` and the split driver components on IDF 6 while retaining the legacy component names on older IDF releases |
|
||||
| `espressif/servo` / `espfriends/servo_dog_ctrl` | `1.0.0` / `0.2.0` | ✅ Upstream support | Uses official registry releases; compilation and linking were validated in the full IDF 6.0.1 build of `esp-hi`, with no local override required |
|
||||
| `78/esp_lcd_nv3023` | [`1.0.1`](https://components.espressif.com/components/78/esp_lcd_nv3023/versions/1.0.1) | ✅ Upstream-based registry release | Mirrors MakerM0 upstream commit `15dae953`; adds IDF 6 color-order and GPIO compatibility while retaining older IDF branches; validated through the registry in a clean `magiclick-c3` full build |
|
||||
| `llgok/cpp_bus_driver` | Excluded from the IDF 6 baseline | ⚪ Waiting for upstream | Version `2.1.0` still requires local patches for IDF 6. It is used only by the `LILYGO T-Display-P4` source, and that board has no release `config.json`, so the component has been removed until upstream support is available |
|
||||
| MQTT protocol AES-CTR | PSA Crypto | ✅ Ported | Replaces the legacy AES context API removed by IDF 6 / Mbed TLS 4 |
|
||||
| BluFi security negotiation (conditional path) | PSA FFDH + SHA-256 + AES-CTR | ✅ Ported | Uses the ESP-IDF 6 security scheme with ffdhe3072 and passed local full ESP32-S3 builds with BluFi enabled on IDF 5.5.4 and 6.0.1. Legacy 1024-bit BluFi clients are not compatible and must be upgraded |
|
||||
| `espressif/bmi270_sensor` | [`0.1.2`](https://components.espressif.com/components/espressif/bmi270_sensor/versions/0.1.2/readme?language=en) | ✅ Upstream support | Provides IDF 6.0 prebuilt libraries for ESP32-C5 and ESP32-S3; validated by a full `esp-spot-c5` build |
|
||||
| `touch_slider_sensor` / `touch_button_sensor` | Disabled for IDF 6 | 🟡 Feature gap | Their manifests require IDF < 6.0, so ESP Vocat omits its PCB capacitive slider/button path on IDF 6. CST816 display touch is unaffected |
|
||||
| ESP32-P4 Rev < 3 / `espressif/esp-sr` | `~2.4.7` | ✅ Upstream support | The 14 Rev1 variants are available on IDF 5.5 and IDF 6; P4X-specific names continue to select Rev >= 3 |
|
||||
|
||||
## Per-Board Progress
|
||||
|
||||
In the table below, "Board" is the source directory and "Build variant" is the firmware name used by the build script. Different chip revisions of the same board may have different status, so they are split into separate rows when necessary.
|
||||
|
||||
| Chip | Board | Build variant | IDF 6.0 status | Current validation | Blocker/next step |
|
||||
|---|---|---|---|---|---|
|
||||
| `esp32` | `atommatrix-echo-base` | `atommatrix-echo-base` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32` | `bread-compact-esp32` | `bread-compact-esp32` | ✅ Full build passed | IDF 6.0.1 `idf.py build` | Hardware smoke/peripheral regression pending |
|
||||
| `esp32` | `bread-compact-esp32` | `bread-compact-esp32-128x32` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32` | `bread-compact-esp32-lcd` | `bread-compact-esp32-lcd` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32` | `wdmomo/esp32-cgc` | `wdmomo-esp32-cgc` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32` | `wdmomo/esp32-cgc-144` | `wdmomo-esp32-cgc-144` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32` | `waveshare/esp32-touch-lcd-3.5` | `waveshare-esp32-touch-lcd-3.5` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32c3` | `espressif/esp-hi` | `espressif-esp-hi` | ✅ Full build passed | IDF 6.0.1 `idf.py build`; `servo_dog_ctrl 0.2.0` | Hardware smoke, servo, and audio peripheral regression pending |
|
||||
| `esp32c3` | `kevin/c3` | `kevin-c3` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32c3` | `lckfb/szpi-esp32c3` | `lckfb-lichuang-c3-dev` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32c3` | `magiclick-c3` | `magiclick-c3` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32c3` | `magiclick-c3-v2` | `magiclick-c3-v2` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32c3` | `surfer-c3-1.14tft` | `surfer-c3-1.14tft` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32c3` | `xmini-c3` | `xmini-c3` | ✅ Full build passed | IDF 6.0.1 `idf.py build` | Hardware smoke/peripheral regression pending |
|
||||
| `esp32c3` | `xmini-c3-4g` | `xmini-c3-4g` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32c3` | `xmini-c3-v3` | `xmini-c3-v3` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32c5` | `espressif/esp-sensairshuttle` | `espressif-esp-sensairshuttle` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32c5` | `espressif/esp-spot` | `espressif-esp-spot-c5` | ✅ Full build passed | IDF 6.0.1 `idf.py build`; `bmi270_sensor 0.1.2` | Hardware smoke, BMI270, audio, and power-management regression pending |
|
||||
| `esp32c5` | `movecall-moji2-esp32c5` | `movecall-moji2-esp32c5` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32c5` | `waveshare/esp32-c5-touch-lcd-1.69` | `waveshare-esp32-c5-touch-lcd-1.69` | ✅ Full build passed | IDF 6.0.1 `idf.py build` | Hardware smoke/peripheral regression pending |
|
||||
| `esp32c6` | `waveshare/esp32-c6-lcd-0.85` | `waveshare-esp32-c6-lcd-0.85` | ✅ Full build passed | IDF 6.0.1 `idf.py build` | Hardware smoke/peripheral regression pending |
|
||||
| `esp32c6` | `waveshare/esp32-c6-lcd-1.69` | `waveshare-esp32-c6-lcd-1.69` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32c6` | `waveshare/esp32-c6-touch-amoled-1.32` | `waveshare-esp32-c6-touch-amoled-1.32` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32c6` | `waveshare/esp32-c6-touch-amoled-1.43` | `waveshare-esp32-c6-touch-amoled-1.43` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32c6` | `waveshare/esp32-c6-touch-amoled-1.8` | `waveshare-esp32-c6-touch-amoled-1.8` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32c6` | `waveshare/esp32-c6-touch-amoled-2.06` | `waveshare-esp32-c6-touch-amoled-2.06` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32c6` | `waveshare/esp32-c6-touch-amoled-2.16` | `waveshare-esp32-c6-touch-amoled-2.16` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32c6` | `waveshare/esp32-c6-touch-lcd-1.54` | `waveshare-esp32-c6-touch-lcd-1.54` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32c6` | `waveshare/esp32-c6-touch-lcd-1.83` | `waveshare-esp32-c6-touch-lcd-1.83` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `xorigin/aipi-lite` | `xorigin-aipi-lite` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `alientek/atk-dnesp32s3` | `alientek-atk-dnesp32s3` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `alientek/atk-dnesp32s3-box` | `alientek-atk-dnesp32s3-box` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `alientek/atk-dnesp32s3-box0` | `alientek-atk-dnesp32s3-box0` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `alientek/atk-dnesp32s3-box2-4g` | `alientek-atk-dnesp32s3-box2-4g` | ✅ Build and hardware validated | GitHub Actions IDF 6.0.1 full build; maintainer hardware validation | Complete for the IDF 6 migration scope |
|
||||
| `esp32s3` | `alientek/atk-dnesp32s3-box2-wifi` | `alientek-atk-dnesp32s3-box2-wifi` | ✅ Build and hardware validated | GitHub Actions IDF 6.0.1 full build; maintainer hardware validation | Complete for the IDF 6 migration scope |
|
||||
| `esp32s3` | `alientek/atk-dnesp32s3-box3` | `alientek-atk-dnesp32s3-box3` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `atom-echos3r` | `atom-echos3r` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `atoms3-echo-base` | `atoms3-echo-base` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `atoms3r-cam-m12-echo-base` | `atoms3r-cam-m12-echo-base` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `atoms3r-echo-base` | `atoms3r-echo-base` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `atoms3r-echo-pyramid` | `atoms3r-echo-pyramid` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `bread-compact-ml307` | `bread-compact-ml307`<br>`bread-compact-ml307-128x64` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `bread-compact-nt26` | `bread-compact-nt26` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `bread-compact-wifi` | `bread-compact-wifi`<br>`bread-compact-wifi-128x64` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `df-k10` | `df-k10` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `df-s3-ai-cam` | `df-s3-ai-cam` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `doit-s3-aibox` | `doit-s3-aibox` | ✅ Full build passed | IDF 6.0.1 `idf.py build` | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `du-chatx` | `du-chatx` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `electron-bot` | `electron-bot` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `espressif/esp32-s3-box` | `espressif-esp-box` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `espressif/esp32-s3-box-3` | `espressif-esp-box-3` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `espressif/esp32-s3-box-lite` | `espressif-esp-box-lite` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `espressif/esp32-s3-lcd-ev-board` | `espressif-esp-s3-lcd-ev-board-1p4`<br>`espressif-esp-s3-lcd-ev-board-1p5` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `espressif/esp32-s3-lcd-ev-board-2` | `espressif-esp32-s3-lcd-ev-board-2` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `espressif/esp-sparkbot` | `espressif-esp-sparkbot` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `espressif/esp-vocat` | `espressif-esp-vocat` | 🟡 Full build passed; feature-degraded | Local ESP-IDF 6.0.1 full build after rebasing onto `origin/main` | PCB capacitive slider/button disabled on IDF 6; CST816 display touch remains enabled; hardware regression pending |
|
||||
| `esp32s3` | `espressif/esp32-s3-korvo-2-v3.0` | `espressif-esp32s3-korvo2-v3` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `espressif/esp32-s3-korvo-2-v3.0-rndis` | `espressif-esp32s3-korvo2-v3-rndis` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `freenove-esp32s3-display-2.8-lcd` | `freenove-esp32s3-display-2.8-lcd` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `genjutech-s3-1.54tft` | `genjutech-s3-1.54tft` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `hu-087` | `hu-087` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `jiuchuan-s3` | `jiuchuan-s3` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `kevin/box-2` | `kevin-box-2` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `kevin/sp-v4-dev` | `kevin-sp-v4-dev` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `kevin/yuying-313lcd` | `kevin-yuying-313lcd` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `labplus/ledong-v2` | `labplus-ledong-v2` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `labplus/mpython-v3` | `labplus-mpython-v3` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `lceda-course-examples/eda-robot-pro` | `lceda-course-examples-eda-robot-pro` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `lceda-course-examples/eda-super-bear` | `lceda-course-examples-eda-super-bear` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `lceda-course-examples/eda-tv-pro` | `lceda-course-examples-eda-tv-pro` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `lckfb/szpi-esp32s3` | `lckfb-lichuang-dev` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `lilygo-t-cameraplus-s3` | `lilygo-t-cameraplus-s3`<br>`lilygo-t-cameraplus-s3-v1.2` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `lilygo-t-circle-s3` | `lilygo-t-circle-s3` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `lilygo-t-display-s3-pro-mvsrlora` | `lilygo-t-display-s3-pro-mvsrlora` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `m5stack-cardputer-adv` | `m5stack-cardputer-adv` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `m5stack-core-s3` | `m5stack-core-s3` | ✅ Build and hardware validated | GitHub Actions IDF 6.0.1 full build; maintainer hardware validation | Complete for the IDF 6 migration scope |
|
||||
| `esp32s3` | `m5stack-stick-s3` | `m5stack-stick-s3` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `m5stack-stopwatch` | `m5stack-stopwatch` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `magiclick-2p4` | `magiclick-2p4` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `magiclick-2p5` | `magiclick-2p5` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `minsi-k08-dual` | `minsi-k08-dual` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `mixgo-nova` | `mixgo-nova` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `movecall-cuican-esp32s3` | `movecall-cuican-esp32s3` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `movecall-moji-esp32s3` | `movecall-moji-esp32s3` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `nulllab-ai-vox-v3` | `nulllab-ai-vox-v3` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `otto-robot` | `otto-robot` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `quandong-s3-dev` | `quandong-s3-dev` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `rymcu/bigsmart` | `rymcu-bigsmart` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `sensecap-watcher` | `sensecap-watcher` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `sp-esp32-s3-1.28-box` | `sp-esp32-s3-1.28-box` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `sp-esp32-s3-1.54-muma` | `sp-esp32-s3-1.54-muma` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `taiji-pi-s3` | `taiji-pi-s3`<br>`taiji-pi-s3-pdm` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `waveshare/esp32-s3-audio-board` | `waveshare-esp32-s3-audio-board` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `waveshare/esp32-s3-cam` | `waveshare-esp32-s3-cam-2`<br>`waveshare-esp32-s3-cam-2.8`<br>`waveshare-esp32-s3-cam-3.5`<br>`waveshare-esp32-s3-cam-1.83` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `waveshare/esp32-s3-epaper-1.54` | `waveshare-esp32-s3-epaper-1.54-v2`<br>`waveshare-esp32-s3-epaper-1.54-v1` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `waveshare/esp32-s3-epaper-3.97` | `waveshare-esp32-s3-epaper-3.97` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `waveshare/esp32-s3-lcd-0.85` | `waveshare-esp32-s3-lcd-0.85` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `waveshare/esp32-s3-rgb-matrix` | `waveshare-esp32-s3-rgb-matrix` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `waveshare/esp32-s3-rlcd-4.2` | `waveshare-esp32-s3-rlcd-4.2` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `waveshare/esp32-s3-touch-amoled-1.32` | `waveshare-esp32-s3-touch-amoled-1.32` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `waveshare/esp32-s3-touch-amoled-1.43c` | `waveshare-esp32-s3-touch-amoled-1.43c` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `waveshare/esp32-s3-touch-amoled-1.75` | `waveshare-esp32-s3-touch-amoled-1.75`<br>`waveshare-esp32-s3-touch-amoled-1.75c` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `waveshare/esp32-s3-touch-amoled-1.8` | `waveshare-esp32-s3-touch-amoled-1.8` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `waveshare/esp32-s3-touch-amoled-1.8-v2` | `waveshare-esp32-s3-touch-amoled-1.8-v2` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `waveshare/esp32-s3-touch-amoled-2.06` | `waveshare-esp32-s3-touch-amoled-2.06` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `waveshare/esp32-s3-touch-amoled-2.16` | `waveshare-esp32-s3-touch-amoled-2.16` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `waveshare/esp32-s3-touch-lcd-1.46` | `waveshare-esp32-s3-touch-lcd-1.46` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `waveshare/esp32-s3-touch-lcd-1.54` | `waveshare-esp32-s3-touch-lcd-1.54` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `waveshare/esp32-s3-touch-lcd-1.83` | `waveshare-esp32-s3-touch-lcd-1.83` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `waveshare/esp32-s3-touch-lcd-1.85` | `waveshare-esp32-s3-touch-lcd-1.85` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `waveshare/esp32-s3-touch-lcd-1.85b` | `waveshare-esp32-s3-touch-lcd-1.85b` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `waveshare/esp32-s3-touch-lcd-1.85c` | `waveshare-esp32-s3-touch-lcd-1.85c` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `waveshare/esp32-s3-touch-lcd-3.49` | `waveshare-esp32-s3-touch-lcd-3.49` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `waveshare/esp32-s3-touch-lcd-3.5` | `waveshare-esp32-s3-touch-lcd-3.5` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `waveshare/esp32-s3-touch-lcd-3.5b` | `waveshare-esp32-s3-touch-lcd-3.5b` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `waveshare/esp32-s3-touch-lcd-4.3c` | `waveshare-esp32-s3-touch-lcd-4.3c` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `waveshare/esp32-s3-touch-lcd-4b` | `waveshare-esp32-s3-touch-lcd-4b` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `waveshare/esp32-s3-touch-lcd-7c` | `waveshare-esp32-s3-touch-lcd-7c` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `xingzhi-abs-2.0` | `xingzhi-abs-2.0` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `xingzhi-cube-0.85tft-ml307` | `xingzhi-cube-0.85tft-ml307` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `xingzhi-cube-0.85tft-wifi` | `xingzhi-cube-0.85tft-wifi` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `xingzhi-cube-0.96oled-ml307` | `xingzhi-cube-0.96oled-ml307` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `xingzhi-cube-0.96oled-wifi` | `xingzhi-cube-0.96oled-wifi` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `xingzhi-cube-1.54tft-ml307` | `xingzhi-cube-1.54tft-ml307`<br>`xingzhi-cube-1.54tft-ml307-wechatui` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `xingzhi-cube-1.54tft-wifi` | `xingzhi-cube-1.54tft-wifi` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `xingzhi-metal-1.54-wifi` | `xingzhi-metal-1.54-wifi` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `yunliao-s3` | `yunliao-s3` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `zhengchen-1.54tft-ml307` | `zhengchen-1.54tft-ml307` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `zhengchen-1.54tft-wifi` | `zhengchen-1.54tft-wifi` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `zhengchen-cam` | `zhengchen-cam` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32s3` | `zhengchen-cam-ml307` | `zhengchen-cam-ml307` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32p4` | `espressif/esp32-p4-function-ev-board` | `espressif-esp32-p4-function-ev-board`<br>`espressif-esp32-p4x-function-ev-board` | ✅ Full builds passed | IDF 6.0.1 canonical release builds with ESP-SR 2.4.7 for Rev1 and Rev3 | Hardware smoke/peripheral regression pending |
|
||||
| `esp32p4` | `m5stack-tab5` | `m5stack-tab5-p4x` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32p4` | `waveshare/esp32-p4-nano` | `waveshare-esp32-p4x-nano-10.1-a` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32p4` | `waveshare/esp32-p4-wifi6-touch-lcd` | `waveshare-esp32-p4x-wifi6-touch-lcd-4b`<br>`waveshare-esp32-p4x-wifi6-touch-lcd-4.3`<br>`waveshare-esp32-p4x-wifi6-touch-lcd-5`<br>`waveshare-esp32-p4x-wifi6-touch-lcd-7b`<br>`waveshare-esp32-p4x-wifi6-touch-lcd-3.4c`<br>`waveshare-esp32-p4x-wifi6-touch-lcd-4c`<br>`waveshare-esp32-p4x-wifi6-touch-lcd-7`<br>`waveshare-esp32-p4x-wifi6-touch-lcd-8`<br>`waveshare-esp32-p4x-wifi6-touch-lcd-10.1` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
| `esp32p4` | `waveshare/esp32-p4-wifi6-touch-lcd-3.5` | `waveshare-esp32-p4x-wifi6-touch-lcd-3.5` | ✅ Full build passed | GitHub Actions IDF 6.0.1 full build | Hardware smoke/peripheral regression pending |
|
||||
## Next Steps and Acceptance Criteria
|
||||
|
||||
1. Run the IDF 6 BluFi CI job, then hardware-test at least one P4 v1.3 device on the legacy SDK path. When IDF 6-compatible releases of `touch_slider_sensor` and `touch_button_sensor` become available, re-enable and hardware-test the ESP Vocat PCB capacitive slider/button path.
|
||||
2. For every green variant, complete a minimal hardware smoke test covering boot, networking, audio input/output, display/touch when present, camera when present, and 4G/Ethernet when present.
|
||||
3. Perform a physical negative test for the `xmini-c3`/`xmini-c3-v3` firmware guard. CI proves that both images compile; it does not prove that a wrong image is safely rejected. Acceptance requires flashing each wrong image to a sacrificial or recoverable board and verifying that startup stops before any board-specific power or peripheral initialization can cause damage.
|
||||
4. Keep third-party experiments under the ignored `components/` directory out of the migration branch. The reproducible baseline must use the published `78/esp_lcd_nv3023 1.0.1` and `wvirgil123/sscma_client 1.0.3` packages. `espfriends/servo_dog_ctrl 0.2.0` is an upstream registry dependency and needs no local override.
|
||||
|
||||
## Reproduction
|
||||
|
||||
```bash
|
||||
source ~/.espressif/v6.0.1/esp-idf/export.sh
|
||||
python scripts/build.py <board> --name <variant>
|
||||
```
|
||||
|
||||
This document is a migration-status snapshot. Build compatibility and hardware compatibility are tracked separately. Add "hardware validated" to a board only after validation on physical hardware is complete.
|
||||
@@ -0,0 +1,185 @@
|
||||
# Dynamic Text Glyph Push Extension
|
||||
|
||||
This document defines version 1 of the `glyph_push` protocol extension. The extension lets a server
|
||||
send bitmap glyphs that are missing from a device's installed text font. It applies equally to the
|
||||
WebSocket and MQTT/UDP transports because capability advertisement and incoming JSON handling are
|
||||
implemented in the shared protocol layer.
|
||||
|
||||
The extension supplements text rendering only. It does not change the text, TTS audio, or STT
|
||||
semantics of the containing message.
|
||||
|
||||
## 1. Capability advertisement
|
||||
|
||||
The device advertises support in its client `hello` message:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "hello",
|
||||
"version": 1,
|
||||
"features": {
|
||||
"mcp": true,
|
||||
"glyph_push": true
|
||||
},
|
||||
"text_font": {
|
||||
"bundle": "noto-v1",
|
||||
"charset": "common",
|
||||
"size": 20,
|
||||
"bpp": 4
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`features.glyph_push` indicates support for this extension. A server must treat a missing or false
|
||||
value as unsupported. The `v` field in each pushed payload carries the extension version.
|
||||
|
||||
The `text_font` object describes the exact font data installed on the device:
|
||||
|
||||
| Field | Type | Meaning |
|
||||
|---|---|---|
|
||||
| `bundle` | string | Explicit font bundle identifier. It changes when glyph metrics, rendering behavior, character sets, or wire compatibility change. |
|
||||
| `charset` | string | Installed character set. Version 1 devices report `basic` or `common`. |
|
||||
| `size` | number | Text font pixel profile used by the firmware. |
|
||||
| `bpp` | number | Bits per pixel of the text font bitmap, currently `1` or `4`. |
|
||||
|
||||
`basic` is the font linked into the firmware. The standard XiaoZhi assets report `common` after
|
||||
loading their common font from the assets partition. The server must use the values from each
|
||||
device's hello message rather than inferring them from the board model.
|
||||
|
||||
An OTA assets package may replace the text font with a different size, bpp, character set, or font
|
||||
family. The firmware still loads any structurally valid CBIN font. When the package also provides
|
||||
complete `text_font_meta` fields, the device advertises those active runtime values and validates
|
||||
glyph pushes against them. A legacy or custom package without compatible glyph metadata continues
|
||||
to use its custom font, emoji, colors, and background, but advertises `glyph_push: false` and omits
|
||||
`text_font`. This prevents incompatible fallback glyphs without restricting theme customization.
|
||||
|
||||
## 2. Server glyph payload
|
||||
|
||||
The server may attach a `glyph_push` object to either of these server-to-device messages:
|
||||
|
||||
- a TTS message with `"type": "tts"` and `"state": "sentence_start"`;
|
||||
- an STT message with `"type": "stt"`.
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "tts",
|
||||
"state": "sentence_start",
|
||||
"text": "𠮷野家",
|
||||
"glyph_push": {
|
||||
"v": 1,
|
||||
"bundle": "noto-v1",
|
||||
"size": 20,
|
||||
"bpp": 4,
|
||||
"glyphs": [
|
||||
{
|
||||
"codepoint": 134071,
|
||||
"adv_w": 320,
|
||||
"box_w": 20,
|
||||
"box_h": 20,
|
||||
"ofs_x": 0,
|
||||
"ofs_y": 0,
|
||||
"bitmap": "<base64-encoded bitmap>"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The payload header must match the device capability exactly:
|
||||
|
||||
| Field | Requirement |
|
||||
|---|---|
|
||||
| `v` | Must be `1`. |
|
||||
| `bundle` | Must equal `text_font.bundle`. |
|
||||
| `size` | Must equal `text_font.size`. |
|
||||
| `bpp` | Must equal `text_font.bpp`. |
|
||||
| `glyphs` | The partial glyph batch being pushed, containing at most 64 entries. |
|
||||
|
||||
Each item uses the LVGL native bitmap-font metrics:
|
||||
|
||||
| Field | Meaning |
|
||||
|---|---|
|
||||
| `codepoint` | Unicode code point from `1` through `0x10FFFF`. |
|
||||
| `adv_w` | Horizontal advance in LVGL fixed-point units with four fractional bits (one pixel is 16 units). |
|
||||
| `box_w`, `box_h` | Bitmap dimensions in pixels. Each dimension must be from 0 through 64. |
|
||||
| `ofs_x`, `ofs_y` | Signed 16-bit glyph offsets relative to the text baseline and cursor position. |
|
||||
| `bitmap` | Base64 encoding of the uncompressed LVGL plain bitmap. |
|
||||
|
||||
The decoded bitmap length must be exactly:
|
||||
|
||||
```text
|
||||
ceil(box_w * box_h * bpp / 8)
|
||||
```
|
||||
|
||||
The bitmap must use the same plain, zero-stride layout as the matching Noto full-bundle CBIN font.
|
||||
Servers should extract and forward the bitmap and metrics directly from that CBIN profile instead of
|
||||
rasterizing an unrelated font at request time.
|
||||
|
||||
The sum of decoded bitmap lengths in one payload must not exceed 64 KiB. If any header, glyph, or
|
||||
bitmap is invalid, the device rejects the entire glyph payload but still displays the message text
|
||||
using its installed fonts. A PSRAM device may also use fallback glyphs cached by earlier messages.
|
||||
|
||||
## 3. Server selection algorithm
|
||||
|
||||
For every connection that advertises `glyph_push: true`, the server should:
|
||||
|
||||
1. Resolve the full font bundle identified by `text_font.bundle`.
|
||||
2. Select the CBIN profile matching `text_font.size` and `text_font.bpp`.
|
||||
3. Decode the message text into Unicode code points.
|
||||
4. Remove control characters, duplicates, and code points already present in
|
||||
`text_font.charset`.
|
||||
5. Extract the remaining glyphs from the full bundle.
|
||||
6. Apply the per-message limits and attach one `glyph_push` object to the text message.
|
||||
7. Omit `glyph_push` when no missing glyph is available.
|
||||
|
||||
The installed text font is searched before the dynamic fallback font. Pushed glyphs therefore fill
|
||||
missing code points; they do not override glyphs in `basic` or `common`.
|
||||
|
||||
The full font bundle may be shared by all device connections in a server process. Per-connection
|
||||
work is limited to using the capability tuple `(bundle, charset, size, bpp)` to select which glyphs
|
||||
are missing and which profile to read.
|
||||
|
||||
## 4. Device cache behavior
|
||||
|
||||
All glyphs in one message are inserted first, followed by a single fallback-font rebuild. The device
|
||||
never rebuilds the font once per glyph.
|
||||
|
||||
On a device with initialized PSRAM:
|
||||
|
||||
- bitmap, cmap, descriptor, and cache-entry storage is allocated in PSRAM;
|
||||
- glyphs are retained across messages;
|
||||
- the cache holds at most 256 glyphs and 64 KiB of decoded bitmap data;
|
||||
- the least recently inserted or updated entries are evicted when a limit is exceeded.
|
||||
|
||||
On a device without PSRAM:
|
||||
|
||||
- storage uses internal RAM;
|
||||
- only the current message's glyph batch is retained;
|
||||
- the next text message replaces or clears the previous batch.
|
||||
|
||||
This distinction does not affect the protocol. A server can send the glyphs needed by each message
|
||||
without knowing whether the device has PSRAM.
|
||||
|
||||
## 5. Compatibility and versioning
|
||||
|
||||
The server must not send glyphs when any of these conditions is true:
|
||||
|
||||
- `features.glyph_push` is absent or not supported by the server;
|
||||
- the server does not have the advertised bundle;
|
||||
- no full-font profile matches the advertised size and bpp;
|
||||
- the glyph data cannot satisfy the version 1 validation rules.
|
||||
|
||||
Fallback is automatic: messages without `glyph_push`, and messages whose glyph payload is rejected,
|
||||
are still processed normally.
|
||||
|
||||
When the font generator changes metrics, bitmap layout, source fonts, character sets, or rendering
|
||||
behavior, publish a new explicit bundle identifier. Do not serve glyphs from one bundle under
|
||||
another bundle's identifier even if their size and bpp happen to match.
|
||||
|
||||
## 6. Security requirements
|
||||
|
||||
Glyph payloads are untrusted network input. Implementations must validate the complete payload before
|
||||
mutating a live font, bound both item count and decoded size, verify base64 decoded length, and reject
|
||||
invalid code points or metrics. Servers should also bound their own per-message work and avoid
|
||||
sending glyphs already covered by the advertised charset.
|
||||
@@ -0,0 +1,174 @@
|
||||
# 动态文字 Glyph Push 扩展
|
||||
|
||||
本文档定义 `glyph_push` 协议扩展的版本 1。服务器可以通过该扩展向设备下发本地文字
|
||||
字库中缺失的位图 glyph。能力声明和 JSON 消息处理位于公共协议层,因此 WebSocket 与
|
||||
MQTT/UDP 使用完全相同的扩展格式。
|
||||
|
||||
该扩展只补充文字渲染能力,不改变消息中的文本、TTS 音频或 STT 语义。
|
||||
|
||||
## 1. 能力声明
|
||||
|
||||
设备在客户端 `hello` 消息中声明能力:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "hello",
|
||||
"version": 1,
|
||||
"features": {
|
||||
"mcp": true,
|
||||
"glyph_push": true
|
||||
},
|
||||
"text_font": {
|
||||
"bundle": "noto-v1",
|
||||
"charset": "common",
|
||||
"size": 20,
|
||||
"bpp": 4
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`features.glyph_push` 表示设备支持该扩展。字段缺失或为 false 时,服务器必须视为设备不支持。
|
||||
每次推送 payload 中的 `v` 字段负责表达扩展版本。
|
||||
|
||||
`text_font` 描述设备实际安装的文字字库:
|
||||
|
||||
| 字段 | 类型 | 含义 |
|
||||
|---|---|---|
|
||||
| `bundle` | string | 显式的字体 bundle 标识。glyph 度量、渲染方式、字符集或线格式变化时需要更换。 |
|
||||
| `charset` | string | 已安装字符集。版本 1 设备报告 `basic` 或 `common`。 |
|
||||
| `size` | number | 固件使用的文字字体像素规格。 |
|
||||
| `bpp` | number | 字体位图的每像素位数,目前为 `1` 或 `4`。 |
|
||||
|
||||
`basic` 是链接进固件的字库。标准小智 assets 从分区加载 common 字库后会报告 `common`。
|
||||
服务器必须使用每个连接在 hello 中报告的实际值,不能根据板型推测。
|
||||
|
||||
OTA assets 可以把文字字体替换为不同字号、bpp、字符集或字体家族的 CBIN 字体。固件仍会
|
||||
加载任何结构有效的 CBIN 字体;如果 assets 同时提供完整的 `text_font_meta`,设备会声明当前
|
||||
实际使用的运行时字体参数,并据此校验 glyph push。旧版或自定义 assets 缺少兼容的 glyph
|
||||
metadata 时,自定义字体、表情、颜色和背景仍然正常使用,但设备会声明 `glyph_push: false`
|
||||
并省略 `text_font`,从而只禁止不兼容的 fallback glyph,不限制主题定制能力。
|
||||
|
||||
## 2. 服务器下发格式
|
||||
|
||||
服务器可以在以下消息中附加 `glyph_push` 对象:
|
||||
|
||||
- `"type": "tts"`、`"state": "sentence_start"` 的 TTS 消息;
|
||||
- `"type": "stt"` 的 STT 消息。
|
||||
|
||||
示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "tts",
|
||||
"state": "sentence_start",
|
||||
"text": "𠮷野家",
|
||||
"glyph_push": {
|
||||
"v": 1,
|
||||
"bundle": "noto-v1",
|
||||
"size": 20,
|
||||
"bpp": 4,
|
||||
"glyphs": [
|
||||
{
|
||||
"codepoint": 134071,
|
||||
"adv_w": 320,
|
||||
"box_w": 20,
|
||||
"box_h": 20,
|
||||
"ofs_x": 0,
|
||||
"ofs_y": 0,
|
||||
"bitmap": "<base64 编码的位图>"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
payload 头必须与设备能力完全匹配:
|
||||
|
||||
| 字段 | 要求 |
|
||||
|---|---|
|
||||
| `v` | 必须为 `1`。 |
|
||||
| `bundle` | 必须等于 `text_font.bundle`。 |
|
||||
| `size` | 必须等于 `text_font.size`。 |
|
||||
| `bpp` | 必须等于 `text_font.bpp`。 |
|
||||
| `glyphs` | 本次增量推送的 glyph 数组,最多 64 项。 |
|
||||
|
||||
每个 item 使用 LVGL 原生位图字体度量:
|
||||
|
||||
| 字段 | 含义 |
|
||||
|---|---|
|
||||
| `codepoint` | `1` 到 `0x10FFFF` 的 Unicode code point。 |
|
||||
| `adv_w` | 带 4 位小数的 LVGL 定点水平 advance,16 个单位等于 1 像素。 |
|
||||
| `box_w`、`box_h` | 位图宽高,单项范围为 0 到 64 像素。 |
|
||||
| `ofs_x`、`ofs_y` | 相对于文字基线和光标位置的有符号 16 位偏移。 |
|
||||
| `bitmap` | 未压缩 LVGL plain 位图的 Base64 编码。 |
|
||||
|
||||
解码后的位图长度必须严格等于:
|
||||
|
||||
```text
|
||||
ceil(box_w * box_h * bpp / 8)
|
||||
```
|
||||
|
||||
位图必须与对应 Noto full bundle CBIN 字体使用相同的 plain、零 stride 布局。服务器应直接
|
||||
提取相同 profile 的 CBIN 位图和度量,不应临时使用其他字体重新光栅化。
|
||||
|
||||
单条 payload 的解码位图总长度不得超过 64 KiB。任意头字段、glyph 或位图无效时,设备会
|
||||
拒绝整批 glyph,但仍使用本地字体显示消息文本;有 PSRAM 的设备还可能使用此前缓存的
|
||||
fallback glyph。
|
||||
|
||||
## 3. 服务器选择流程
|
||||
|
||||
对于声明 `glyph_push: true` 的每个连接,服务器应:
|
||||
|
||||
1. 根据 `text_font.bundle` 找到对应 full 字体 bundle。
|
||||
2. 根据 `text_font.size` 和 `text_font.bpp` 选择 CBIN profile。
|
||||
3. 将消息文本解码为 Unicode code point。
|
||||
4. 去掉控制字符、重复字符及 `text_font.charset` 已包含的字符。
|
||||
5. 从 full bundle 提取剩余 glyph。
|
||||
6. 执行单消息限制,并将一个 `glyph_push` 对象附加到文字消息。
|
||||
7. 没有可用的缺失 glyph 时省略 `glyph_push`。
|
||||
|
||||
设备先查询本地文字字体,再查询动态 fallback。因此 glyph push 只补充缺字,不会覆盖
|
||||
`basic` 或 `common` 中已有的 glyph。
|
||||
|
||||
同一服务器进程中的所有设备连接可以共享 full 字体 bundle 服务。每个连接只需根据
|
||||
`(bundle, charset, size, bpp)` 能力组合判断缺字并选择 profile。
|
||||
|
||||
## 4. 设备缓存行为
|
||||
|
||||
同一消息中的全部 glyph 会先加入缓存,随后只执行一次 fallback 字体 rebuild,不会每加入
|
||||
一个 glyph 就 rebuild 一次。
|
||||
|
||||
设备有已初始化的 PSRAM 时:
|
||||
|
||||
- bitmap、cmap、descriptor 和缓存条目存放在 PSRAM;
|
||||
- glyph 跨消息保留;
|
||||
- 缓存上限为 256 个 glyph 和 64 KiB 解码位图;
|
||||
- 超限时淘汰最早插入或更新的条目。
|
||||
|
||||
设备没有 PSRAM 时:
|
||||
|
||||
- 数据使用内部 RAM;
|
||||
- 只保留当前消息的 glyph 批次;
|
||||
- 下一条文字消息会替换或清空上一批 glyph。
|
||||
|
||||
该差异不改变协议。服务器无需知道设备是否有 PSRAM,可以为每条消息发送它所需的 glyph。
|
||||
|
||||
## 5. 兼容与版本管理
|
||||
|
||||
以下任一条件成立时,服务器不得发送 glyph:
|
||||
|
||||
- `features.glyph_push` 缺失、为 false,或服务器不支持该扩展;
|
||||
- 服务器没有设备声明的 bundle;
|
||||
- 找不到匹配 size 和 bpp 的 full 字体 profile;
|
||||
- glyph 数据不满足版本 1 的校验要求。
|
||||
|
||||
兼容降级是自动的:没有 `glyph_push`,或 glyph payload 被拒绝时,设备仍会正常处理消息。
|
||||
|
||||
字体生成器的度量、位图布局、源字体、字符集或渲染方式变化时,应发布新的显式 bundle
|
||||
标识。即使 size 和 bpp 相同,也不能用一个 bundle 的标识下发另一个 bundle 的 glyph。
|
||||
|
||||
## 6. 安全要求
|
||||
|
||||
glyph payload 是不可信网络输入。实现必须先完整校验 payload,再修改正在使用的字体;限制
|
||||
item 数量和解码总长度;验证 Base64 解码长度;拒绝非法 code point 或度量。服务器也应限制
|
||||
单消息处理量,避免重复发送设备声明字符集已经包含的 glyph。
|
||||
+214
-213
@@ -1,269 +1,270 @@
|
||||
# MCP (Model Context Protocol) 交互流程
|
||||
# MCP (Model Context Protocol) Interaction Flow
|
||||
|
||||
NOTICE: AI 辅助生成, 在实现后台服务时, 请参照代码确认细节!!
|
||||
NOTICE: This document was AI-assisted; when implementing a backend, always cross-check the details against the code.
|
||||
|
||||
本项目中的 MCP 协议用于后台 API(MCP 客户端)与 ESP32 设备(MCP 服务器)之间的通信,以便后台能够发现和调用设备提供的功能(工具)。
|
||||
In this project, MCP is used between the backend API (MCP client) and the ESP32 device (MCP server) to let the backend discover and invoke the device's capabilities (tools).
|
||||
|
||||
## 协议格式
|
||||
## Message Format
|
||||
|
||||
根据代码 (`main/protocols/protocol.cc`, `main/mcp_server.cc`),MCP 消息是封装在基础通信协议(如 WebSocket 或 MQTT)的消息体中的。其内部结构遵循 [JSON-RPC 2.0](https://www.jsonrpc.org/specification) 规范。
|
||||
From `main/protocols/protocol.cc` and `main/mcp_server.cc`, MCP messages are wrapped inside the underlying transport (WebSocket or MQTT). The inner payload follows the [JSON-RPC 2.0](https://www.jsonrpc.org/specification) specification.
|
||||
|
||||
整体消息结构示例:
|
||||
Overall message layout:
|
||||
|
||||
```json
|
||||
{
|
||||
"session_id": "...", // 会话 ID
|
||||
"type": "mcp", // 消息类型,固定为 "mcp"
|
||||
"payload": { // JSON-RPC 2.0 负载
|
||||
"session_id": "...", // session id
|
||||
"type": "mcp", // fixed value "mcp"
|
||||
"payload": { // JSON-RPC 2.0 payload
|
||||
"jsonrpc": "2.0",
|
||||
"method": "...", // 方法名 (如 "initialize", "tools/list", "tools/call")
|
||||
"params": { ... }, // 方法参数 (对于 request)
|
||||
"id": ..., // 请求 ID (对于 request 和 response)
|
||||
"result": { ... }, // 方法执行结果 (对于 success response)
|
||||
"error": { ... } // 错误信息 (对于 error response)
|
||||
"method": "...", // method name ("initialize", "tools/list", "tools/call", ...)
|
||||
"params": { ... }, // arguments (for requests)
|
||||
"id": ..., // request id (for requests and responses)
|
||||
"result": { ... }, // success result (response)
|
||||
"error": { ... } // error (response)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
其中,`payload` 部分是标准的 JSON-RPC 2.0 消息:
|
||||
The `payload` follows standard JSON-RPC 2.0:
|
||||
|
||||
- `jsonrpc`: 固定的字符串 "2.0"。
|
||||
- `method`: 要调用的方法名称 (对于 Request)。
|
||||
- `params`: 方法的参数,一个结构化值,通常为对象 (对于 Request)。
|
||||
- `id`: 请求的标识符,客户端发送请求时提供,服务器响应时原样返回。用于匹配请求和响应。
|
||||
- `result`: 方法成功执行时的结果 (对于 Success Response)。
|
||||
- `error`: 方法执行失败时的错误信息 (对于 Error Response)。
|
||||
- `jsonrpc`: always `"2.0"`.
|
||||
- `method`: the method name (requests).
|
||||
- `params`: structured parameters, usually an object (requests).
|
||||
- `id`: request identifier; echoed back in responses.
|
||||
- `result`: success value (responses).
|
||||
- `error`: error information (responses).
|
||||
|
||||
## 交互流程及发送时机
|
||||
## Interaction Flow
|
||||
|
||||
MCP 的交互主要围绕客户端(后台 API)发现和调用设备上的“工具”(Tool)进行。
|
||||
MCP interactions are driven by the client (backend) discovering and invoking tools on the device.
|
||||
|
||||
1. **连接建立与能力通告**
|
||||
1. **Connection and capability announcement**
|
||||
|
||||
- **时机:** 设备启动并成功连接到后台 API 后。
|
||||
- **发送方:** 设备。
|
||||
- **消息:** 设备发送基础协议的 "hello" 消息给后台 API,消息中包含设备支持的能力列表,例如通过支持 MCP 协议 (`"mcp": true`)。
|
||||
- **示例 (非 MCP 负载,而是基础协议消息):**
|
||||
```json
|
||||
{
|
||||
"type": "hello",
|
||||
"version": ...,
|
||||
"features": {
|
||||
"mcp": true,
|
||||
...
|
||||
},
|
||||
"transport": "websocket", // 或 "mqtt"
|
||||
"audio_params": { ... },
|
||||
"session_id": "..." // 设备收到服务器hello后可能设置
|
||||
}
|
||||
```
|
||||
- **When**: after the device boots and connects to the backend.
|
||||
- **Direction**: device -> backend.
|
||||
- **Message**: the device sends the transport hello, advertising supported capabilities. MCP support is signaled via `"mcp": true` in the `features` map.
|
||||
- **Example (transport hello, not an MCP payload):**
|
||||
```json
|
||||
{
|
||||
"type": "hello",
|
||||
"version": 1,
|
||||
"features": {
|
||||
"mcp": true
|
||||
},
|
||||
"transport": "websocket",
|
||||
"audio_params": { ... },
|
||||
"session_id": "..."
|
||||
}
|
||||
```
|
||||
|
||||
2. **初始化 MCP 会话**
|
||||
2. **Initialize the MCP session**
|
||||
|
||||
- **时机:** 后台 API 收到设备 "hello" 消息,确认设备支持 MCP 后,通常作为 MCP 会话的第一个请求发送。
|
||||
- **发送方:** 后台 API (客户端)。
|
||||
- **方法:** `initialize`
|
||||
- **消息 (MCP payload):**
|
||||
- **When**: after the backend sees that the device supports MCP. Usually the first MCP request.
|
||||
- **Direction**: backend -> device.
|
||||
- **Method**: `initialize`
|
||||
- **Message (MCP payload):**
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "initialize",
|
||||
"params": {
|
||||
"capabilities": {
|
||||
// optional client capabilities
|
||||
"vision": {
|
||||
"url": "...", // camera image upload endpoint (must be an http URL, not a websocket URL)
|
||||
"token": "..." // token for the upload URL
|
||||
}
|
||||
// ... other client capabilities
|
||||
}
|
||||
},
|
||||
"id": 1
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "initialize",
|
||||
"params": {
|
||||
"capabilities": {
|
||||
// 客户端能力,可选
|
||||
- **Device response:**
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": {
|
||||
"tools": {}
|
||||
},
|
||||
"serverInfo": {
|
||||
"name": "...", // device name (BOARD_NAME)
|
||||
"version": "..." // firmware version
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
// 摄像头视觉相关
|
||||
"vision": {
|
||||
"url": "...", //摄像头: 图片处理地址(必须是http地址, 不是websocket地址)
|
||||
"token": "..." // url token
|
||||
}
|
||||
3. **Discover the tools**
|
||||
|
||||
// ... 其他客户端能力
|
||||
}
|
||||
},
|
||||
"id": 1 // 请求 ID
|
||||
}
|
||||
```
|
||||
- **When**: whenever the backend needs the list of callable tools and their signatures.
|
||||
- **Direction**: backend -> device.
|
||||
- **Method**: `tools/list`
|
||||
- **Request parameters**:
|
||||
- `cursor` (string, optional): pagination cursor. Empty on the first request.
|
||||
- `withUserTools` (boolean, optional, default `false`): if `true`, the device also includes "user-only" tools (see "User-only tools" below) in the listing. This is typically used by a companion app that lets the user trigger privileged actions directly.
|
||||
- **Message (MCP payload):**
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "tools/list",
|
||||
"params": {
|
||||
"cursor": "",
|
||||
"withUserTools": false
|
||||
},
|
||||
"id": 2
|
||||
}
|
||||
```
|
||||
- **Device response:**
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"result": {
|
||||
"tools": [
|
||||
{
|
||||
"name": "self.get_device_status",
|
||||
"description": "...",
|
||||
"inputSchema": { ... }
|
||||
},
|
||||
{
|
||||
"name": "self.audio_speaker.set_volume",
|
||||
"description": "...",
|
||||
"inputSchema": { ... }
|
||||
}
|
||||
// ... more tools
|
||||
],
|
||||
"nextCursor": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
- **Pagination**: when `nextCursor` is non-empty, the backend must send another `tools/list` request with that cursor to fetch the next page.
|
||||
|
||||
- **设备响应时机:** 设备收到 `initialize` 请求并处理后。
|
||||
- **设备响应消息 (MCP payload):**
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1, // 匹配请求 ID
|
||||
"result": {
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": {
|
||||
"tools": {} // 这里的 tools 似乎不列出详细信息,需要 tools/list
|
||||
},
|
||||
"serverInfo": {
|
||||
"name": "...", // 设备名称 (BOARD_NAME)
|
||||
"version": "..." // 设备固件版本
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
4. **Call a tool**
|
||||
|
||||
3. **发现设备工具列表**
|
||||
- **When**: the backend wants to execute a specific device function.
|
||||
- **Direction**: backend -> device.
|
||||
- **Method**: `tools/call`
|
||||
- **Message (MCP payload):**
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "self.audio_speaker.set_volume",
|
||||
"arguments": {
|
||||
"volume": 50
|
||||
}
|
||||
},
|
||||
"id": 3
|
||||
}
|
||||
```
|
||||
- **Successful response:**
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 3,
|
||||
"result": {
|
||||
"content": [
|
||||
{ "type": "text", "text": "true" }
|
||||
],
|
||||
"isError": false
|
||||
}
|
||||
}
|
||||
```
|
||||
- **Error response:**
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 3,
|
||||
"error": {
|
||||
"code": -32601,
|
||||
"message": "Unknown tool: self.non_existent_tool"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **时机:** 后台 API 需要获取设备当前支持的具体功能(工具)列表及其调用方式时。
|
||||
- **发送方:** 后台 API (客户端)。
|
||||
- **方法:** `tools/list`
|
||||
- **消息 (MCP payload):**
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "tools/list",
|
||||
"params": {
|
||||
"cursor": "" // 用于分页,首次请求为空字符串
|
||||
},
|
||||
"id": 2 // 请求 ID
|
||||
}
|
||||
```
|
||||
- **设备响应时机:** 设备收到 `tools/list` 请求并生成工具列表后。
|
||||
- **设备响应消息 (MCP payload):**
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2, // 匹配请求 ID
|
||||
"result": {
|
||||
"tools": [ // 工具对象列表
|
||||
{
|
||||
"name": "self.get_device_status",
|
||||
"description": "...",
|
||||
"inputSchema": { ... } // 参数 schema
|
||||
},
|
||||
{
|
||||
"name": "self.audio_speaker.set_volume",
|
||||
"description": "...",
|
||||
"inputSchema": { ... } // 参数 schema
|
||||
}
|
||||
// ... 更多工具
|
||||
],
|
||||
"nextCursor": "..." // 如果列表很大需要分页,这里会包含下一个请求的 cursor 值
|
||||
}
|
||||
}
|
||||
```
|
||||
- **分页处理:** 如果 `nextCursor` 字段非空,客户端需要再次发送 `tools/list` 请求,并在 `params` 中带上这个 `cursor` 值以获取下一页工具。
|
||||
5. **Device-initiated notifications**
|
||||
|
||||
4. **调用设备工具**
|
||||
- **When**: the device wants to inform the backend of internal events (e.g. state transitions). `Application::SendMcpMessage` is the outbound entry point.
|
||||
- **Direction**: device -> backend.
|
||||
- **Method**: conventionally `notifications/...` or any custom method.
|
||||
- **Message (MCP payload)**: JSON-RPC notifications have no `id`.
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "notifications/state_changed",
|
||||
"params": {
|
||||
"newState": "idle",
|
||||
"oldState": "connecting"
|
||||
}
|
||||
}
|
||||
```
|
||||
- **Backend handling**: process the notification without replying.
|
||||
|
||||
- **时机:** 后台 API 需要执行设备上的某个具体功能时。
|
||||
- **发送方:** 后台 API (客户端)。
|
||||
- **方法:** `tools/call`
|
||||
- **消息 (MCP payload):**
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "self.audio_speaker.set_volume", // 要调用的工具名称
|
||||
"arguments": {
|
||||
// 工具参数,对象格式
|
||||
"volume": 50 // 参数名及其值
|
||||
}
|
||||
},
|
||||
"id": 3 // 请求 ID
|
||||
}
|
||||
```
|
||||
- **设备响应时机:** 设备收到 `tools/call` 请求,执行相应的工具函数后。
|
||||
- **设备成功响应消息 (MCP payload):**
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 3, // 匹配请求 ID
|
||||
"result": {
|
||||
"content": [
|
||||
// 工具执行结果内容
|
||||
{ "type": "text", "text": "true" } // 示例:set_volume 返回 bool
|
||||
],
|
||||
"isError": false // 表示成功
|
||||
}
|
||||
}
|
||||
```
|
||||
- **设备失败响应消息 (MCP payload):**
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 3, // 匹配请求 ID
|
||||
"error": {
|
||||
"code": -32601, // JSON-RPC 错误码,例如 Method not found (-32601)
|
||||
"message": "Unknown tool: self.non_existent_tool" // 错误描述
|
||||
}
|
||||
}
|
||||
```
|
||||
## User-only Tools
|
||||
|
||||
5. **设备主动发送消息 (Notifications)**
|
||||
- **时机:** 设备内部发生需要通知后台 API 的事件时(例如,状态变化,虽然代码示例中没有明确的工具发送此类消息,但 `Application::SendMcpMessage` 的存在暗示了设备可能主动发送 MCP 消息)。
|
||||
- **发送方:** 设备 (服务器)。
|
||||
- **方法:** 可能是以 `notifications/` 开头的方法名,或者其他自定义方法。
|
||||
- **消息 (MCP payload):** 遵循 JSON-RPC Notification 格式,没有 `id` 字段。
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "notifications/state_changed", // 示例方法名
|
||||
"params": {
|
||||
"newState": "idle",
|
||||
"oldState": "connecting"
|
||||
}
|
||||
// 没有 id 字段
|
||||
}
|
||||
```
|
||||
- **后台 API 处理:** 接收到 Notification 后,后台 API 进行相应的处理,但不回复。
|
||||
The MCP server on the device maintains two kinds of tools:
|
||||
|
||||
## 交互图
|
||||
- **Regular tools** - registered via `McpServer::AddTool`. Exposed to the backend (and hence the AI model) by default.
|
||||
- **User-only tools** - registered via `McpServer::AddUserOnlyTool`. These are hidden from standard `tools/list` results, because they are privileged or user-facing actions that should not be invoked autonomously by the AI. Examples include system reboot, firmware upgrade, and screen snapshot upload.
|
||||
|
||||
下面是一个简化的交互序列图,展示了主要的 MCP 消息流程:
|
||||
The backend opts in to user-only tools by sending `tools/list` with `params.withUserTools = true`. Typical usage: a companion app screen that exposes these actions to the end user.
|
||||
|
||||
See [MCP IoT control usage](./mcp-usage.md) for how to register either kind of tool on the device side.
|
||||
|
||||
## Sequence Diagram
|
||||
|
||||
A simplified diagram of the main MCP message flow:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Device as ESP32 Device
|
||||
participant BackendAPI as 后台 API (Client)
|
||||
participant BackendAPI as Backend API (Client)
|
||||
|
||||
Note over Device, BackendAPI: 建立 WebSocket / MQTT 连接
|
||||
Note over Device, BackendAPI: Establish WebSocket / MQTT
|
||||
|
||||
Device->>BackendAPI: Hello Message (包含 "mcp": true)
|
||||
Device->>BackendAPI: Hello (features.mcp = true)
|
||||
|
||||
BackendAPI->>Device: MCP Initialize Request
|
||||
BackendAPI->>Device: MCP Initialize request
|
||||
Note over BackendAPI: method: initialize
|
||||
Note over BackendAPI: params: { capabilities: ... }
|
||||
|
||||
Device->>BackendAPI: MCP Initialize Response
|
||||
Note over Device: result: { protocolVersion: ..., serverInfo: ... }
|
||||
Device->>BackendAPI: MCP Initialize response
|
||||
Note over Device: result: { protocolVersion, serverInfo, ... }
|
||||
|
||||
BackendAPI->>Device: MCP Get Tools List Request
|
||||
Note over BackendAPI: method: tools/list
|
||||
Note over BackendAPI: params: { cursor: "" }
|
||||
BackendAPI->>Device: MCP tools/list request
|
||||
Note over BackendAPI: params: { cursor: "", withUserTools: false }
|
||||
|
||||
Device->>BackendAPI: MCP Get Tools List Response
|
||||
Device->>BackendAPI: MCP tools/list response
|
||||
Note over Device: result: { tools: [...], nextCursor: ... }
|
||||
|
||||
loop Optional Pagination
|
||||
BackendAPI->>Device: MCP Get Tools List Request
|
||||
Note over BackendAPI: method: tools/list
|
||||
loop Optional pagination
|
||||
BackendAPI->>Device: MCP tools/list request
|
||||
Note over BackendAPI: params: { cursor: "..." }
|
||||
Device->>BackendAPI: MCP Get Tools List Response
|
||||
Device->>BackendAPI: MCP tools/list response
|
||||
Note over Device: result: { tools: [...], nextCursor: "" }
|
||||
end
|
||||
|
||||
BackendAPI->>Device: MCP Call Tool Request
|
||||
Note over BackendAPI: method: tools/call
|
||||
Note over BackendAPI: params: { name: "...", arguments: { ... } }
|
||||
BackendAPI->>Device: MCP tools/call request
|
||||
Note over BackendAPI: params: { name, arguments }
|
||||
|
||||
alt Tool Call Successful
|
||||
Device->>BackendAPI: MCP Tool Call Success Response
|
||||
Note over Device: result: { content: [...], isError: false }
|
||||
else Tool Call Failed
|
||||
Device->>BackendAPI: MCP Tool Call Error Response
|
||||
Note over Device: error: { code: ..., message: ... }
|
||||
alt Call succeeds
|
||||
Device->>BackendAPI: MCP tools/call success response
|
||||
Note over Device: result: { content, isError: false }
|
||||
else Call fails
|
||||
Device->>BackendAPI: MCP tools/call error response
|
||||
Note over Device: error: { code, message }
|
||||
end
|
||||
|
||||
opt Device Notification
|
||||
Device->>BackendAPI: MCP Notification
|
||||
opt Device notification
|
||||
Device->>BackendAPI: MCP notification
|
||||
Note over Device: method: notifications/...
|
||||
Note over Device: params: { ... }
|
||||
end
|
||||
```
|
||||
|
||||
这份文档概述了该项目中 MCP 协议的主要交互流程。具体的参数细节和工具功能需要参考 `main/mcp_server.cc` 中 `McpServer::AddCommonTools` 以及各个工具的实现。
|
||||
This document summarizes the MCP interaction flow in this project. For exact parameter shapes, behavior, and available tools, refer to `McpServer::AddCommonTools` / `AddUserOnlyTools` in `main/mcp_server.cc` and the per-board `InitializeTools` implementations.
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
# MCP (Model Context Protocol) 交互流程
|
||||
|
||||
NOTICE: AI 辅助生成, 在实现后台服务时, 请参照代码确认细节!!
|
||||
|
||||
本项目中的 MCP 协议用于后台 API(MCP 客户端)与 ESP32 设备(MCP 服务器)之间的通信,以便后台能够发现和调用设备提供的功能(工具)。
|
||||
|
||||
## 协议格式
|
||||
|
||||
根据代码 (`main/protocols/protocol.cc`, `main/mcp_server.cc`),MCP 消息是封装在基础通信协议(如 WebSocket 或 MQTT)的消息体中的。其内部结构遵循 [JSON-RPC 2.0](https://www.jsonrpc.org/specification) 规范。
|
||||
|
||||
整体消息结构示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"session_id": "...", // 会话 ID
|
||||
"type": "mcp", // 消息类型,固定为 "mcp"
|
||||
"payload": { // JSON-RPC 2.0 负载
|
||||
"jsonrpc": "2.0",
|
||||
"method": "...", // 方法名 (如 "initialize", "tools/list", "tools/call")
|
||||
"params": { ... }, // 方法参数 (对于 request)
|
||||
"id": ..., // 请求 ID (对于 request 和 response)
|
||||
"result": { ... }, // 方法执行结果 (对于 success response)
|
||||
"error": { ... } // 错误信息 (对于 error response)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
其中,`payload` 部分是标准的 JSON-RPC 2.0 消息:
|
||||
|
||||
- `jsonrpc`: 固定的字符串 "2.0"。
|
||||
- `method`: 要调用的方法名称 (对于 Request)。
|
||||
- `params`: 方法的参数,一个结构化值,通常为对象 (对于 Request)。
|
||||
- `id`: 请求的标识符,客户端发送请求时提供,服务器响应时原样返回。用于匹配请求和响应。
|
||||
- `result`: 方法成功执行时的结果 (对于 Success Response)。
|
||||
- `error`: 方法执行失败时的错误信息 (对于 Error Response)。
|
||||
|
||||
## 交互流程及发送时机
|
||||
|
||||
MCP 的交互主要围绕客户端(后台 API)发现和调用设备上的“工具”(Tool)进行。
|
||||
|
||||
1. **连接建立与能力通告**
|
||||
|
||||
- **时机:** 设备启动并成功连接到后台 API 后。
|
||||
- **发送方:** 设备。
|
||||
- **消息:** 设备发送基础协议的 "hello" 消息给后台 API,消息中包含设备支持的能力列表,例如通过支持 MCP 协议 (`"mcp": true`)。
|
||||
- **示例 (非 MCP 负载,而是基础协议消息):**
|
||||
```json
|
||||
{
|
||||
"type": "hello",
|
||||
"version": ...,
|
||||
"features": {
|
||||
"mcp": true,
|
||||
...
|
||||
},
|
||||
"transport": "websocket", // 或 "mqtt"
|
||||
"audio_params": { ... },
|
||||
"session_id": "..." // 设备收到服务器hello后可能设置
|
||||
}
|
||||
```
|
||||
|
||||
2. **初始化 MCP 会话**
|
||||
|
||||
- **时机:** 后台 API 收到设备 "hello" 消息,确认设备支持 MCP 后,通常作为 MCP 会话的第一个请求发送。
|
||||
- **发送方:** 后台 API (客户端)。
|
||||
- **方法:** `initialize`
|
||||
- **消息 (MCP payload):**
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "initialize",
|
||||
"params": {
|
||||
"capabilities": {
|
||||
// 客户端能力,可选
|
||||
|
||||
// 摄像头视觉相关
|
||||
"vision": {
|
||||
"url": "...", //摄像头: 图片处理地址(必须是http地址, 不是websocket地址)
|
||||
"token": "..." // url token
|
||||
}
|
||||
|
||||
// ... 其他客户端能力
|
||||
}
|
||||
},
|
||||
"id": 1 // 请求 ID
|
||||
}
|
||||
```
|
||||
|
||||
- **设备响应时机:** 设备收到 `initialize` 请求并处理后。
|
||||
- **设备响应消息 (MCP payload):**
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1, // 匹配请求 ID
|
||||
"result": {
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": {
|
||||
"tools": {} // 这里的 tools 似乎不列出详细信息,需要 tools/list
|
||||
},
|
||||
"serverInfo": {
|
||||
"name": "...", // 设备名称 (BOARD_NAME)
|
||||
"version": "..." // 设备固件版本
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **发现设备工具列表**
|
||||
|
||||
- **时机:** 后台 API 需要获取设备当前支持的具体功能(工具)列表及其调用方式时。
|
||||
- **发送方:** 后台 API (客户端)。
|
||||
- **方法:** `tools/list`
|
||||
- **消息 (MCP payload):**
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "tools/list",
|
||||
"params": {
|
||||
"cursor": "" // 用于分页,首次请求为空字符串
|
||||
},
|
||||
"id": 2 // 请求 ID
|
||||
}
|
||||
```
|
||||
- **设备响应时机:** 设备收到 `tools/list` 请求并生成工具列表后。
|
||||
- **设备响应消息 (MCP payload):**
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2, // 匹配请求 ID
|
||||
"result": {
|
||||
"tools": [ // 工具对象列表
|
||||
{
|
||||
"name": "self.get_device_status",
|
||||
"description": "...",
|
||||
"inputSchema": { ... } // 参数 schema
|
||||
},
|
||||
{
|
||||
"name": "self.audio_speaker.set_volume",
|
||||
"description": "...",
|
||||
"inputSchema": { ... } // 参数 schema
|
||||
}
|
||||
// ... 更多工具
|
||||
],
|
||||
"nextCursor": "..." // 如果列表很大需要分页,这里会包含下一个请求的 cursor 值
|
||||
}
|
||||
}
|
||||
```
|
||||
- **分页处理:** 如果 `nextCursor` 字段非空,客户端需要再次发送 `tools/list` 请求,并在 `params` 中带上这个 `cursor` 值以获取下一页工具。
|
||||
|
||||
4. **调用设备工具**
|
||||
|
||||
- **时机:** 后台 API 需要执行设备上的某个具体功能时。
|
||||
- **发送方:** 后台 API (客户端)。
|
||||
- **方法:** `tools/call`
|
||||
- **消息 (MCP payload):**
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "self.audio_speaker.set_volume", // 要调用的工具名称
|
||||
"arguments": {
|
||||
// 工具参数,对象格式
|
||||
"volume": 50 // 参数名及其值
|
||||
}
|
||||
},
|
||||
"id": 3 // 请求 ID
|
||||
}
|
||||
```
|
||||
- **设备响应时机:** 设备收到 `tools/call` 请求,执行相应的工具函数后。
|
||||
- **设备成功响应消息 (MCP payload):**
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 3, // 匹配请求 ID
|
||||
"result": {
|
||||
"content": [
|
||||
// 工具执行结果内容
|
||||
{ "type": "text", "text": "true" } // 示例:set_volume 返回 bool
|
||||
],
|
||||
"isError": false // 表示成功
|
||||
}
|
||||
}
|
||||
```
|
||||
- **设备失败响应消息 (MCP payload):**
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 3, // 匹配请求 ID
|
||||
"error": {
|
||||
"code": -32601, // JSON-RPC 错误码,例如 Method not found (-32601)
|
||||
"message": "Unknown tool: self.non_existent_tool" // 错误描述
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
5. **设备主动发送消息 (Notifications)**
|
||||
- **时机:** 设备内部发生需要通知后台 API 的事件时(例如,状态变化,虽然代码示例中没有明确的工具发送此类消息,但 `Application::SendMcpMessage` 的存在暗示了设备可能主动发送 MCP 消息)。
|
||||
- **发送方:** 设备 (服务器)。
|
||||
- **方法:** 可能是以 `notifications/` 开头的方法名,或者其他自定义方法。
|
||||
- **消息 (MCP payload):** 遵循 JSON-RPC Notification 格式,没有 `id` 字段。
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "notifications/state_changed", // 示例方法名
|
||||
"params": {
|
||||
"newState": "idle",
|
||||
"oldState": "connecting"
|
||||
}
|
||||
// 没有 id 字段
|
||||
}
|
||||
```
|
||||
- **后台 API 处理:** 接收到 Notification 后,后台 API 进行相应的处理,但不回复。
|
||||
|
||||
## 交互图
|
||||
|
||||
下面是一个简化的交互序列图,展示了主要的 MCP 消息流程:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Device as ESP32 Device
|
||||
participant BackendAPI as 后台 API (Client)
|
||||
|
||||
Note over Device, BackendAPI: 建立 WebSocket / MQTT 连接
|
||||
|
||||
Device->>BackendAPI: Hello Message (包含 "mcp": true)
|
||||
|
||||
BackendAPI->>Device: MCP Initialize Request
|
||||
Note over BackendAPI: method: initialize
|
||||
Note over BackendAPI: params: { capabilities: ... }
|
||||
|
||||
Device->>BackendAPI: MCP Initialize Response
|
||||
Note over Device: result: { protocolVersion: ..., serverInfo: ... }
|
||||
|
||||
BackendAPI->>Device: MCP Get Tools List Request
|
||||
Note over BackendAPI: method: tools/list
|
||||
Note over BackendAPI: params: { cursor: "" }
|
||||
|
||||
Device->>BackendAPI: MCP Get Tools List Response
|
||||
Note over Device: result: { tools: [...], nextCursor: ... }
|
||||
|
||||
loop Optional Pagination
|
||||
BackendAPI->>Device: MCP Get Tools List Request
|
||||
Note over BackendAPI: method: tools/list
|
||||
Note over BackendAPI: params: { cursor: "..." }
|
||||
Device->>BackendAPI: MCP Get Tools List Response
|
||||
Note over Device: result: { tools: [...], nextCursor: "" }
|
||||
end
|
||||
|
||||
BackendAPI->>Device: MCP Call Tool Request
|
||||
Note over BackendAPI: method: tools/call
|
||||
Note over BackendAPI: params: { name: "...", arguments: { ... } }
|
||||
|
||||
alt Tool Call Successful
|
||||
Device->>BackendAPI: MCP Tool Call Success Response
|
||||
Note over Device: result: { content: [...], isError: false }
|
||||
else Tool Call Failed
|
||||
Device->>BackendAPI: MCP Tool Call Error Response
|
||||
Note over Device: error: { code: ..., message: ... }
|
||||
end
|
||||
|
||||
opt Device Notification
|
||||
Device->>BackendAPI: MCP Notification
|
||||
Note over Device: method: notifications/...
|
||||
Note over Device: params: { ... }
|
||||
end
|
||||
```
|
||||
|
||||
这份文档概述了该项目中 MCP 协议的主要交互流程。具体的参数细节和工具功能需要参考 `main/mcp_server.cc` 中 `McpServer::AddCommonTools` 以及各个工具的实现。
|
||||
+120
-50
@@ -1,76 +1,143 @@
|
||||
# MCP 协议物联网控制用法说明
|
||||
# MCP IoT Control Usage
|
||||
|
||||
> 本文档介绍如何基于 MCP 协议实现 ESP32 设备的物联网控制。详细协议流程请参考 [`mcp-protocol.md`](./mcp-protocol.md)。
|
||||
> This document describes how to implement IoT control for ESP32 devices using the MCP protocol. For the detailed wire protocol, see [`mcp-protocol.md`](./mcp-protocol.md).
|
||||
|
||||
## 简介
|
||||
## Introduction
|
||||
|
||||
MCP(Model Context Protocol)是新一代推荐用于物联网控制的协议,通过标准 JSON-RPC 2.0 格式在后台与设备间发现和调用"工具"(Tool),实现灵活的设备控制。
|
||||
MCP (Model Context Protocol) is the recommended protocol for IoT control in this project. It uses JSON-RPC 2.0 to let the backend discover and invoke "tools" registered by the device, giving you a flexible way to expose device functionality.
|
||||
|
||||
## 典型使用流程
|
||||
## Typical Flow
|
||||
|
||||
1. 设备启动后通过基础协议(如 WebSocket/MQTT)与后台建立连接。
|
||||
2. 后台通过 MCP 协议的 `initialize` 方法初始化会话。
|
||||
3. 后台通过 `tools/list` 获取设备支持的所有工具(功能)及参数说明。
|
||||
4. 后台通过 `tools/call` 调用具体工具,实现对设备的控制。
|
||||
1. The device boots and connects to the backend over WebSocket or MQTT.
|
||||
2. The backend sends an `initialize` call to start the MCP session.
|
||||
3. The backend issues `tools/list` to discover available tools and their input schemas.
|
||||
4. The backend calls individual tools with `tools/call` to control the device.
|
||||
|
||||
详细协议格式与交互请见 [`mcp-protocol.md`](./mcp-protocol.md)。
|
||||
See [`mcp-protocol.md`](./mcp-protocol.md) for the exact message format.
|
||||
|
||||
## 设备端工具注册方法说明
|
||||
## Registering Tools on the Device
|
||||
|
||||
设备通过 `McpServer::AddTool` 方法注册可被后台调用的"工具"。其常用函数签名如下:
|
||||
Tools are registered through the `McpServer` singleton. There are two registration APIs:
|
||||
|
||||
- `McpServer::AddTool` - regular tool, visible in the default `tools/list` response and callable by the AI model.
|
||||
- `McpServer::AddUserOnlyTool` - hidden tool, only returned when the backend lists tools with `withUserTools=true`. Use this for privileged or user-initiated actions (reboot, firmware upgrade, snapshots, etc.) that must not be invoked autonomously by the model.
|
||||
|
||||
Both APIs share the same signature:
|
||||
|
||||
```cpp
|
||||
void AddTool(
|
||||
const std::string& name, // 工具名称,建议唯一且有层次感,如 self.dog.forward
|
||||
const std::string& description, // 工具描述,简明说明功能,便于大模型理解
|
||||
const PropertyList& properties, // 输入参数列表(可为空),支持类型:布尔、整数、字符串
|
||||
std::function<ReturnValue(const PropertyList&)> callback // 工具被调用时的回调实现
|
||||
const std::string& name, // unique tool name, e.g. self.dog.forward
|
||||
const std::string& description, // short description for the model
|
||||
const PropertyList& properties, // input parameters (may be empty); supported types: bool, int, string
|
||||
std::function<ReturnValue(const PropertyList&)> callback // implementation
|
||||
);
|
||||
|
||||
void AddUserOnlyTool(
|
||||
const std::string& name,
|
||||
const std::string& description,
|
||||
const PropertyList& properties,
|
||||
std::function<ReturnValue(const PropertyList&)> callback
|
||||
);
|
||||
```
|
||||
- name:工具唯一标识,建议用"模块.功能"命名风格。
|
||||
- description:自然语言描述,便于 AI/用户理解。
|
||||
- properties:参数列表,支持类型有布尔、整数、字符串,可指定范围和默认值。
|
||||
- callback:收到调用请求时的实际执行逻辑,返回值可为 bool/int/string。
|
||||
|
||||
## 典型注册示例(以 ESP-Hi 为例)
|
||||
- `name` - unique identifier. A `module.action` naming style works well.
|
||||
- `description` - natural-language description; used by the AI to decide when to call the tool.
|
||||
- `properties` - input parameters. Supported property types are boolean, integer, and string, with optional min/max and default values.
|
||||
- `callback` - implementation. Return values may be `bool`, `int`, or `std::string`.
|
||||
|
||||
## Example (ESP-Hi)
|
||||
|
||||
```cpp
|
||||
void InitializeTools() {
|
||||
auto& mcp_server = McpServer::GetInstance();
|
||||
// 例1:无参数,控制机器人前进
|
||||
mcp_server.AddTool("self.dog.forward", "机器人向前移动", PropertyList(), [this](const PropertyList&) -> ReturnValue {
|
||||
servo_dog_ctrl_send(DOG_STATE_FORWARD, NULL);
|
||||
return true;
|
||||
});
|
||||
// 例2:带参数,设置灯光 RGB 颜色
|
||||
mcp_server.AddTool("self.light.set_rgb", "设置RGB颜色", PropertyList({
|
||||
Property("r", kPropertyTypeInteger, 0, 255),
|
||||
Property("g", kPropertyTypeInteger, 0, 255),
|
||||
Property("b", kPropertyTypeInteger, 0, 255)
|
||||
}), [this](const PropertyList& properties) -> ReturnValue {
|
||||
int r = properties["r"].value<int>();
|
||||
int g = properties["g"].value<int>();
|
||||
int b = properties["b"].value<int>();
|
||||
led_on_ = true;
|
||||
SetLedColor(r, g, b);
|
||||
return true;
|
||||
});
|
||||
|
||||
// Example 1: no arguments - move the robot forward
|
||||
mcp_server.AddTool("self.dog.forward",
|
||||
"Move the robot forward",
|
||||
PropertyList(),
|
||||
[this](const PropertyList&) -> ReturnValue {
|
||||
servo_dog_ctrl_send(DOG_STATE_FORWARD, NULL);
|
||||
return true;
|
||||
});
|
||||
|
||||
// Example 2: with arguments - set RGB light color
|
||||
mcp_server.AddTool("self.light.set_rgb",
|
||||
"Set the RGB color of the light",
|
||||
PropertyList({
|
||||
Property("r", kPropertyTypeInteger, 0, 255),
|
||||
Property("g", kPropertyTypeInteger, 0, 255),
|
||||
Property("b", kPropertyTypeInteger, 0, 255)
|
||||
}),
|
||||
[this](const PropertyList& properties) -> ReturnValue {
|
||||
int r = properties["r"].value<int>();
|
||||
int g = properties["g"].value<int>();
|
||||
int b = properties["b"].value<int>();
|
||||
led_on_ = true;
|
||||
SetLedColor(r, g, b);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 常见工具调用 JSON-RPC 示例
|
||||
## Example - Registering a User-only Tool
|
||||
|
||||
```cpp
|
||||
mcp_server.AddUserOnlyTool("self.display.clear_cache",
|
||||
"Clear locally cached images. User-only action.",
|
||||
PropertyList(),
|
||||
[](const PropertyList&) -> ReturnValue {
|
||||
ClearLocalCache();
|
||||
return true;
|
||||
});
|
||||
```
|
||||
|
||||
A tool registered this way will not appear in a regular `tools/list` response. The backend must set `params.withUserTools = true` to see it.
|
||||
|
||||
## Built-in Tools
|
||||
|
||||
`McpServer::AddCommonTools` and `McpServer::AddUserOnlyTools` register a number of tools automatically:
|
||||
|
||||
### Default (AI-callable) tools - from `AddCommonTools`
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `self.get_device_status` | Returns the current volume, screen, battery, network, etc. |
|
||||
| `self.audio_speaker.set_volume` | Set speaker volume (`volume`: 0-100). |
|
||||
| `self.screen.set_brightness` | Set screen brightness when a backlight is available (`brightness`: 0-100). |
|
||||
| `self.screen.set_theme` | Switch UI theme (`theme`: `"light"` or `"dark"`), when LVGL is enabled. |
|
||||
| `self.camera.take_photo` | Take a picture with the on-board camera (when the board has one) and answer the given `question` about it. |
|
||||
|
||||
Board-specific tools are appended after these by each board's `InitializeTools()`.
|
||||
|
||||
### User-only tools - from `AddUserOnlyTools`
|
||||
|
||||
These tools are hidden by default. The backend must pass `withUserTools=true` to `tools/list` to see them. They are intended for companion apps / end users rather than the AI model.
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `self.get_system_info` | Return a JSON blob describing the system. |
|
||||
| `self.reboot` | Reboot the device after a short delay. |
|
||||
| `self.upgrade_firmware` | Download firmware from `url` and install it, then reboot. |
|
||||
| `self.screen.get_info` | Return the current screen width, height, and whether it is monochrome (LVGL boards only). |
|
||||
| `self.screen.snapshot` | Snapshot the screen as JPEG and upload it to `url` (LVGL boards, when `CONFIG_LV_USE_SNAPSHOT=y`). |
|
||||
| `self.screen.preview_image` | Download and display an image from `url` on the screen. |
|
||||
| `self.assets.set_download_url` | Set the download URL for the assets partition. |
|
||||
|
||||
## JSON-RPC Examples
|
||||
|
||||
### 1. Get the tools list
|
||||
|
||||
### 1. 获取工具列表
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "tools/list",
|
||||
"params": { "cursor": "" },
|
||||
"params": { "cursor": "", "withUserTools": false },
|
||||
"id": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 控制底盘前进
|
||||
### 2. Move the chassis forward
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
@@ -83,7 +150,8 @@ void InitializeTools() {
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 切换灯光模式
|
||||
### 3. Switch the light mode
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
@@ -96,20 +164,22 @@ void InitializeTools() {
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 摄像头翻转
|
||||
### 4. Reboot the device (user-only)
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "self.camera.set_camera_flipped",
|
||||
"name": "self.reboot",
|
||||
"arguments": {}
|
||||
},
|
||||
"id": 4
|
||||
}
|
||||
```
|
||||
|
||||
## 备注
|
||||
- 工具名称、参数及返回值请以设备端 `AddTool` 注册为准。
|
||||
- 推荐所有新项目统一采用 MCP 协议进行物联网控制。
|
||||
- 详细协议与进阶用法请查阅 [`mcp-protocol.md`](./mcp-protocol.md)。
|
||||
## Notes
|
||||
|
||||
- Tool names, parameters, and return values must match what the device registers via `AddTool` / `AddUserOnlyTool`.
|
||||
- Prefer MCP for any new IoT control.
|
||||
- For the wire protocol and advanced topics, see [`mcp-protocol.md`](./mcp-protocol.md).
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
# MCP 协议物联网控制用法说明
|
||||
|
||||
> 本文档介绍如何基于 MCP 协议实现 ESP32 设备的物联网控制。详细协议流程请参考 [`mcp-protocol_zh.md`](./mcp-protocol_zh.md)。
|
||||
|
||||
## 简介
|
||||
|
||||
MCP(Model Context Protocol)是新一代推荐用于物联网控制的协议,通过标准 JSON-RPC 2.0 格式在后台与设备间发现和调用"工具"(Tool),实现灵活的设备控制。
|
||||
|
||||
## 典型使用流程
|
||||
|
||||
1. 设备启动后通过基础协议(如 WebSocket/MQTT)与后台建立连接。
|
||||
2. 后台通过 MCP 协议的 `initialize` 方法初始化会话。
|
||||
3. 后台通过 `tools/list` 获取设备支持的所有工具(功能)及参数说明。
|
||||
4. 后台通过 `tools/call` 调用具体工具,实现对设备的控制。
|
||||
|
||||
详细协议格式与交互请见 [`mcp-protocol_zh.md`](./mcp-protocol_zh.md)。
|
||||
|
||||
## 设备端工具注册方法说明
|
||||
|
||||
设备通过 `McpServer::AddTool` 方法注册可被后台调用的"工具"。其常用函数签名如下:
|
||||
|
||||
```cpp
|
||||
void AddTool(
|
||||
const std::string& name, // 工具名称,建议唯一且有层次感,如 self.dog.forward
|
||||
const std::string& description, // 工具描述,简明说明功能,便于大模型理解
|
||||
const PropertyList& properties, // 输入参数列表(可为空),支持类型:布尔、整数、字符串
|
||||
std::function<ReturnValue(const PropertyList&)> callback // 工具被调用时的回调实现
|
||||
);
|
||||
```
|
||||
- name:工具唯一标识,建议用"模块.功能"命名风格。
|
||||
- description:自然语言描述,便于 AI/用户理解。
|
||||
- properties:参数列表,支持类型有布尔、整数、字符串,可指定范围和默认值。
|
||||
- callback:收到调用请求时的实际执行逻辑,返回值可为 bool/int/string。
|
||||
|
||||
## 典型注册示例(以 ESP-Hi 为例)
|
||||
|
||||
```cpp
|
||||
void InitializeTools() {
|
||||
auto& mcp_server = McpServer::GetInstance();
|
||||
// 例1:无参数,控制机器人前进
|
||||
mcp_server.AddTool("self.dog.forward", "机器人向前移动", PropertyList(), [this](const PropertyList&) -> ReturnValue {
|
||||
servo_dog_ctrl_send(DOG_STATE_FORWARD, NULL);
|
||||
return true;
|
||||
});
|
||||
// 例2:带参数,设置灯光 RGB 颜色
|
||||
mcp_server.AddTool("self.light.set_rgb", "设置RGB颜色", PropertyList({
|
||||
Property("r", kPropertyTypeInteger, 0, 255),
|
||||
Property("g", kPropertyTypeInteger, 0, 255),
|
||||
Property("b", kPropertyTypeInteger, 0, 255)
|
||||
}), [this](const PropertyList& properties) -> ReturnValue {
|
||||
int r = properties["r"].value<int>();
|
||||
int g = properties["g"].value<int>();
|
||||
int b = properties["b"].value<int>();
|
||||
led_on_ = true;
|
||||
SetLedColor(r, g, b);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 常见工具调用 JSON-RPC 示例
|
||||
|
||||
### 1. 获取工具列表
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "tools/list",
|
||||
"params": { "cursor": "" },
|
||||
"id": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 控制底盘前进
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "self.chassis.go_forward",
|
||||
"arguments": {}
|
||||
},
|
||||
"id": 2
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 切换灯光模式
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "self.chassis.switch_light_mode",
|
||||
"arguments": { "light_mode": 3 }
|
||||
},
|
||||
"id": 3
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 摄像头翻转
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "self.camera.set_camera_flipped",
|
||||
"arguments": {}
|
||||
},
|
||||
"id": 4
|
||||
}
|
||||
```
|
||||
|
||||
## 备注
|
||||
- 工具名称、参数及返回值请以设备端 `AddTool` 注册为准。
|
||||
- 推荐所有新项目统一采用 MCP 协议进行物联网控制。
|
||||
- 详细协议与进阶用法请查阅 [`mcp-protocol_zh.md`](./mcp-protocol_zh.md)。
|
||||
+212
-187
@@ -1,76 +1,77 @@
|
||||
# MQTT + UDP 混合通信协议文档
|
||||
# MQTT + UDP Hybrid Communication Protocol
|
||||
|
||||
基于代码实现整理的 MQTT + UDP 混合通信协议文档,概述设备端与服务器之间如何通过 MQTT 进行控制消息传输,通过 UDP 进行音频数据传输的交互方式。
|
||||
This document describes the MQTT + UDP hybrid protocol used between the device and the server, based on the current implementation: MQTT carries control messages, UDP carries real-time audio.
|
||||
|
||||
---
|
||||
|
||||
## 1. 协议概览
|
||||
## 1. Overview
|
||||
|
||||
本协议采用混合传输方式:
|
||||
- **MQTT**:用于控制消息、状态同步、JSON 数据交换
|
||||
- **UDP**:用于实时音频数据传输,支持加密
|
||||
The protocol uses two channels:
|
||||
|
||||
### 1.1 协议特点
|
||||
- **MQTT** - control messages, state synchronization, JSON payloads.
|
||||
- **UDP** - real-time audio, encrypted.
|
||||
|
||||
- **双通道设计**:控制与数据分离,确保实时性
|
||||
- **加密传输**:UDP 音频数据使用 AES-CTR 加密
|
||||
- **序列号保护**:防止数据包重放和乱序
|
||||
- **自动重连**:MQTT 连接断开时自动重连
|
||||
### 1.1 Key characteristics
|
||||
|
||||
- **Dual channel design** - control is separated from data so audio has low latency.
|
||||
- **Encrypted transport** - UDP audio is encrypted with AES-CTR.
|
||||
- **Sequence numbers** - guard against replay and reordering.
|
||||
- **Automatic reconnect** - MQTT reconnects on disconnect.
|
||||
|
||||
---
|
||||
|
||||
## 2. 总体流程概览
|
||||
## 2. End-to-end Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Device as ESP32 设备
|
||||
participant MQTT as MQTT 服务器
|
||||
participant UDP as UDP 服务器
|
||||
participant Device as ESP32 device
|
||||
participant MQTT as MQTT broker
|
||||
participant UDP as UDP server
|
||||
|
||||
Note over Device, UDP: 1. 建立 MQTT 连接
|
||||
Note over Device, UDP: 1. Establish MQTT connection
|
||||
Device->>MQTT: MQTT Connect
|
||||
MQTT->>Device: Connected
|
||||
|
||||
Note over Device, UDP: 2. 请求音频通道
|
||||
Device->>MQTT: Hello Message (type: "hello", transport: "udp")
|
||||
MQTT->>Device: Hello Response (UDP 连接信息 + 加密密钥)
|
||||
Note over Device, UDP: 2. Request audio channel
|
||||
Device->>MQTT: Hello message (type: "hello", transport: "udp")
|
||||
MQTT->>Device: Hello response (UDP endpoint + encryption keys)
|
||||
|
||||
Note over Device, UDP: 3. 建立 UDP 连接
|
||||
Note over Device, UDP: 3. Establish UDP connection
|
||||
Device->>UDP: UDP Connect
|
||||
UDP->>Device: Connected
|
||||
|
||||
Note over Device, UDP: 4. 音频数据传输
|
||||
loop 音频流传输
|
||||
Device->>UDP: 加密音频数据 (Opus)
|
||||
UDP->>Device: 加密音频数据 (Opus)
|
||||
Note over Device, UDP: 4. Audio streaming
|
||||
loop Audio stream
|
||||
Device->>UDP: Encrypted audio (Opus)
|
||||
UDP->>Device: Encrypted audio (Opus)
|
||||
end
|
||||
|
||||
Note over Device, UDP: 5. 控制消息交换
|
||||
par 控制消息
|
||||
Device->>MQTT: Listen/TTS/MCP 消息
|
||||
MQTT->>Device: STT/TTS/MCP 响应
|
||||
Note over Device, UDP: 5. Control messages
|
||||
par Control
|
||||
Device->>MQTT: Listen / TTS / MCP messages
|
||||
MQTT->>Device: STT / TTS / MCP / Alert responses
|
||||
end
|
||||
|
||||
Note over Device, UDP: 6. 关闭连接
|
||||
Device->>MQTT: Goodbye Message
|
||||
Note over Device, UDP: 6. Teardown
|
||||
Device->>MQTT: Goodbye
|
||||
Device->>UDP: Disconnect
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. MQTT 控制通道
|
||||
## 3. MQTT Control Channel
|
||||
|
||||
### 3.1 连接建立
|
||||
### 3.1 Connection
|
||||
|
||||
设备通过 MQTT 连接到服务器,连接参数包括:
|
||||
- **Endpoint**:MQTT 服务器地址和端口
|
||||
- **Client ID**:设备唯一标识符
|
||||
- **Username/Password**:认证凭据
|
||||
- **Keep Alive**:心跳间隔(默认240秒)
|
||||
The device connects to the broker using:
|
||||
- **Endpoint** - broker host and port.
|
||||
- **Client ID** - device identifier.
|
||||
- **Username / Password** - credentials.
|
||||
- **Keep Alive** - heartbeat interval (default 240 s).
|
||||
|
||||
### 3.2 Hello 消息交换
|
||||
### 3.2 Hello exchange
|
||||
|
||||
#### 3.2.1 设备端发送 Hello
|
||||
#### 3.2.1 Device -> Server
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -78,7 +79,15 @@ sequenceDiagram
|
||||
"version": 3,
|
||||
"transport": "udp",
|
||||
"features": {
|
||||
"mcp": true
|
||||
"mcp": true,
|
||||
"aec": true,
|
||||
"glyph_push": true
|
||||
},
|
||||
"text_font": {
|
||||
"bundle": "noto-v1",
|
||||
"charset": "common",
|
||||
"size": 20,
|
||||
"bpp": 4
|
||||
},
|
||||
"audio_params": {
|
||||
"format": "opus",
|
||||
@@ -89,7 +98,11 @@ sequenceDiagram
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2.2 服务器响应 Hello
|
||||
`features.mcp` is always set; `features.aec` is set when `CONFIG_USE_SERVER_AEC` is enabled.
|
||||
`features.glyph_push` and `text_font` advertise the shared dynamic text-glyph extension described in
|
||||
[Dynamic Text Glyph Push Extension](glyph-push.md).
|
||||
|
||||
#### 3.2.2 Server -> Device
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -111,17 +124,17 @@ sequenceDiagram
|
||||
}
|
||||
```
|
||||
|
||||
**字段说明:**
|
||||
- `udp.server`:UDP 服务器地址
|
||||
- `udp.port`:UDP 服务器端口
|
||||
- `udp.key`:AES 加密密钥(十六进制字符串)
|
||||
- `udp.nonce`:AES 加密随机数(十六进制字符串)
|
||||
Field reference:
|
||||
- `udp.server` - UDP server address.
|
||||
- `udp.port` - UDP server port.
|
||||
- `udp.key` - AES key, hex-encoded.
|
||||
- `udp.nonce` - AES nonce, hex-encoded.
|
||||
|
||||
### 3.3 JSON 消息类型
|
||||
### 3.3 JSON message types
|
||||
|
||||
#### 3.3.1 设备端→服务器
|
||||
#### 3.3.1 Device -> Server
|
||||
|
||||
1. **Listen 消息**
|
||||
1. **Listen**
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
@@ -131,7 +144,7 @@ sequenceDiagram
|
||||
}
|
||||
```
|
||||
|
||||
2. **Abort 消息**
|
||||
2. **Abort**
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
@@ -140,7 +153,7 @@ sequenceDiagram
|
||||
}
|
||||
```
|
||||
|
||||
3. **MCP 消息**
|
||||
3. **MCP**
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
@@ -148,12 +161,12 @@ sequenceDiagram
|
||||
"payload": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {...}
|
||||
"result": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. **Goodbye 消息**
|
||||
4. **Goodbye**
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
@@ -161,71 +174,84 @@ sequenceDiagram
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.3.2 服务器→设备端
|
||||
#### 3.3.2 Server -> Device
|
||||
|
||||
支持的消息类型与 WebSocket 协议一致,包括:
|
||||
- **STT**:语音识别结果
|
||||
- **TTS**:语音合成控制
|
||||
- **LLM**:情感表达控制
|
||||
- **MCP**:物联网控制
|
||||
- **System**:系统控制
|
||||
- **Custom**:自定义消息(可选)
|
||||
Semantics match the WebSocket protocol. Supported types:
|
||||
- **STT** - speech recognition result.
|
||||
- **TTS** - TTS lifecycle (`start`, `stop`, `sentence_start`).
|
||||
- **LLM** - emotion update for the UI.
|
||||
- **MCP** - IoT control.
|
||||
- **System** - system control, e.g. `"command": "reboot"`.
|
||||
- **Alert** - show an alert on the UI; fields: `status`, `message`, `emotion`.
|
||||
- **Goodbye** - server-initiated shutdown of the audio session. The device responds by closing the UDP channel without sending its own goodbye.
|
||||
- **Custom** (optional, enabled via `CONFIG_RECEIVE_CUSTOM_MESSAGE`).
|
||||
|
||||
Example alert:
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
"type": "alert",
|
||||
"status": "Warning",
|
||||
"message": "Battery low",
|
||||
"emotion": "sad"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. UDP 音频通道
|
||||
## 4. UDP Audio Channel
|
||||
|
||||
### 4.1 连接建立
|
||||
### 4.1 Establishing the channel
|
||||
|
||||
设备收到 MQTT Hello 响应后,使用其中的 UDP 连接信息建立音频通道:
|
||||
1. 解析 UDP 服务器地址和端口
|
||||
2. 解析加密密钥和随机数
|
||||
3. 初始化 AES-CTR 加密上下文
|
||||
4. 建立 UDP 连接
|
||||
After the device receives the MQTT hello response, it:
|
||||
1. Parses the UDP host and port.
|
||||
2. Parses the AES key and nonce.
|
||||
3. Initializes the AES-CTR context.
|
||||
4. Opens the UDP socket.
|
||||
|
||||
### 4.2 音频数据格式
|
||||
### 4.2 Audio packet format
|
||||
|
||||
#### 4.2.1 加密音频包结构
|
||||
#### 4.2.1 Encrypted audio packet
|
||||
|
||||
```
|
||||
|type 1byte|flags 1byte|payload_len 2bytes|ssrc 4bytes|timestamp 4bytes|sequence 4bytes|
|
||||
|type 1B|flags 1B|payload_len 2B|ssrc 4B|timestamp 4B|sequence 4B|
|
||||
|payload payload_len bytes|
|
||||
```
|
||||
|
||||
**字段说明:**
|
||||
- `type`:数据包类型,固定为 0x01
|
||||
- `flags`:标志位,当前未使用
|
||||
- `payload_len`:负载长度(网络字节序)
|
||||
- `ssrc`:同步源标识符
|
||||
- `timestamp`:时间戳(网络字节序)
|
||||
- `sequence`:序列号(网络字节序)
|
||||
- `payload`:加密的 Opus 音频数据
|
||||
Field reference:
|
||||
- `type`: packet type, always `0x01`.
|
||||
- `flags`: flags, currently unused.
|
||||
- `payload_len`: payload length (network byte order).
|
||||
- `ssrc`: synchronization source identifier.
|
||||
- `timestamp`: timestamp (network byte order).
|
||||
- `sequence`: sequence number (network byte order).
|
||||
- `payload`: encrypted Opus audio data.
|
||||
|
||||
#### 4.2.2 加密算法
|
||||
#### 4.2.2 Encryption
|
||||
|
||||
使用 **AES-CTR** 模式加密:
|
||||
- **密钥**:128位,由服务器提供
|
||||
- **随机数**:128位,由服务器提供
|
||||
- **计数器**:包含时间戳和序列号信息
|
||||
Uses **AES-CTR** with:
|
||||
- **Key**: 128-bit, provided by the server.
|
||||
- **Nonce**: 128-bit, provided by the server.
|
||||
- **Counter**: built from the timestamp and sequence number.
|
||||
|
||||
### 4.3 序列号管理
|
||||
### 4.3 Sequence number management
|
||||
|
||||
- **发送端**:`local_sequence_` 单调递增
|
||||
- **接收端**:`remote_sequence_` 验证连续性
|
||||
- **防重放**:拒绝序列号小于期望值的数据包
|
||||
- **容错处理**:允许轻微的序列号跳跃,记录警告
|
||||
- **Sender**: `local_sequence_` is incremented monotonically.
|
||||
- **Receiver**: `remote_sequence_` validates continuity.
|
||||
- **Anti-replay**: packets with sequence numbers below the expected value are dropped.
|
||||
- **Tolerance**: small gaps are logged as warnings but still accepted.
|
||||
|
||||
### 4.4 错误处理
|
||||
### 4.4 Error handling
|
||||
|
||||
1. **解密失败**:记录错误,丢弃数据包
|
||||
2. **序列号异常**:记录警告,但仍处理数据包
|
||||
3. **数据包格式错误**:记录错误,丢弃数据包
|
||||
1. **Decryption failure** - log an error and drop the packet.
|
||||
2. **Sequence gap** - log a warning, continue processing the packet.
|
||||
3. **Malformed packet** - log an error and drop.
|
||||
|
||||
---
|
||||
|
||||
## 5. 状态管理
|
||||
## 5. State Management
|
||||
|
||||
### 5.1 连接状态
|
||||
### 5.1 Connection state
|
||||
|
||||
```mermaid
|
||||
stateDiagram
|
||||
@@ -233,21 +259,21 @@ stateDiagram
|
||||
[*] --> Disconnected
|
||||
Disconnected --> MqttConnecting: StartMqttClient()
|
||||
MqttConnecting --> MqttConnected: MQTT Connected
|
||||
MqttConnecting --> Disconnected: Connect Failed
|
||||
MqttConnecting --> Disconnected: Connect failed
|
||||
MqttConnected --> RequestingChannel: OpenAudioChannel()
|
||||
RequestingChannel --> ChannelOpened: Hello Exchange Success
|
||||
RequestingChannel --> MqttConnected: Hello Timeout/Failed
|
||||
ChannelOpened --> UdpConnected: UDP Connect Success
|
||||
UdpConnected --> AudioStreaming: Start Audio Transfer
|
||||
AudioStreaming --> UdpConnected: Stop Audio Transfer
|
||||
UdpConnected --> ChannelOpened: UDP Disconnect
|
||||
RequestingChannel --> ChannelOpened: Hello exchange success
|
||||
RequestingChannel --> MqttConnected: Hello timeout / failed
|
||||
ChannelOpened --> UdpConnected: UDP connect success
|
||||
UdpConnected --> AudioStreaming: Start audio
|
||||
AudioStreaming --> UdpConnected: Stop audio
|
||||
UdpConnected --> ChannelOpened: UDP disconnect
|
||||
ChannelOpened --> MqttConnected: CloseAudioChannel()
|
||||
MqttConnected --> Disconnected: MQTT Disconnect
|
||||
MqttConnected --> Disconnected: MQTT disconnect
|
||||
```
|
||||
|
||||
### 5.2 状态检查
|
||||
### 5.2 State check
|
||||
|
||||
设备通过以下条件判断音频通道是否可用:
|
||||
The device determines whether the audio channel is available with:
|
||||
```cpp
|
||||
bool IsAudioChannelOpened() const {
|
||||
return udp_ != nullptr && !error_occurred_ && !IsTimeout();
|
||||
@@ -256,138 +282,137 @@ bool IsAudioChannelOpened() const {
|
||||
|
||||
---
|
||||
|
||||
## 6. 配置参数
|
||||
## 6. Configuration Parameters
|
||||
|
||||
### 6.1 MQTT 配置
|
||||
### 6.1 MQTT settings
|
||||
|
||||
从设置中读取的配置项:
|
||||
- `endpoint`:MQTT 服务器地址
|
||||
- `client_id`:客户端标识符
|
||||
- `username`:用户名
|
||||
- `password`:密码
|
||||
- `keepalive`:心跳间隔(默认240秒)
|
||||
- `publish_topic`:发布主题
|
||||
Read from storage:
|
||||
- `endpoint` - broker address.
|
||||
- `client_id` - client identifier.
|
||||
- `username` - user name.
|
||||
- `password` - password.
|
||||
- `keepalive` - keep-alive interval (default 240 s).
|
||||
- `publish_topic` - publish topic.
|
||||
|
||||
### 6.2 音频参数
|
||||
### 6.2 Audio parameters
|
||||
|
||||
- **格式**:Opus
|
||||
- **采样率**:16000 Hz(设备端)/ 24000 Hz(服务器端)
|
||||
- **声道数**:1(单声道)
|
||||
- **帧时长**:60ms
|
||||
- **Format**: Opus
|
||||
- **Sample rate**: 16 kHz device / 24 kHz server
|
||||
- **Channels**: 1 (mono)
|
||||
- **Frame duration**: 60 ms
|
||||
|
||||
---
|
||||
|
||||
## 7. 错误处理与重连
|
||||
## 7. Error Handling and Reconnection
|
||||
|
||||
### 7.1 MQTT 重连机制
|
||||
### 7.1 MQTT reconnect
|
||||
|
||||
- 连接失败时自动重试
|
||||
- 支持错误上报控制
|
||||
- 断线时触发清理流程
|
||||
- Automatic retry on connect failure.
|
||||
- Optional error reporting.
|
||||
- Clean-up runs on disconnect.
|
||||
|
||||
### 7.2 UDP 连接管理
|
||||
### 7.2 UDP connection
|
||||
|
||||
- 连接失败时不自动重试
|
||||
- 依赖 MQTT 通道重新协商
|
||||
- 支持连接状态查询
|
||||
- No automatic retry; depends on re-negotiation via MQTT.
|
||||
- Status can be queried at any time.
|
||||
|
||||
### 7.3 超时处理
|
||||
### 7.3 Timeouts
|
||||
|
||||
基类 `Protocol` 提供超时检测:
|
||||
- 默认超时时间:120 秒
|
||||
- 基于最后接收时间计算
|
||||
- 超时时自动标记为不可用
|
||||
The base `Protocol` class provides timeout detection:
|
||||
- Default timeout: 120 s.
|
||||
- Based on the time since the last incoming packet.
|
||||
- After a timeout the channel is marked unavailable.
|
||||
|
||||
---
|
||||
|
||||
## 8. 安全考虑
|
||||
## 8. Security
|
||||
|
||||
### 8.1 传输加密
|
||||
### 8.1 Transport encryption
|
||||
|
||||
- **MQTT**:支持 TLS/SSL 加密(端口8883)
|
||||
- **UDP**:使用 AES-CTR 加密音频数据
|
||||
- **MQTT**: supports TLS/SSL (port 8883).
|
||||
- **UDP**: AES-CTR on audio payloads.
|
||||
|
||||
### 8.2 认证机制
|
||||
### 8.2 Authentication
|
||||
|
||||
- **MQTT**:用户名/密码认证
|
||||
- **UDP**:通过 MQTT 通道分发密钥
|
||||
- **MQTT**: user name / password.
|
||||
- **UDP**: keys are distributed via the MQTT channel.
|
||||
|
||||
### 8.3 防重放攻击
|
||||
### 8.3 Anti-replay
|
||||
|
||||
- 序列号单调递增
|
||||
- 拒绝过期数据包
|
||||
- 时间戳验证
|
||||
- Monotonically increasing sequence numbers.
|
||||
- Stale packets are dropped.
|
||||
- Timestamps are validated.
|
||||
|
||||
---
|
||||
|
||||
## 9. 性能优化
|
||||
## 9. Performance Notes
|
||||
|
||||
### 9.1 并发控制
|
||||
### 9.1 Concurrency
|
||||
|
||||
使用互斥锁保护 UDP 连接:
|
||||
A mutex protects the UDP connection:
|
||||
```cpp
|
||||
std::lock_guard<std::mutex> lock(channel_mutex_);
|
||||
```
|
||||
|
||||
### 9.2 内存管理
|
||||
### 9.2 Memory management
|
||||
|
||||
- 动态创建/销毁网络对象
|
||||
- 智能指针管理音频数据包
|
||||
- 及时释放加密上下文
|
||||
- Network objects are created and destroyed dynamically.
|
||||
- Audio packets are managed with smart pointers.
|
||||
- Encryption contexts are released promptly.
|
||||
|
||||
### 9.3 网络优化
|
||||
### 9.3 Network optimizations
|
||||
|
||||
- UDP 连接复用
|
||||
- 数据包大小优化
|
||||
- 序列号连续性检查
|
||||
- UDP connection reuse.
|
||||
- Reasonable packet sizes.
|
||||
- Sequence continuity checks.
|
||||
|
||||
---
|
||||
|
||||
## 10. 与 WebSocket 协议的比较
|
||||
## 10. Comparison with WebSocket
|
||||
|
||||
| 特性 | MQTT + UDP | WebSocket |
|
||||
|------|------------|-----------|
|
||||
| 控制通道 | MQTT | WebSocket |
|
||||
| 音频通道 | UDP (加密) | WebSocket (二进制) |
|
||||
| 实时性 | 高 (UDP) | 中等 |
|
||||
| 可靠性 | 中等 | 高 |
|
||||
| 复杂度 | 高 | 低 |
|
||||
| 加密 | AES-CTR | TLS |
|
||||
| 防火墙友好度 | 低 | 高 |
|
||||
| Feature | MQTT + UDP | WebSocket |
|
||||
|---------|------------|-----------|
|
||||
| Control channel | MQTT | WebSocket |
|
||||
| Audio channel | UDP (encrypted) | WebSocket (binary) |
|
||||
| Latency | Low (UDP) | Medium |
|
||||
| Reliability | Medium | High |
|
||||
| Complexity | High | Low |
|
||||
| Encryption | AES-CTR | TLS |
|
||||
| Firewall friendliness | Low | High |
|
||||
|
||||
---
|
||||
|
||||
## 11. 部署建议
|
||||
## 11. Deployment Notes
|
||||
|
||||
### 11.1 网络环境
|
||||
### 11.1 Network
|
||||
|
||||
- 确保 UDP 端口可达
|
||||
- 配置防火墙规则
|
||||
- 考虑 NAT 穿透
|
||||
- Ensure UDP ports are reachable.
|
||||
- Configure firewall rules accordingly.
|
||||
- Plan for NAT traversal if needed.
|
||||
|
||||
### 11.2 服务器配置
|
||||
### 11.2 Server infrastructure
|
||||
|
||||
- MQTT Broker 配置
|
||||
- UDP 服务器部署
|
||||
- 密钥管理系统
|
||||
- MQTT broker configuration.
|
||||
- UDP server deployment.
|
||||
- Key management.
|
||||
|
||||
### 11.3 监控指标
|
||||
### 11.3 Monitoring
|
||||
|
||||
- 连接成功率
|
||||
- 音频传输延迟
|
||||
- 数据包丢失率
|
||||
- 解密失败率
|
||||
- Connection success rate.
|
||||
- Audio transmission latency.
|
||||
- Packet loss.
|
||||
- Decryption failures.
|
||||
|
||||
---
|
||||
|
||||
## 12. 总结
|
||||
## 12. Summary
|
||||
|
||||
MQTT + UDP 混合协议通过以下设计实现高效的音视频通信:
|
||||
The MQTT + UDP hybrid protocol achieves efficient audio communication through:
|
||||
|
||||
- **分离式架构**:控制与数据通道分离,各司其职
|
||||
- **加密保护**:AES-CTR 确保音频数据安全传输
|
||||
- **序列化管理**:防止重放攻击和数据乱序
|
||||
- **自动恢复**:支持连接断开后的自动重连
|
||||
- **性能优化**:UDP 传输保证音频数据的实时性
|
||||
- **Split architecture** - separate control and data channels with clear responsibilities.
|
||||
- **Encryption** - AES-CTR protects audio payloads.
|
||||
- **Sequence management** - prevents replay and reordering.
|
||||
- **Automatic recovery** - MQTT reconnects on failure.
|
||||
- **Performance** - UDP keeps audio latency low.
|
||||
|
||||
该协议适用于对实时性要求较高的语音交互场景,但需要在网络复杂度和传输性能之间做出权衡。
|
||||
The protocol is a good fit for low-latency voice interaction, at the cost of higher network complexity than pure WebSocket.
|
||||
|
||||
@@ -0,0 +1,403 @@
|
||||
# MQTT + UDP 混合通信协议文档
|
||||
|
||||
基于代码实现整理的 MQTT + UDP 混合通信协议文档,概述设备端与服务器之间如何通过 MQTT 进行控制消息传输,通过 UDP 进行音频数据传输的交互方式。
|
||||
|
||||
---
|
||||
|
||||
## 1. 协议概览
|
||||
|
||||
本协议采用混合传输方式:
|
||||
- **MQTT**:用于控制消息、状态同步、JSON 数据交换
|
||||
- **UDP**:用于实时音频数据传输,支持加密
|
||||
|
||||
### 1.1 协议特点
|
||||
|
||||
- **双通道设计**:控制与数据分离,确保实时性
|
||||
- **加密传输**:UDP 音频数据使用 AES-CTR 加密
|
||||
- **序列号保护**:防止数据包重放和乱序
|
||||
- **自动重连**:MQTT 连接断开时自动重连
|
||||
|
||||
---
|
||||
|
||||
## 2. 总体流程概览
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Device as ESP32 设备
|
||||
participant MQTT as MQTT 服务器
|
||||
participant UDP as UDP 服务器
|
||||
|
||||
Note over Device, UDP: 1. 建立 MQTT 连接
|
||||
Device->>MQTT: MQTT Connect
|
||||
MQTT->>Device: Connected
|
||||
|
||||
Note over Device, UDP: 2. 请求音频通道
|
||||
Device->>MQTT: Hello Message (type: "hello", transport: "udp")
|
||||
MQTT->>Device: Hello Response (UDP 连接信息 + 加密密钥)
|
||||
|
||||
Note over Device, UDP: 3. 建立 UDP 连接
|
||||
Device->>UDP: UDP Connect
|
||||
UDP->>Device: Connected
|
||||
|
||||
Note over Device, UDP: 4. 音频数据传输
|
||||
loop 音频流传输
|
||||
Device->>UDP: 加密音频数据 (Opus)
|
||||
UDP->>Device: 加密音频数据 (Opus)
|
||||
end
|
||||
|
||||
Note over Device, UDP: 5. 控制消息交换
|
||||
par 控制消息
|
||||
Device->>MQTT: Listen/TTS/MCP 消息
|
||||
MQTT->>Device: STT/TTS/MCP 响应
|
||||
end
|
||||
|
||||
Note over Device, UDP: 6. 关闭连接
|
||||
Device->>MQTT: Goodbye Message
|
||||
Device->>UDP: Disconnect
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. MQTT 控制通道
|
||||
|
||||
### 3.1 连接建立
|
||||
|
||||
设备通过 MQTT 连接到服务器,连接参数包括:
|
||||
- **Endpoint**:MQTT 服务器地址和端口
|
||||
- **Client ID**:设备唯一标识符
|
||||
- **Username/Password**:认证凭据
|
||||
- **Keep Alive**:心跳间隔(默认240秒)
|
||||
|
||||
### 3.2 Hello 消息交换
|
||||
|
||||
#### 3.2.1 设备端发送 Hello
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "hello",
|
||||
"version": 3,
|
||||
"transport": "udp",
|
||||
"features": {
|
||||
"mcp": true,
|
||||
"glyph_push": true
|
||||
},
|
||||
"text_font": {
|
||||
"bundle": "noto-v1",
|
||||
"charset": "common",
|
||||
"size": 20,
|
||||
"bpp": 4
|
||||
},
|
||||
"audio_params": {
|
||||
"format": "opus",
|
||||
"sample_rate": 16000,
|
||||
"channels": 1,
|
||||
"frame_duration": 60
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`features.glyph_push` 和 `text_font` 声明 WebSocket 与 MQTT/UDP 共用的动态文字扩展,详见
|
||||
[动态文字 Glyph Push 扩展](glyph-push_zh.md)。
|
||||
|
||||
#### 3.2.2 服务器响应 Hello
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "hello",
|
||||
"transport": "udp",
|
||||
"session_id": "xxx",
|
||||
"audio_params": {
|
||||
"format": "opus",
|
||||
"sample_rate": 24000,
|
||||
"channels": 1,
|
||||
"frame_duration": 60
|
||||
},
|
||||
"udp": {
|
||||
"server": "192.168.1.100",
|
||||
"port": 8888,
|
||||
"key": "0123456789ABCDEF0123456789ABCDEF",
|
||||
"nonce": "0123456789ABCDEF0123456789ABCDEF"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**字段说明:**
|
||||
- `udp.server`:UDP 服务器地址
|
||||
- `udp.port`:UDP 服务器端口
|
||||
- `udp.key`:AES 加密密钥(十六进制字符串)
|
||||
- `udp.nonce`:AES 加密随机数(十六进制字符串)
|
||||
|
||||
### 3.3 JSON 消息类型
|
||||
|
||||
#### 3.3.1 设备端→服务器
|
||||
|
||||
1. **Listen 消息**
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
"type": "listen",
|
||||
"state": "start",
|
||||
"mode": "manual"
|
||||
}
|
||||
```
|
||||
|
||||
2. **Abort 消息**
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
"type": "abort",
|
||||
"reason": "wake_word_detected"
|
||||
}
|
||||
```
|
||||
|
||||
3. **MCP 消息**
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
"type": "mcp",
|
||||
"payload": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {...}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. **Goodbye 消息**
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
"type": "goodbye"
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.3.2 服务器→设备端
|
||||
|
||||
支持的消息类型与 WebSocket 协议一致,包括:
|
||||
- **STT**:语音识别结果
|
||||
- **TTS**:语音合成控制
|
||||
- **LLM**:情感表达控制
|
||||
- **MCP**:物联网控制
|
||||
- **System**:系统控制
|
||||
- **Custom**:自定义消息(可选)
|
||||
|
||||
---
|
||||
|
||||
## 4. UDP 音频通道
|
||||
|
||||
### 4.1 连接建立
|
||||
|
||||
设备收到 MQTT Hello 响应后,使用其中的 UDP 连接信息建立音频通道:
|
||||
1. 解析 UDP 服务器地址和端口
|
||||
2. 解析加密密钥和随机数
|
||||
3. 初始化 AES-CTR 加密上下文
|
||||
4. 建立 UDP 连接
|
||||
|
||||
### 4.2 音频数据格式
|
||||
|
||||
#### 4.2.1 加密音频包结构
|
||||
|
||||
```
|
||||
|type 1byte|flags 1byte|payload_len 2bytes|ssrc 4bytes|timestamp 4bytes|sequence 4bytes|
|
||||
|payload payload_len bytes|
|
||||
```
|
||||
|
||||
**字段说明:**
|
||||
- `type`:数据包类型,固定为 0x01
|
||||
- `flags`:标志位,当前未使用
|
||||
- `payload_len`:负载长度(网络字节序)
|
||||
- `ssrc`:同步源标识符
|
||||
- `timestamp`:时间戳(网络字节序)
|
||||
- `sequence`:序列号(网络字节序)
|
||||
- `payload`:加密的 Opus 音频数据
|
||||
|
||||
#### 4.2.2 加密算法
|
||||
|
||||
使用 **AES-CTR** 模式加密:
|
||||
- **密钥**:128位,由服务器提供
|
||||
- **随机数**:128位,由服务器提供
|
||||
- **计数器**:包含时间戳和序列号信息
|
||||
|
||||
### 4.3 序列号管理
|
||||
|
||||
- **发送端**:`local_sequence_` 单调递增
|
||||
- **接收端**:`remote_sequence_` 验证连续性
|
||||
- **防重放**:拒绝序列号小于期望值的数据包
|
||||
- **容错处理**:允许轻微的序列号跳跃,记录警告
|
||||
|
||||
### 4.4 错误处理
|
||||
|
||||
1. **解密失败**:记录错误,丢弃数据包
|
||||
2. **序列号异常**:记录警告,但仍处理数据包
|
||||
3. **数据包格式错误**:记录错误,丢弃数据包
|
||||
|
||||
---
|
||||
|
||||
## 5. 状态管理
|
||||
|
||||
### 5.1 连接状态
|
||||
|
||||
```mermaid
|
||||
stateDiagram
|
||||
direction TB
|
||||
[*] --> Disconnected
|
||||
Disconnected --> MqttConnecting: StartMqttClient()
|
||||
MqttConnecting --> MqttConnected: MQTT Connected
|
||||
MqttConnecting --> Disconnected: Connect Failed
|
||||
MqttConnected --> RequestingChannel: OpenAudioChannel()
|
||||
RequestingChannel --> ChannelOpened: Hello Exchange Success
|
||||
RequestingChannel --> MqttConnected: Hello Timeout/Failed
|
||||
ChannelOpened --> UdpConnected: UDP Connect Success
|
||||
UdpConnected --> AudioStreaming: Start Audio Transfer
|
||||
AudioStreaming --> UdpConnected: Stop Audio Transfer
|
||||
UdpConnected --> ChannelOpened: UDP Disconnect
|
||||
ChannelOpened --> MqttConnected: CloseAudioChannel()
|
||||
MqttConnected --> Disconnected: MQTT Disconnect
|
||||
```
|
||||
|
||||
### 5.2 状态检查
|
||||
|
||||
设备通过以下条件判断音频通道是否可用:
|
||||
```cpp
|
||||
bool IsAudioChannelOpened() const {
|
||||
return udp_ != nullptr && !error_occurred_ && !IsTimeout();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 配置参数
|
||||
|
||||
### 6.1 MQTT 配置
|
||||
|
||||
从设置中读取的配置项:
|
||||
- `endpoint`:MQTT 服务器地址
|
||||
- `client_id`:客户端标识符
|
||||
- `username`:用户名
|
||||
- `password`:密码
|
||||
- `keepalive`:心跳间隔(默认240秒)
|
||||
- `publish_topic`:发布主题
|
||||
|
||||
### 6.2 音频参数
|
||||
|
||||
- **格式**:Opus
|
||||
- **采样率**:16000 Hz(设备端)/ 24000 Hz(服务器端)
|
||||
- **声道数**:1(单声道)
|
||||
- **帧时长**:60ms
|
||||
|
||||
---
|
||||
|
||||
## 7. 错误处理与重连
|
||||
|
||||
### 7.1 MQTT 重连机制
|
||||
|
||||
- 连接失败时自动重试
|
||||
- 支持错误上报控制
|
||||
- 断线时触发清理流程
|
||||
|
||||
### 7.2 UDP 连接管理
|
||||
|
||||
- 连接失败时不自动重试
|
||||
- 依赖 MQTT 通道重新协商
|
||||
- 支持连接状态查询
|
||||
|
||||
### 7.3 超时处理
|
||||
|
||||
基类 `Protocol` 提供超时检测:
|
||||
- 默认超时时间:120 秒
|
||||
- 基于最后接收时间计算
|
||||
- 超时时自动标记为不可用
|
||||
|
||||
---
|
||||
|
||||
## 8. 安全考虑
|
||||
|
||||
### 8.1 传输加密
|
||||
|
||||
- **MQTT**:支持 TLS/SSL 加密(端口8883)
|
||||
- **UDP**:使用 AES-CTR 加密音频数据
|
||||
|
||||
### 8.2 认证机制
|
||||
|
||||
- **MQTT**:用户名/密码认证
|
||||
- **UDP**:通过 MQTT 通道分发密钥
|
||||
|
||||
### 8.3 防重放攻击
|
||||
|
||||
- 序列号单调递增
|
||||
- 拒绝过期数据包
|
||||
- 时间戳验证
|
||||
|
||||
---
|
||||
|
||||
## 9. 性能优化
|
||||
|
||||
### 9.1 并发控制
|
||||
|
||||
使用互斥锁保护 UDP 连接:
|
||||
```cpp
|
||||
std::lock_guard<std::mutex> lock(channel_mutex_);
|
||||
```
|
||||
|
||||
### 9.2 内存管理
|
||||
|
||||
- 动态创建/销毁网络对象
|
||||
- 智能指针管理音频数据包
|
||||
- 及时释放加密上下文
|
||||
|
||||
### 9.3 网络优化
|
||||
|
||||
- UDP 连接复用
|
||||
- 数据包大小优化
|
||||
- 序列号连续性检查
|
||||
|
||||
---
|
||||
|
||||
## 10. 与 WebSocket 协议的比较
|
||||
|
||||
| 特性 | MQTT + UDP | WebSocket |
|
||||
|------|------------|-----------|
|
||||
| 控制通道 | MQTT | WebSocket |
|
||||
| 音频通道 | UDP (加密) | WebSocket (二进制) |
|
||||
| 实时性 | 高 (UDP) | 中等 |
|
||||
| 可靠性 | 中等 | 高 |
|
||||
| 复杂度 | 高 | 低 |
|
||||
| 加密 | AES-CTR | TLS |
|
||||
| 防火墙友好度 | 低 | 高 |
|
||||
|
||||
---
|
||||
|
||||
## 11. 部署建议
|
||||
|
||||
### 11.1 网络环境
|
||||
|
||||
- 确保 UDP 端口可达
|
||||
- 配置防火墙规则
|
||||
- 考虑 NAT 穿透
|
||||
|
||||
### 11.2 服务器配置
|
||||
|
||||
- MQTT Broker 配置
|
||||
- UDP 服务器部署
|
||||
- 密钥管理系统
|
||||
|
||||
### 11.3 监控指标
|
||||
|
||||
- 连接成功率
|
||||
- 音频传输延迟
|
||||
- 数据包丢失率
|
||||
- 解密失败率
|
||||
|
||||
---
|
||||
|
||||
## 12. 总结
|
||||
|
||||
MQTT + UDP 混合协议通过以下设计实现高效的音视频通信:
|
||||
|
||||
- **分离式架构**:控制与数据通道分离,各司其职
|
||||
- **加密保护**:AES-CTR 确保音频数据安全传输
|
||||
- **序列化管理**:防止重放攻击和数据乱序
|
||||
- **自动恢复**:支持连接断开后的自动重连
|
||||
- **性能优化**:UDP 传输保证音频数据的实时性
|
||||
|
||||
该协议适用于对实时性要求较高的语音交互场景,但需要在网络复杂度和传输性能之间做出权衡。
|
||||
+256
-213
@@ -1,32 +1,40 @@
|
||||
以下是一份基于代码实现整理的 WebSocket 通信协议文档,概述设备端与服务器之间如何通过 WebSocket 进行交互。
|
||||
# WebSocket Communication Protocol
|
||||
|
||||
该文档仅基于所提供的代码推断,实际部署时可能需要结合服务器端实现进行进一步确认或补充。
|
||||
This document describes the WebSocket communication protocol between the device and the server, based on the current code. When implementing a server, please cross-check with the actual implementation.
|
||||
|
||||
---
|
||||
|
||||
## 1. 总体流程概览
|
||||
## 1. Overall Flow
|
||||
|
||||
1. **设备端初始化**
|
||||
- 设备上电、初始化 `Application`:
|
||||
- 初始化音频编解码器、显示屏、LED 等
|
||||
- 连接网络
|
||||
- 创建并初始化实现 `Protocol` 接口的 WebSocket 协议实例(`WebsocketProtocol`)
|
||||
- 进入主循环等待事件(音频输入、音频输出、调度任务等)。
|
||||
1. **Device initialization**
|
||||
- The device boots and initializes `Application`:
|
||||
- Initializes the audio codec, display, LEDs, etc.
|
||||
- Connects to the network.
|
||||
- Creates a WebSocket protocol instance (`WebsocketProtocol`) that implements the `Protocol` interface.
|
||||
- Enters the main loop and waits for events (audio input, audio output, scheduled tasks, etc.).
|
||||
|
||||
2. **建立 WebSocket 连接**
|
||||
- 当设备需要开始语音会话时(例如用户唤醒、手动按键触发等),调用 `OpenAudioChannel()`:
|
||||
- 根据配置获取 WebSocket URL
|
||||
- 设置若干请求头(`Authorization`, `Protocol-Version`, `Device-Id`, `Client-Id`)
|
||||
- 调用 `Connect()` 与服务器建立 WebSocket 连接
|
||||
2. **Opening the WebSocket connection**
|
||||
- When the device needs to start a voice session (wake-up, button press, etc.), it calls `OpenAudioChannel()`:
|
||||
- Reads the WebSocket URL from settings.
|
||||
- Sets the request headers (`Authorization`, `Protocol-Version`, `Device-Id`, `Client-Id`).
|
||||
- Calls `Connect()` to establish the WebSocket connection.
|
||||
|
||||
3. **设备端发送 "hello" 消息**
|
||||
- 连接成功后,设备会发送一条 JSON 消息,示例结构如下:
|
||||
3. **Device sends a "hello" message**
|
||||
- Once connected, the device sends a JSON message. Example:
|
||||
```json
|
||||
{
|
||||
"type": "hello",
|
||||
"version": 1,
|
||||
"features": {
|
||||
"mcp": true
|
||||
"mcp": true,
|
||||
"aec": true,
|
||||
"glyph_push": true
|
||||
},
|
||||
"text_font": {
|
||||
"bundle": "noto-v1",
|
||||
"charset": "common",
|
||||
"size": 20,
|
||||
"bpp": 4
|
||||
},
|
||||
"transport": "websocket",
|
||||
"audio_params": {
|
||||
@@ -37,13 +45,14 @@
|
||||
}
|
||||
}
|
||||
```
|
||||
- 其中 `features` 字段为可选,内容根据设备编译配置自动生成。例如:`"mcp": true` 表示支持 MCP 协议。
|
||||
- `frame_duration` 的值对应 `OPUS_FRAME_DURATION_MS`(例如 60ms)。
|
||||
- `features` is optional and generated from compile-time configuration. For example, `"mcp": true` means the device supports MCP, and `"aec": true` is emitted when `CONFIG_USE_SERVER_AEC` is enabled.
|
||||
- `"glyph_push": true` and `text_font` advertise the optional dynamic text-glyph extension. See [Dynamic Text Glyph Push Extension](glyph-push.md).
|
||||
- `frame_duration` matches `OPUS_FRAME_DURATION_MS` (typically 60 ms).
|
||||
|
||||
4. **服务器回复 "hello"**
|
||||
- 设备等待服务器返回一条包含 `"type": "hello"` 的 JSON 消息,并检查 `"transport": "websocket"` 是否匹配。
|
||||
- 服务器可选下发 `session_id` 字段,设备端收到后会自动记录。
|
||||
- 示例:
|
||||
4. **Server replies with "hello"**
|
||||
- The device waits for a JSON message whose `"type"` is `"hello"` and whose `"transport"` is `"websocket"`.
|
||||
- The server may include a `session_id`; the device will store it.
|
||||
- Example:
|
||||
```json
|
||||
{
|
||||
"type": "hello",
|
||||
@@ -57,89 +66,90 @@
|
||||
}
|
||||
}
|
||||
```
|
||||
- 如果匹配,则认为服务器已就绪,标记音频通道打开成功。
|
||||
- 如果在超时时间(默认 10 秒)内未收到正确回复,认为连接失败并触发网络错误回调。
|
||||
- If `transport` matches, the device marks the audio channel as opened.
|
||||
- If no valid hello arrives within the timeout (default 10 seconds), the connection is considered failed and the network error callback is fired.
|
||||
|
||||
5. **后续消息交互**
|
||||
- 设备端和服务器端之间可发送两种主要类型的数据:
|
||||
1. **二进制音频数据**(Opus 编码)
|
||||
2. **文本 JSON 消息**(用于传输聊天状态、TTS/STT 事件、MCP 协议消息等)
|
||||
5. **Subsequent exchanges**
|
||||
- Two kinds of data are sent in either direction:
|
||||
1. **Binary audio data** (Opus encoded)
|
||||
2. **Text JSON messages** (chat state, TTS/STT events, MCP messages, etc.)
|
||||
|
||||
- 在代码里,接收回调主要分为:
|
||||
- `OnData(...)`:
|
||||
- 当 `binary` 为 `true` 时,认为是音频帧;设备会将其当作 Opus 数据进行解码。
|
||||
- 当 `binary` 为 `false` 时,认为是 JSON 文本,需要在设备端用 cJSON 进行解析并做相应业务逻辑处理(如聊天、TTS、MCP 协议消息等)。
|
||||
- In the code, the receive callback splits traffic as follows:
|
||||
- `OnData(...)`:
|
||||
- If `binary` is `true`, the payload is treated as an Opus frame and decoded.
|
||||
- If `binary` is `false`, the payload is parsed as JSON and dispatched by `type`.
|
||||
|
||||
- 当服务器或网络出现断连,回调 `OnDisconnected()` 被触发:
|
||||
- 设备会调用 `on_audio_channel_closed_()`,并最终回到空闲状态。
|
||||
- When the server or network drops, `OnDisconnected()` fires:
|
||||
- The device invokes `on_audio_channel_closed_()` and eventually returns to the idle state.
|
||||
|
||||
6. **关闭 WebSocket 连接**
|
||||
- 设备在需要结束语音会话时,会调用 `CloseAudioChannel()` 主动断开连接,并回到空闲状态。
|
||||
- 或者如果服务器端主动断开,也会引发同样的回调流程。
|
||||
6. **Closing the WebSocket connection**
|
||||
- When the device wants to end the session, it calls `CloseAudioChannel()` to tear down the socket and returns to idle.
|
||||
- The same callback chain runs if the server closes the socket first.
|
||||
|
||||
---
|
||||
|
||||
## 2. 通用请求头
|
||||
## 2. Common Request Headers
|
||||
|
||||
在建立 WebSocket 连接时,代码示例中设置了以下请求头:
|
||||
When establishing the WebSocket connection, the device sets the following headers:
|
||||
|
||||
- `Authorization`: 用于存放访问令牌,形如 `"Bearer <token>"`
|
||||
- `Protocol-Version`: 协议版本号,与 hello 消息体内的 `version` 字段保持一致
|
||||
- `Device-Id`: 设备物理网卡 MAC 地址
|
||||
- `Client-Id`: 软件生成的 UUID(擦除 NVS 或重新烧录完整固件会重置)
|
||||
- `Authorization`: access token, usually formatted as `"Bearer <token>"`.
|
||||
- `Protocol-Version`: the protocol version number, matching the `version` field in the hello message.
|
||||
- `Device-Id`: the physical MAC address of the device.
|
||||
- `Client-Id`: a software-generated UUID (reset when NVS is erased or the full firmware is re-flashed).
|
||||
|
||||
这些头会随着 WebSocket 握手一起发送到服务器,服务器可根据需求进行校验、认证等。
|
||||
These headers are sent with the WebSocket handshake; the server can use them for authentication or bookkeeping.
|
||||
|
||||
---
|
||||
|
||||
## 3. 二进制协议版本
|
||||
## 3. Binary Protocol Versions
|
||||
|
||||
设备支持多种二进制协议版本,通过配置中的 `version` 字段指定:
|
||||
The device supports several binary protocol versions, selected by the `version` field in settings:
|
||||
|
||||
### 3.1 版本1(默认)
|
||||
直接发送 Opus 音频数据,无额外元数据。Websocket 协议会区分 text 与 binary。
|
||||
### 3.1 Version 1 (default)
|
||||
Raw Opus frames with no extra metadata. The WebSocket layer already distinguishes text and binary frames.
|
||||
|
||||
### 3.2 版本2
|
||||
使用 `BinaryProtocol2` 结构:
|
||||
### 3.2 Version 2
|
||||
Uses the `BinaryProtocol2` structure:
|
||||
```c
|
||||
struct BinaryProtocol2 {
|
||||
uint16_t version; // 协议版本
|
||||
uint16_t type; // 消息类型 (0: OPUS, 1: JSON)
|
||||
uint32_t reserved; // 保留字段
|
||||
uint32_t timestamp; // 时间戳(毫秒,用于服务器端AEC)
|
||||
uint32_t payload_size; // 负载大小(字节)
|
||||
uint8_t payload[]; // 负载数据
|
||||
uint16_t version; // protocol version
|
||||
uint16_t type; // message type (0: OPUS, 1: JSON)
|
||||
uint32_t reserved; // reserved
|
||||
uint32_t timestamp; // timestamp in milliseconds (useful for server-side AEC)
|
||||
uint32_t payload_size; // payload size in bytes
|
||||
uint8_t payload[]; // payload
|
||||
} __attribute__((packed));
|
||||
```
|
||||
|
||||
### 3.3 版本3
|
||||
使用 `BinaryProtocol3` 结构:
|
||||
### 3.3 Version 3
|
||||
Uses the `BinaryProtocol3` structure:
|
||||
```c
|
||||
struct BinaryProtocol3 {
|
||||
uint8_t type; // 消息类型
|
||||
uint8_t reserved; // 保留字段
|
||||
uint16_t payload_size; // 负载大小
|
||||
uint8_t payload[]; // 负载数据
|
||||
uint8_t type; // message type
|
||||
uint8_t reserved; // reserved
|
||||
uint16_t payload_size; // payload size
|
||||
uint8_t payload[]; // payload
|
||||
} __attribute__((packed));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. JSON 消息结构
|
||||
## 4. JSON Message Structure
|
||||
|
||||
WebSocket 文本帧以 JSON 方式传输,以下为常见的 `"type"` 字段及其对应业务逻辑。若消息里包含未列出的字段,可能为可选或特定实现细节。
|
||||
WebSocket text frames carry JSON. The most common `"type"` values and their semantics are listed below. Fields that are not listed may be implementation-specific or optional.
|
||||
|
||||
### 4.1 设备端→服务器
|
||||
### 4.1 Device -> Server
|
||||
|
||||
1. **Hello**
|
||||
- 连接成功后,由设备端发送,告知服务器基本参数。
|
||||
- 例:
|
||||
1. **Hello**
|
||||
- Sent once the connection is established; announces the device parameters.
|
||||
- Example:
|
||||
```json
|
||||
{
|
||||
"type": "hello",
|
||||
"version": 1,
|
||||
"features": {
|
||||
"mcp": true
|
||||
"mcp": true,
|
||||
"aec": true
|
||||
},
|
||||
"transport": "websocket",
|
||||
"audio_params": {
|
||||
@@ -151,14 +161,14 @@ WebSocket 文本帧以 JSON 方式传输,以下为常见的 `"type"` 字段及
|
||||
}
|
||||
```
|
||||
|
||||
2. **Listen**
|
||||
- 表示设备端开始或停止录音监听。
|
||||
- 常见字段:
|
||||
- `"session_id"`:会话标识
|
||||
- `"type": "listen"`
|
||||
- `"state"`:`"start"`, `"stop"`, `"detect"`(唤醒检测已触发)
|
||||
- `"mode"`:`"auto"`, `"manual"` 或 `"realtime"`,表示识别模式。
|
||||
- 例:开始监听
|
||||
2. **Listen**
|
||||
- Tells the server that the device is starting or stopping microphone capture.
|
||||
- Common fields:
|
||||
- `"session_id"`: session identifier.
|
||||
- `"type": "listen"`
|
||||
- `"state"`: `"start"`, `"stop"`, or `"detect"` (wake word detected).
|
||||
- `"mode"`: `"auto"`, `"manual"`, or `"realtime"`.
|
||||
- Example (start listening):
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
@@ -168,9 +178,9 @@ WebSocket 文本帧以 JSON 方式传输,以下为常见的 `"type"` 字段及
|
||||
}
|
||||
```
|
||||
|
||||
3. **Abort**
|
||||
- 终止当前说话(TTS 播放)或语音通道。
|
||||
- 例:
|
||||
3. **Abort**
|
||||
- Aborts the current TTS playback or the voice channel.
|
||||
- Example:
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
@@ -178,25 +188,24 @@ WebSocket 文本帧以 JSON 方式传输,以下为常见的 `"type"` 字段及
|
||||
"reason": "wake_word_detected"
|
||||
}
|
||||
```
|
||||
- `reason` 值可为 `"wake_word_detected"` 或其他。
|
||||
- `reason` may be `"wake_word_detected"` or other implementation-defined values.
|
||||
|
||||
4. **Wake Word Detected**
|
||||
- 用于设备端向服务器告知检测到唤醒词。
|
||||
- 在发送该消息之前,可提前发送唤醒词的 Opus 音频数据,用于服务器进行声纹检测。
|
||||
- 例:
|
||||
4. **Wake Word Detected**
|
||||
- Sent by the device when the local wake word detector fires.
|
||||
- Opus audio containing the wake word may be streamed before this message to let the server run voice-print verification.
|
||||
- Example:
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
"type": "listen",
|
||||
"state": "detect",
|
||||
"text": "你好小明"
|
||||
"text": "Hi XiaoZhi"
|
||||
}
|
||||
```
|
||||
|
||||
5. **MCP**
|
||||
- 推荐用于物联网控制的新一代协议。所有设备能力发现、工具调用等均通过 type: "mcp" 的消息进行,payload 内部为标准 JSON-RPC 2.0(详见 [MCP 协议文档](./mcp-protocol.md))。
|
||||
|
||||
- **设备端到服务器发送 result 的例子:**
|
||||
- The recommended channel for IoT control. Device capability discovery and tool invocation all flow through `type: "mcp"` messages whose `payload` is JSON-RPC 2.0 (see [MCP protocol document](./mcp-protocol.md)).
|
||||
- Device-to-server response example:
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
@@ -216,34 +225,31 @@ WebSocket 文本帧以 JSON 方式传输,以下为常见的 `"type"` 字段及
|
||||
|
||||
---
|
||||
|
||||
### 4.2 服务器→设备端
|
||||
### 4.2 Server -> Device
|
||||
|
||||
1. **Hello**
|
||||
- 服务器端返回的握手确认消息。
|
||||
- 必须包含 `"type": "hello"` 和 `"transport": "websocket"`。
|
||||
- 可能会带有 `audio_params`,表示服务器期望的音频参数,或与设备端对齐的配置。
|
||||
- 服务器可选下发 `session_id` 字段,设备端收到后会自动记录。
|
||||
- 成功接收后设备端会设置事件标志,表示 WebSocket 通道就绪。
|
||||
1. **Hello**
|
||||
- The handshake acknowledgement.
|
||||
- Must include `"type": "hello"` and `"transport": "websocket"`.
|
||||
- May include `audio_params`, meaning the audio parameters the server expects / the canonical set agreed with the device.
|
||||
- May include a `session_id` which the device records.
|
||||
- Once received, the device sets the "audio channel open" event.
|
||||
|
||||
2. **STT**
|
||||
2. **STT**
|
||||
- `{"session_id": "xxx", "type": "stt", "text": "..."}`
|
||||
- 表示服务器端识别到了用户语音。(例如语音转文本结果)
|
||||
- 设备可能将此文本显示到屏幕上,后续再进入回答等流程。
|
||||
- The speech-to-text result for the user utterance. Typically shown on the display before moving to the response.
|
||||
|
||||
3. **LLM**
|
||||
3. **LLM**
|
||||
- `{"session_id": "xxx", "type": "llm", "emotion": "happy", "text": "😀"}`
|
||||
- 服务器指示设备调整表情动画 / UI 表达。
|
||||
- Tells the device to update the emotion / facial expression on the UI.
|
||||
|
||||
4. **TTS**
|
||||
- `{"session_id": "xxx", "type": "tts", "state": "start"}`:服务器准备下发 TTS 音频,设备端进入 "speaking" 播放状态。
|
||||
- `{"session_id": "xxx", "type": "tts", "state": "stop"}`:表示本次 TTS 结束。
|
||||
- `{"session_id": "xxx", "type": "tts", "state": "sentence_start", "text": "..."}`
|
||||
- 让设备在界面上显示当前要播放或朗读的文本片段(例如用于显示给用户)。
|
||||
4. **TTS**
|
||||
- `{"session_id": "xxx", "type": "tts", "state": "start"}`: the server is about to stream TTS audio. The device transitions to the speaking state.
|
||||
- `{"session_id": "xxx", "type": "tts", "state": "stop"}`: the TTS segment is finished.
|
||||
- `{"session_id": "xxx", "type": "tts", "state": "sentence_start", "text": "..."}`: show the current sentence on the UI (for example, subtitle display).
|
||||
|
||||
5. **MCP**
|
||||
- 服务器通过 type: "mcp" 的消息下发物联网相关的控制指令或返回调用结果,payload 结构同上。
|
||||
|
||||
- **服务器到设备端发送 tools/call 的例子:**
|
||||
- The server sends IoT-related commands or receives tool-call results. The `payload` structure follows JSON-RPC 2.0.
|
||||
- Server-to-device `tools/call` example:
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
@@ -261,8 +267,8 @@ WebSocket 文本帧以 JSON 方式传输,以下为常见的 `"type"` 字段及
|
||||
```
|
||||
|
||||
6. **System**
|
||||
- 系统控制命令,常用于远程升级更新。
|
||||
- 例:
|
||||
- System-level control, often used for remote upgrades / management.
|
||||
- Example:
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
@@ -270,153 +276,190 @@ WebSocket 文本帧以 JSON 方式传输,以下为常见的 `"type"` 字段及
|
||||
"command": "reboot"
|
||||
}
|
||||
```
|
||||
- 支持的命令:
|
||||
- `"reboot"`:重启设备
|
||||
- Supported commands:
|
||||
- `"reboot"`: reboot the device.
|
||||
|
||||
7. **Custom**(可选)
|
||||
- 自定义消息,当 `CONFIG_RECEIVE_CUSTOM_MESSAGE` 启用时支持。
|
||||
- 例:
|
||||
7. **Alert**
|
||||
- Instructs the device to show an alert and play a vibration sound. Handled in `Application::OnIncomingJson`.
|
||||
- Example:
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
"type": "alert",
|
||||
"status": "Warning",
|
||||
"message": "Battery low",
|
||||
"emotion": "sad"
|
||||
}
|
||||
```
|
||||
- Fields:
|
||||
- `status`: short title displayed on screen.
|
||||
- `message`: detailed message.
|
||||
- `emotion`: emotion shown while alerting (e.g. `"sad"`, `"neutral"`).
|
||||
|
||||
8. **Custom** (optional)
|
||||
- Available when `CONFIG_RECEIVE_CUSTOM_MESSAGE` is enabled.
|
||||
- Example:
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
"type": "custom",
|
||||
"payload": {
|
||||
"message": "自定义内容"
|
||||
"message": "anything you want"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
8. **音频数据:二进制帧**
|
||||
- 当服务器发送音频二进制帧(Opus 编码)时,设备端解码并播放。
|
||||
- 若设备端正在处于 "listening" (录音)状态,收到的音频帧会被忽略或清空以防冲突。
|
||||
9. **Binary audio frames**
|
||||
- When the server pushes Opus-encoded audio as binary frames, the device decodes and plays them.
|
||||
- Frames received while the device is in the `listening` state are dropped to avoid conflicts with the microphone stream.
|
||||
|
||||
---
|
||||
|
||||
## 5. 音频编解码
|
||||
## 5. Audio Codec
|
||||
|
||||
1. **设备端发送录音数据**
|
||||
- 音频输入经过可能的回声消除、降噪或音量增益后,通过 Opus 编码打包为二进制帧发送给服务器。
|
||||
- 根据协议版本,可能直接发送 Opus 数据(版本1)或使用带元数据的二进制协议(版本2/3)。
|
||||
1. **Device uploads microphone audio**
|
||||
- After optional AEC / NR / AGC processing, the audio is Opus-encoded and sent as binary frames.
|
||||
- Depending on the protocol version, the frames may be raw Opus (v1) or wrapped in the metadata structures (v2/v3).
|
||||
|
||||
2. **设备端播放收到的音频**
|
||||
- 收到服务器的二进制帧时,同样认定是 Opus 数据。
|
||||
- 设备端会进行解码,然后交由音频输出接口播放。
|
||||
- 如果服务器的音频采样率与设备不一致,会在解码后再进行重采样。
|
||||
2. **Device plays server audio**
|
||||
- Incoming binary frames are also treated as Opus.
|
||||
- The device decodes and sends them to the audio output.
|
||||
- If the sample rate differs from the device's output, it is resampled after decoding.
|
||||
|
||||
---
|
||||
|
||||
## 6. 常见状态流转
|
||||
## 6. Device States
|
||||
|
||||
以下为常见设备端关键状态流转,与 WebSocket 消息对应:
|
||||
### 6.1 Main states
|
||||
|
||||
1. **Idle** → **Connecting**
|
||||
- 用户触发或唤醒后,设备调用 `OpenAudioChannel()` → 建立 WebSocket 连接 → 发送 `"type":"hello"`。
|
||||
The device state machine is defined in [`main/device_state.h`](../main/device_state.h) and includes:
|
||||
|
||||
2. **Connecting** → **Listening**
|
||||
- 成功建立连接后,若继续执行 `SendStartListening(...)`,则进入录音状态。此时设备会持续编码麦克风数据并发送到服务器。
|
||||
- `kDeviceStateUnknown`
|
||||
- `kDeviceStateStarting`
|
||||
- `kDeviceStateWifiConfiguring`
|
||||
- `kDeviceStateIdle`
|
||||
- `kDeviceStateConnecting`
|
||||
- `kDeviceStateListening`
|
||||
- `kDeviceStateSpeaking`
|
||||
- `kDeviceStateUpgrading`
|
||||
- `kDeviceStateActivating`
|
||||
- `kDeviceStateAudioTesting` (factory / bring-up audio testing)
|
||||
- `kDeviceStateFatalError` (non-recoverable error requiring user action)
|
||||
|
||||
3. **Listening** → **Speaking**
|
||||
- 收到服务器 TTS Start 消息 (`{"type":"tts","state":"start"}`) → 停止录音并播放接收到的音频。
|
||||
### 6.2 Typical transitions
|
||||
|
||||
4. **Speaking** → **Idle**
|
||||
- 服务器 TTS Stop (`{"type":"tts","state":"stop"}`) → 音频播放结束。若未继续进入自动监听,则返回 Idle;如果配置了自动循环,则再度进入 Listening。
|
||||
1. **Idle -> Connecting**
|
||||
- Triggered by wake word or button press. The device calls `OpenAudioChannel()`, sets up the WebSocket, and sends `"type":"hello"`.
|
||||
|
||||
5. **Listening** / **Speaking** → **Idle**(遇到异常或主动中断)
|
||||
- 调用 `SendAbortSpeaking(...)` 或 `CloseAudioChannel()` → 中断会话 → 关闭 WebSocket → 状态回到 Idle。
|
||||
2. **Connecting -> Listening**
|
||||
- Once connected, `SendStartListening(...)` is called and microphone streaming begins.
|
||||
|
||||
### 自动模式状态流转图
|
||||
3. **Listening -> Speaking**
|
||||
- Server sends `{"type":"tts","state":"start"}`; the device stops sending mic audio and plays incoming TTS.
|
||||
|
||||
4. **Speaking -> Idle**
|
||||
- Server sends `{"type":"tts","state":"stop"}`. When auto-continue is enabled the device transitions back to Listening; otherwise it returns to Idle.
|
||||
|
||||
5. **Listening / Speaking -> Idle** (abort)
|
||||
- `SendAbortSpeaking(...)` or `CloseAudioChannel()` interrupts the session and closes the WebSocket.
|
||||
|
||||
### 6.3 Auto-mode state diagram
|
||||
|
||||
```mermaid
|
||||
stateDiagram
|
||||
direction TB
|
||||
[*] --> kDeviceStateUnknown
|
||||
kDeviceStateUnknown --> kDeviceStateStarting:初始化
|
||||
kDeviceStateStarting --> kDeviceStateWifiConfiguring:配置WiFi
|
||||
kDeviceStateStarting --> kDeviceStateActivating:激活设备
|
||||
kDeviceStateActivating --> kDeviceStateUpgrading:检测到新版本
|
||||
kDeviceStateActivating --> kDeviceStateIdle:激活完成
|
||||
kDeviceStateIdle --> kDeviceStateConnecting:开始连接
|
||||
kDeviceStateConnecting --> kDeviceStateIdle:连接失败
|
||||
kDeviceStateConnecting --> kDeviceStateListening:连接成功
|
||||
kDeviceStateListening --> kDeviceStateSpeaking:开始说话
|
||||
kDeviceStateSpeaking --> kDeviceStateListening:结束说话
|
||||
kDeviceStateListening --> kDeviceStateIdle:手动终止
|
||||
kDeviceStateSpeaking --> kDeviceStateIdle:自动终止
|
||||
kDeviceStateUnknown --> kDeviceStateStarting: Initialize
|
||||
kDeviceStateStarting --> kDeviceStateWifiConfiguring: Configure WiFi
|
||||
kDeviceStateStarting --> kDeviceStateActivating: Activate device
|
||||
kDeviceStateActivating --> kDeviceStateUpgrading: New firmware detected
|
||||
kDeviceStateActivating --> kDeviceStateIdle: Activation complete
|
||||
kDeviceStateIdle --> kDeviceStateConnecting: Start connecting
|
||||
kDeviceStateConnecting --> kDeviceStateIdle: Connection failed
|
||||
kDeviceStateConnecting --> kDeviceStateListening: Connection succeeded
|
||||
kDeviceStateListening --> kDeviceStateSpeaking: TTS start
|
||||
kDeviceStateSpeaking --> kDeviceStateListening: TTS stop
|
||||
kDeviceStateListening --> kDeviceStateIdle: Manual abort
|
||||
kDeviceStateSpeaking --> kDeviceStateIdle: Auto stop
|
||||
kDeviceStateStarting --> kDeviceStateAudioTesting: Factory audio test
|
||||
kDeviceStateStarting --> kDeviceStateFatalError: Fatal error
|
||||
```
|
||||
|
||||
### 手动模式状态流转图
|
||||
### 6.4 Manual-mode state diagram
|
||||
|
||||
```mermaid
|
||||
stateDiagram
|
||||
direction TB
|
||||
[*] --> kDeviceStateUnknown
|
||||
kDeviceStateUnknown --> kDeviceStateStarting:初始化
|
||||
kDeviceStateStarting --> kDeviceStateWifiConfiguring:配置WiFi
|
||||
kDeviceStateStarting --> kDeviceStateActivating:激活设备
|
||||
kDeviceStateActivating --> kDeviceStateUpgrading:检测到新版本
|
||||
kDeviceStateActivating --> kDeviceStateIdle:激活完成
|
||||
kDeviceStateIdle --> kDeviceStateConnecting:开始连接
|
||||
kDeviceStateConnecting --> kDeviceStateIdle:连接失败
|
||||
kDeviceStateConnecting --> kDeviceStateListening:连接成功
|
||||
kDeviceStateIdle --> kDeviceStateListening:开始监听
|
||||
kDeviceStateListening --> kDeviceStateIdle:停止监听
|
||||
kDeviceStateIdle --> kDeviceStateSpeaking:开始说话
|
||||
kDeviceStateSpeaking --> kDeviceStateIdle:结束说话
|
||||
kDeviceStateUnknown --> kDeviceStateStarting: Initialize
|
||||
kDeviceStateStarting --> kDeviceStateWifiConfiguring: Configure WiFi
|
||||
kDeviceStateStarting --> kDeviceStateActivating: Activate device
|
||||
kDeviceStateActivating --> kDeviceStateUpgrading: New firmware detected
|
||||
kDeviceStateActivating --> kDeviceStateIdle: Activation complete
|
||||
kDeviceStateIdle --> kDeviceStateConnecting: Start connecting
|
||||
kDeviceStateConnecting --> kDeviceStateIdle: Connection failed
|
||||
kDeviceStateConnecting --> kDeviceStateListening: Connection succeeded
|
||||
kDeviceStateIdle --> kDeviceStateListening: Start listening
|
||||
kDeviceStateListening --> kDeviceStateIdle: Stop listening
|
||||
kDeviceStateIdle --> kDeviceStateSpeaking: Start speaking
|
||||
kDeviceStateSpeaking --> kDeviceStateIdle: Stop speaking
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 错误处理
|
||||
## 7. Error Handling
|
||||
|
||||
1. **连接失败**
|
||||
- 如果 `Connect(url)` 返回失败或在等待服务器 "hello" 消息时超时,触发 `on_network_error_()` 回调。设备会提示"无法连接到服务"或类似错误信息。
|
||||
1. **Connection failure**
|
||||
- If `Connect(url)` fails or the server hello is not received before the timeout, `on_network_error_()` is invoked and the device shows a "cannot connect" alert.
|
||||
|
||||
2. **服务器断开**
|
||||
- 如果 WebSocket 异常断开,回调 `OnDisconnected()`:
|
||||
- 设备回调 `on_audio_channel_closed_()`
|
||||
- 切换到 Idle 或其他重试逻辑。
|
||||
2. **Server disconnect**
|
||||
- If the WebSocket drops unexpectedly, `OnDisconnected()` is called:
|
||||
- `on_audio_channel_closed_()` runs.
|
||||
- The device returns to Idle (or retries, depending on policy).
|
||||
|
||||
---
|
||||
|
||||
## 8. 其它注意事项
|
||||
## 8. Other Notes
|
||||
|
||||
1. **鉴权**
|
||||
- 设备通过设置 `Authorization: Bearer <token>` 提供鉴权,服务器端需验证是否有效。
|
||||
- 如果令牌过期或无效,服务器可拒绝握手或在后续断开。
|
||||
1. **Authentication**
|
||||
- The device supplies `Authorization: Bearer <token>`; the server must validate it.
|
||||
- If the token is missing or invalid the server may reject the handshake or terminate the session later.
|
||||
|
||||
2. **会话控制**
|
||||
- 代码中部分消息包含 `session_id`,用于区分独立的对话或操作。服务端可根据需要对不同会话做分离处理。
|
||||
2. **Session scope**
|
||||
- Many messages carry a `session_id`, useful when the server serves multiple concurrent interactions.
|
||||
|
||||
3. **音频负载**
|
||||
- 代码里默认使用 Opus 格式,并设置 `sample_rate = 16000`,单声道。帧时长由 `OPUS_FRAME_DURATION_MS` 控制,一般为 60ms。可根据带宽或性能做适当调整。为了获得更好的音乐播放效果,服务器下行音频可能使用 24000 采样率。
|
||||
3. **Audio payload**
|
||||
- Default audio format is Opus at 16 kHz, mono. The frame duration is controlled by `OPUS_FRAME_DURATION_MS` (typically 60 ms). The server may use 24 kHz on the downlink for better music playback.
|
||||
|
||||
4. **协议版本配置**
|
||||
- 通过设置中的 `version` 字段配置二进制协议版本(1、2 或 3)
|
||||
- 版本1:直接发送 Opus 数据
|
||||
- 版本2:使用带时间戳的二进制协议,适用于服务器端 AEC
|
||||
- 版本3:使用简化的二进制协议
|
||||
4. **Binary protocol version selection**
|
||||
- Configured through the `version` setting:
|
||||
- v1: raw Opus
|
||||
- v2: metadata + timestamp (useful for server-side AEC)
|
||||
- v3: lightweight header
|
||||
- The value is echoed back in the `Protocol-Version` header and the hello message.
|
||||
|
||||
5. **物联网控制推荐 MCP 协议**
|
||||
- 设备与服务器之间的物联网能力发现、状态同步、控制指令等,建议全部通过 MCP 协议(type: "mcp")实现。原有的 type: "iot" 方案已废弃。
|
||||
- MCP 协议可在 WebSocket、MQTT 等多种底层协议上传输,具备更好的扩展性和标准化能力。
|
||||
- 详细用法请参考 [MCP 协议文档](./mcp-protocol.md) 及 [MCP 物联网控制用法](./mcp-usage.md)。
|
||||
5. **IoT control via MCP**
|
||||
- All IoT capability discovery and control flows through MCP (`type: "mcp"`). The legacy `type: "iot"` protocol is deprecated.
|
||||
- MCP works over both WebSocket and MQTT, giving better standardization and extensibility.
|
||||
- See [MCP protocol document](./mcp-protocol.md) and [MCP IoT control usage](./mcp-usage.md) for details.
|
||||
|
||||
6. **错误或异常 JSON**
|
||||
- 当 JSON 中缺少必要字段,例如 `{"type": ...}`,设备端会记录错误日志(`ESP_LOGE(TAG, "Missing message type, data: %s", data);`),不会执行任何业务。
|
||||
6. **Malformed JSON**
|
||||
- When a required field such as `type` is missing, the device logs `ESP_LOGE(TAG, "Missing message type, data: %s", data);` and ignores the message.
|
||||
|
||||
---
|
||||
|
||||
## 9. 消息示例
|
||||
## 9. Example Message Flow
|
||||
|
||||
下面给出一个典型的双向消息示例(流程简化示意):
|
||||
A simplified two-way exchange:
|
||||
|
||||
1. **设备端 → 服务器**(握手)
|
||||
1. **Device -> Server** (handshake)
|
||||
```json
|
||||
{
|
||||
"type": "hello",
|
||||
"version": 1,
|
||||
"features": {
|
||||
"mcp": true
|
||||
"mcp": true,
|
||||
"aec": true
|
||||
},
|
||||
"transport": "websocket",
|
||||
"audio_params": {
|
||||
@@ -428,7 +471,7 @@ stateDiagram
|
||||
}
|
||||
```
|
||||
|
||||
2. **服务器 → 设备端**(握手应答)
|
||||
2. **Server -> Device** (handshake ack)
|
||||
```json
|
||||
{
|
||||
"type": "hello",
|
||||
@@ -441,7 +484,7 @@ stateDiagram
|
||||
}
|
||||
```
|
||||
|
||||
3. **设备端 → 服务器**(开始监听)
|
||||
3. **Device -> Server** (start listening)
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
@@ -450,18 +493,18 @@ stateDiagram
|
||||
"mode": "auto"
|
||||
}
|
||||
```
|
||||
同时设备端开始发送二进制帧(Opus 数据)。
|
||||
The device begins streaming binary Opus frames.
|
||||
|
||||
4. **服务器 → 设备端**(ASR 结果)
|
||||
4. **Server -> Device** (ASR result)
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
"type": "stt",
|
||||
"text": "用户说的话"
|
||||
"text": "what the user said"
|
||||
}
|
||||
```
|
||||
|
||||
5. **服务器 → 设备端**(TTS开始)
|
||||
5. **Server -> Device** (TTS start)
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
@@ -469,9 +512,9 @@ stateDiagram
|
||||
"state": "start"
|
||||
}
|
||||
```
|
||||
接着服务器发送二进制音频帧给设备端播放。
|
||||
The server follows up with binary Opus frames for the device to play.
|
||||
|
||||
6. **服务器 → 设备端**(TTS结束)
|
||||
6. **Server -> Device** (TTS stop)
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
@@ -479,17 +522,17 @@ stateDiagram
|
||||
"state": "stop"
|
||||
}
|
||||
```
|
||||
设备端停止播放音频,若无更多指令,则回到空闲状态。
|
||||
The device stops playback and, if no further instructions arrive, returns to idle.
|
||||
|
||||
---
|
||||
|
||||
## 10. 总结
|
||||
## 10. Summary
|
||||
|
||||
本协议通过在 WebSocket 上层传输 JSON 文本与二进制音频帧,完成功能包括音频流上传、TTS 音频播放、语音识别与状态管理、MCP 指令下发等。其核心特征:
|
||||
This protocol carries JSON text and binary Opus frames over a WebSocket connection to implement audio streaming, TTS playback, speech recognition, device state management, MCP dispatch, and more. Key traits:
|
||||
|
||||
- **握手阶段**:发送 `"type":"hello"`,等待服务器返回。
|
||||
- **音频通道**:采用 Opus 编码的二进制帧双向传输语音流,支持多种协议版本。
|
||||
- **JSON 消息**:使用 `"type"` 为核心字段标识不同业务逻辑,包括 TTS、STT、MCP、WakeWord、System、Custom 等。
|
||||
- **扩展性**:可根据实际需求在 JSON 消息中添加字段,或在 headers 里进行额外鉴权。
|
||||
- **Handshake**: send `"type":"hello"` and wait for the server reply.
|
||||
- **Audio channel**: bidirectional Opus streaming, with three binary framing variants.
|
||||
- **JSON messages**: dispatched by `"type"` (TTS, STT, MCP, WakeWord, System, Alert, Custom, ...).
|
||||
- **Extensibility**: extra fields in JSON, additional headers for authentication.
|
||||
|
||||
服务器与设备端需提前约定各类消息的字段含义、时序逻辑以及错误处理规则,方能保证通信顺畅。上述信息可作为基础文档,便于后续对接、开发或扩展。
|
||||
Server and device must agree on the meaning, timing, and error handling of each message type so the session runs smoothly. The text above provides the baseline for integration, debugging, and extension.
|
||||
|
||||
@@ -0,0 +1,503 @@
|
||||
以下是一份基于代码实现整理的 WebSocket 通信协议文档,概述设备端与服务器之间如何通过 WebSocket 进行交互。
|
||||
|
||||
该文档仅基于所提供的代码推断,实际部署时可能需要结合服务器端实现进行进一步确认或补充。
|
||||
|
||||
---
|
||||
|
||||
## 1. 总体流程概览
|
||||
|
||||
1. **设备端初始化**
|
||||
- 设备上电、初始化 `Application`:
|
||||
- 初始化音频编解码器、显示屏、LED 等
|
||||
- 连接网络
|
||||
- 创建并初始化实现 `Protocol` 接口的 WebSocket 协议实例(`WebsocketProtocol`)
|
||||
- 进入主循环等待事件(音频输入、音频输出、调度任务等)。
|
||||
|
||||
2. **建立 WebSocket 连接**
|
||||
- 当设备需要开始语音会话时(例如用户唤醒、手动按键触发等),调用 `OpenAudioChannel()`:
|
||||
- 根据配置获取 WebSocket URL
|
||||
- 设置若干请求头(`Authorization`, `Protocol-Version`, `Device-Id`, `Client-Id`)
|
||||
- 调用 `Connect()` 与服务器建立 WebSocket 连接
|
||||
|
||||
3. **设备端发送 "hello" 消息**
|
||||
- 连接成功后,设备会发送一条 JSON 消息,示例结构如下:
|
||||
```json
|
||||
{
|
||||
"type": "hello",
|
||||
"version": 1,
|
||||
"features": {
|
||||
"mcp": true,
|
||||
"glyph_push": true
|
||||
},
|
||||
"text_font": {
|
||||
"bundle": "noto-v1",
|
||||
"charset": "common",
|
||||
"size": 20,
|
||||
"bpp": 4
|
||||
},
|
||||
"transport": "websocket",
|
||||
"audio_params": {
|
||||
"format": "opus",
|
||||
"sample_rate": 16000,
|
||||
"channels": 1,
|
||||
"frame_duration": 60
|
||||
}
|
||||
}
|
||||
```
|
||||
- 其中 `features` 字段为可选,内容根据设备编译配置自动生成。例如:`"mcp": true` 表示支持 MCP 协议。
|
||||
- `"glyph_push": true` 和 `text_font` 声明可选的动态文字 glyph 扩展,详见[动态文字 Glyph Push 扩展](glyph-push_zh.md)。
|
||||
- `frame_duration` 的值对应 `OPUS_FRAME_DURATION_MS`(例如 60ms)。
|
||||
|
||||
4. **服务器回复 "hello"**
|
||||
- 设备等待服务器返回一条包含 `"type": "hello"` 的 JSON 消息,并检查 `"transport": "websocket"` 是否匹配。
|
||||
- 服务器可选下发 `session_id` 字段,设备端收到后会自动记录。
|
||||
- 示例:
|
||||
```json
|
||||
{
|
||||
"type": "hello",
|
||||
"transport": "websocket",
|
||||
"session_id": "xxx",
|
||||
"audio_params": {
|
||||
"format": "opus",
|
||||
"sample_rate": 24000,
|
||||
"channels": 1,
|
||||
"frame_duration": 60
|
||||
}
|
||||
}
|
||||
```
|
||||
- 如果匹配,则认为服务器已就绪,标记音频通道打开成功。
|
||||
- 如果在超时时间(默认 10 秒)内未收到正确回复,认为连接失败并触发网络错误回调。
|
||||
|
||||
5. **后续消息交互**
|
||||
- 设备端和服务器端之间可发送两种主要类型的数据:
|
||||
1. **二进制音频数据**(Opus 编码)
|
||||
2. **文本 JSON 消息**(用于传输聊天状态、TTS/STT 事件、MCP 协议消息等)
|
||||
|
||||
- 在代码里,接收回调主要分为:
|
||||
- `OnData(...)`:
|
||||
- 当 `binary` 为 `true` 时,认为是音频帧;设备会将其当作 Opus 数据进行解码。
|
||||
- 当 `binary` 为 `false` 时,认为是 JSON 文本,需要在设备端用 cJSON 进行解析并做相应业务逻辑处理(如聊天、TTS、MCP 协议消息等)。
|
||||
|
||||
- 当服务器或网络出现断连,回调 `OnDisconnected()` 被触发:
|
||||
- 设备会调用 `on_audio_channel_closed_()`,并最终回到空闲状态。
|
||||
|
||||
6. **关闭 WebSocket 连接**
|
||||
- 设备在需要结束语音会话时,会调用 `CloseAudioChannel()` 主动断开连接,并回到空闲状态。
|
||||
- 或者如果服务器端主动断开,也会引发同样的回调流程。
|
||||
|
||||
---
|
||||
|
||||
## 2. 通用请求头
|
||||
|
||||
在建立 WebSocket 连接时,代码示例中设置了以下请求头:
|
||||
|
||||
- `Authorization`: 用于存放访问令牌,形如 `"Bearer <token>"`
|
||||
- `Protocol-Version`: 协议版本号,与 hello 消息体内的 `version` 字段保持一致
|
||||
- `Device-Id`: 设备物理网卡 MAC 地址
|
||||
- `Client-Id`: 软件生成的 UUID(擦除 NVS 或重新烧录完整固件会重置)
|
||||
|
||||
这些头会随着 WebSocket 握手一起发送到服务器,服务器可根据需求进行校验、认证等。
|
||||
|
||||
---
|
||||
|
||||
## 3. 二进制协议版本
|
||||
|
||||
设备支持多种二进制协议版本,通过配置中的 `version` 字段指定:
|
||||
|
||||
### 3.1 版本1(默认)
|
||||
直接发送 Opus 音频数据,无额外元数据。Websocket 协议会区分 text 与 binary。
|
||||
|
||||
### 3.2 版本2
|
||||
使用 `BinaryProtocol2` 结构:
|
||||
```c
|
||||
struct BinaryProtocol2 {
|
||||
uint16_t version; // 协议版本
|
||||
uint16_t type; // 消息类型 (0: OPUS, 1: JSON)
|
||||
uint32_t reserved; // 保留字段
|
||||
uint32_t timestamp; // 时间戳(毫秒,用于服务器端AEC)
|
||||
uint32_t payload_size; // 负载大小(字节)
|
||||
uint8_t payload[]; // 负载数据
|
||||
} __attribute__((packed));
|
||||
```
|
||||
|
||||
### 3.3 版本3
|
||||
使用 `BinaryProtocol3` 结构:
|
||||
```c
|
||||
struct BinaryProtocol3 {
|
||||
uint8_t type; // 消息类型
|
||||
uint8_t reserved; // 保留字段
|
||||
uint16_t payload_size; // 负载大小
|
||||
uint8_t payload[]; // 负载数据
|
||||
} __attribute__((packed));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. JSON 消息结构
|
||||
|
||||
WebSocket 文本帧以 JSON 方式传输,以下为常见的 `"type"` 字段及其对应业务逻辑。若消息里包含未列出的字段,可能为可选或特定实现细节。
|
||||
|
||||
### 4.1 设备端→服务器
|
||||
|
||||
1. **Hello**
|
||||
- 连接成功后,由设备端发送,告知服务器基本参数。
|
||||
- 例:
|
||||
```json
|
||||
{
|
||||
"type": "hello",
|
||||
"version": 1,
|
||||
"features": {
|
||||
"mcp": true
|
||||
},
|
||||
"transport": "websocket",
|
||||
"audio_params": {
|
||||
"format": "opus",
|
||||
"sample_rate": 16000,
|
||||
"channels": 1,
|
||||
"frame_duration": 60
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Listen**
|
||||
- 表示设备端开始或停止录音监听。
|
||||
- 常见字段:
|
||||
- `"session_id"`:会话标识
|
||||
- `"type": "listen"`
|
||||
- `"state"`:`"start"`, `"stop"`, `"detect"`(唤醒检测已触发)
|
||||
- `"mode"`:`"auto"`, `"manual"` 或 `"realtime"`,表示识别模式。
|
||||
- 例:开始监听
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
"type": "listen",
|
||||
"state": "start",
|
||||
"mode": "manual"
|
||||
}
|
||||
```
|
||||
|
||||
3. **Abort**
|
||||
- 终止当前说话(TTS 播放)或语音通道。
|
||||
- 例:
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
"type": "abort",
|
||||
"reason": "wake_word_detected"
|
||||
}
|
||||
```
|
||||
- `reason` 值可为 `"wake_word_detected"` 或其他。
|
||||
|
||||
4. **Wake Word Detected**
|
||||
- 用于设备端向服务器告知检测到唤醒词。
|
||||
- 在发送该消息之前,可提前发送唤醒词的 Opus 音频数据,用于服务器进行声纹检测。
|
||||
- 例:
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
"type": "listen",
|
||||
"state": "detect",
|
||||
"text": "你好小明"
|
||||
}
|
||||
```
|
||||
|
||||
5. **MCP**
|
||||
- 推荐用于物联网控制的新一代协议。所有设备能力发现、工具调用等均通过 type: "mcp" 的消息进行,payload 内部为标准 JSON-RPC 2.0(详见 [MCP 协议文档](./mcp-protocol_zh.md))。
|
||||
|
||||
- **设备端到服务器发送 result 的例子:**
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
"type": "mcp",
|
||||
"payload": {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"result": {
|
||||
"content": [
|
||||
{ "type": "text", "text": "true" }
|
||||
],
|
||||
"isError": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4.2 服务器→设备端
|
||||
|
||||
1. **Hello**
|
||||
- 服务器端返回的握手确认消息。
|
||||
- 必须包含 `"type": "hello"` 和 `"transport": "websocket"`。
|
||||
- 可能会带有 `audio_params`,表示服务器期望的音频参数,或与设备端对齐的配置。
|
||||
- 服务器可选下发 `session_id` 字段,设备端收到后会自动记录。
|
||||
- 成功接收后设备端会设置事件标志,表示 WebSocket 通道就绪。
|
||||
|
||||
2. **STT**
|
||||
- `{"session_id": "xxx", "type": "stt", "text": "..."}`
|
||||
- 表示服务器端识别到了用户语音。(例如语音转文本结果)
|
||||
- 设备可能将此文本显示到屏幕上,后续再进入回答等流程。
|
||||
|
||||
3. **LLM**
|
||||
- `{"session_id": "xxx", "type": "llm", "emotion": "happy", "text": "😀"}`
|
||||
- 服务器指示设备调整表情动画 / UI 表达。
|
||||
|
||||
4. **TTS**
|
||||
- `{"session_id": "xxx", "type": "tts", "state": "start"}`:服务器准备下发 TTS 音频,设备端进入 "speaking" 播放状态。
|
||||
- `{"session_id": "xxx", "type": "tts", "state": "stop"}`:表示本次 TTS 结束。
|
||||
- `{"session_id": "xxx", "type": "tts", "state": "sentence_start", "text": "..."}`
|
||||
- 让设备在界面上显示当前要播放或朗读的文本片段(例如用于显示给用户)。
|
||||
|
||||
5. **MCP**
|
||||
- 服务器通过 type: "mcp" 的消息下发物联网相关的控制指令或返回调用结果,payload 结构同上。
|
||||
|
||||
- **服务器到设备端发送 tools/call 的例子:**
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
"type": "mcp",
|
||||
"payload": {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "self.light.set_rgb",
|
||||
"arguments": { "r": 255, "g": 0, "b": 0 }
|
||||
},
|
||||
"id": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
6. **System**
|
||||
- 系统控制命令,常用于远程升级更新。
|
||||
- 例:
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
"type": "system",
|
||||
"command": "reboot"
|
||||
}
|
||||
```
|
||||
- 支持的命令:
|
||||
- `"reboot"`:重启设备
|
||||
|
||||
7. **Custom**(可选)
|
||||
- 自定义消息,当 `CONFIG_RECEIVE_CUSTOM_MESSAGE` 启用时支持。
|
||||
- 例:
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
"type": "custom",
|
||||
"payload": {
|
||||
"message": "自定义内容"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
8. **音频数据:二进制帧**
|
||||
- 当服务器发送音频二进制帧(Opus 编码)时,设备端解码并播放。
|
||||
- 若设备端正在处于 "listening" (录音)状态,收到的音频帧会被忽略或清空以防冲突。
|
||||
|
||||
---
|
||||
|
||||
## 5. 音频编解码
|
||||
|
||||
1. **设备端发送录音数据**
|
||||
- 音频输入经过可能的回声消除、降噪或音量增益后,通过 Opus 编码打包为二进制帧发送给服务器。
|
||||
- 根据协议版本,可能直接发送 Opus 数据(版本1)或使用带元数据的二进制协议(版本2/3)。
|
||||
|
||||
2. **设备端播放收到的音频**
|
||||
- 收到服务器的二进制帧时,同样认定是 Opus 数据。
|
||||
- 设备端会进行解码,然后交由音频输出接口播放。
|
||||
- 如果服务器的音频采样率与设备不一致,会在解码后再进行重采样。
|
||||
|
||||
---
|
||||
|
||||
## 6. 常见状态流转
|
||||
|
||||
以下为常见设备端关键状态流转,与 WebSocket 消息对应:
|
||||
|
||||
1. **Idle** → **Connecting**
|
||||
- 用户触发或唤醒后,设备调用 `OpenAudioChannel()` → 建立 WebSocket 连接 → 发送 `"type":"hello"`。
|
||||
|
||||
2. **Connecting** → **Listening**
|
||||
- 成功建立连接后,若继续执行 `SendStartListening(...)`,则进入录音状态。此时设备会持续编码麦克风数据并发送到服务器。
|
||||
|
||||
3. **Listening** → **Speaking**
|
||||
- 收到服务器 TTS Start 消息 (`{"type":"tts","state":"start"}`) → 停止录音并播放接收到的音频。
|
||||
|
||||
4. **Speaking** → **Idle**
|
||||
- 服务器 TTS Stop (`{"type":"tts","state":"stop"}`) → 音频播放结束。若未继续进入自动监听,则返回 Idle;如果配置了自动循环,则再度进入 Listening。
|
||||
|
||||
5. **Listening** / **Speaking** → **Idle**(遇到异常或主动中断)
|
||||
- 调用 `SendAbortSpeaking(...)` 或 `CloseAudioChannel()` → 中断会话 → 关闭 WebSocket → 状态回到 Idle。
|
||||
|
||||
### 自动模式状态流转图
|
||||
|
||||
```mermaid
|
||||
stateDiagram
|
||||
direction TB
|
||||
[*] --> kDeviceStateUnknown
|
||||
kDeviceStateUnknown --> kDeviceStateStarting:初始化
|
||||
kDeviceStateStarting --> kDeviceStateWifiConfiguring:配置WiFi
|
||||
kDeviceStateStarting --> kDeviceStateActivating:激活设备
|
||||
kDeviceStateActivating --> kDeviceStateUpgrading:检测到新版本
|
||||
kDeviceStateActivating --> kDeviceStateIdle:激活完成
|
||||
kDeviceStateIdle --> kDeviceStateConnecting:开始连接
|
||||
kDeviceStateConnecting --> kDeviceStateIdle:连接失败
|
||||
kDeviceStateConnecting --> kDeviceStateListening:连接成功
|
||||
kDeviceStateListening --> kDeviceStateSpeaking:开始说话
|
||||
kDeviceStateSpeaking --> kDeviceStateListening:结束说话
|
||||
kDeviceStateListening --> kDeviceStateIdle:手动终止
|
||||
kDeviceStateSpeaking --> kDeviceStateIdle:自动终止
|
||||
```
|
||||
|
||||
### 手动模式状态流转图
|
||||
|
||||
```mermaid
|
||||
stateDiagram
|
||||
direction TB
|
||||
[*] --> kDeviceStateUnknown
|
||||
kDeviceStateUnknown --> kDeviceStateStarting:初始化
|
||||
kDeviceStateStarting --> kDeviceStateWifiConfiguring:配置WiFi
|
||||
kDeviceStateStarting --> kDeviceStateActivating:激活设备
|
||||
kDeviceStateActivating --> kDeviceStateUpgrading:检测到新版本
|
||||
kDeviceStateActivating --> kDeviceStateIdle:激活完成
|
||||
kDeviceStateIdle --> kDeviceStateConnecting:开始连接
|
||||
kDeviceStateConnecting --> kDeviceStateIdle:连接失败
|
||||
kDeviceStateConnecting --> kDeviceStateListening:连接成功
|
||||
kDeviceStateIdle --> kDeviceStateListening:开始监听
|
||||
kDeviceStateListening --> kDeviceStateIdle:停止监听
|
||||
kDeviceStateIdle --> kDeviceStateSpeaking:开始说话
|
||||
kDeviceStateSpeaking --> kDeviceStateIdle:结束说话
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 错误处理
|
||||
|
||||
1. **连接失败**
|
||||
- 如果 `Connect(url)` 返回失败或在等待服务器 "hello" 消息时超时,触发 `on_network_error_()` 回调。设备会提示"无法连接到服务"或类似错误信息。
|
||||
|
||||
2. **服务器断开**
|
||||
- 如果 WebSocket 异常断开,回调 `OnDisconnected()`:
|
||||
- 设备回调 `on_audio_channel_closed_()`
|
||||
- 切换到 Idle 或其他重试逻辑。
|
||||
|
||||
---
|
||||
|
||||
## 8. 其它注意事项
|
||||
|
||||
1. **鉴权**
|
||||
- 设备通过设置 `Authorization: Bearer <token>` 提供鉴权,服务器端需验证是否有效。
|
||||
- 如果令牌过期或无效,服务器可拒绝握手或在后续断开。
|
||||
|
||||
2. **会话控制**
|
||||
- 代码中部分消息包含 `session_id`,用于区分独立的对话或操作。服务端可根据需要对不同会话做分离处理。
|
||||
|
||||
3. **音频负载**
|
||||
- 代码里默认使用 Opus 格式,并设置 `sample_rate = 16000`,单声道。帧时长由 `OPUS_FRAME_DURATION_MS` 控制,一般为 60ms。可根据带宽或性能做适当调整。为了获得更好的音乐播放效果,服务器下行音频可能使用 24000 采样率。
|
||||
|
||||
4. **协议版本配置**
|
||||
- 通过设置中的 `version` 字段配置二进制协议版本(1、2 或 3)
|
||||
- 版本1:直接发送 Opus 数据
|
||||
- 版本2:使用带时间戳的二进制协议,适用于服务器端 AEC
|
||||
- 版本3:使用简化的二进制协议
|
||||
|
||||
5. **物联网控制推荐 MCP 协议**
|
||||
- 设备与服务器之间的物联网能力发现、状态同步、控制指令等,建议全部通过 MCP 协议(type: "mcp")实现。原有的 type: "iot" 方案已废弃。
|
||||
- MCP 协议可在 WebSocket、MQTT 等多种底层协议上传输,具备更好的扩展性和标准化能力。
|
||||
- 详细用法请参考 [MCP 协议文档](./mcp-protocol_zh.md) 及 [MCP 物联网控制用法](./mcp-usage_zh.md)。
|
||||
|
||||
6. **错误或异常 JSON**
|
||||
- 当 JSON 中缺少必要字段,例如 `{"type": ...}`,设备端会记录错误日志(`ESP_LOGE(TAG, "Missing message type, data: %s", data);`),不会执行任何业务。
|
||||
|
||||
---
|
||||
|
||||
## 9. 消息示例
|
||||
|
||||
下面给出一个典型的双向消息示例(流程简化示意):
|
||||
|
||||
1. **设备端 → 服务器**(握手)
|
||||
```json
|
||||
{
|
||||
"type": "hello",
|
||||
"version": 1,
|
||||
"features": {
|
||||
"mcp": true
|
||||
},
|
||||
"transport": "websocket",
|
||||
"audio_params": {
|
||||
"format": "opus",
|
||||
"sample_rate": 16000,
|
||||
"channels": 1,
|
||||
"frame_duration": 60
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **服务器 → 设备端**(握手应答)
|
||||
```json
|
||||
{
|
||||
"type": "hello",
|
||||
"transport": "websocket",
|
||||
"session_id": "xxx",
|
||||
"audio_params": {
|
||||
"format": "opus",
|
||||
"sample_rate": 16000
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **设备端 → 服务器**(开始监听)
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
"type": "listen",
|
||||
"state": "start",
|
||||
"mode": "auto"
|
||||
}
|
||||
```
|
||||
同时设备端开始发送二进制帧(Opus 数据)。
|
||||
|
||||
4. **服务器 → 设备端**(ASR 结果)
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
"type": "stt",
|
||||
"text": "用户说的话"
|
||||
}
|
||||
```
|
||||
|
||||
5. **服务器 → 设备端**(TTS开始)
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
"type": "tts",
|
||||
"state": "start"
|
||||
}
|
||||
```
|
||||
接着服务器发送二进制音频帧给设备端播放。
|
||||
|
||||
6. **服务器 → 设备端**(TTS结束)
|
||||
```json
|
||||
{
|
||||
"session_id": "xxx",
|
||||
"type": "tts",
|
||||
"state": "stop"
|
||||
}
|
||||
```
|
||||
设备端停止播放音频,若无更多指令,则回到空闲状态。
|
||||
|
||||
---
|
||||
|
||||
## 10. 总结
|
||||
|
||||
本协议通过在 WebSocket 上层传输 JSON 文本与二进制音频帧,完成功能包括音频流上传、TTS 音频播放、语音识别与状态管理、MCP 指令下发等。其核心特征:
|
||||
|
||||
- **握手阶段**:发送 `"type":"hello"`,等待服务器返回。
|
||||
- **音频通道**:采用 Opus 编码的二进制帧双向传输语音流,支持多种协议版本。
|
||||
- **JSON 消息**:使用 `"type"` 为核心字段标识不同业务逻辑,包括 TTS、STT、MCP、WakeWord、System、Custom 等。
|
||||
- **扩展性**:可根据实际需求在 JSON 消息中添加字段,或在 headers 里进行额外鉴权。
|
||||
|
||||
服务器与设备端需提前约定各类消息的字段含义、时序逻辑以及错误处理规则,方能保证通信顺畅。上述信息可作为基础文档,便于后续对接、开发或扩展。
|
||||
+1174
-175
File diff suppressed because it is too large
Load Diff
+868
-301
File diff suppressed because it is too large
Load Diff
+908
-488
File diff suppressed because it is too large
Load Diff
+126
-19
@@ -9,20 +9,31 @@
|
||||
#include <string>
|
||||
#include <mutex>
|
||||
#include <deque>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <functional>
|
||||
|
||||
#include "protocol.h"
|
||||
#include "ota.h"
|
||||
#include "audio_service.h"
|
||||
#include "device_state_event.h"
|
||||
#include "device_state.h"
|
||||
#include "device_state_machine.h"
|
||||
|
||||
// Main event bits
|
||||
#define MAIN_EVENT_SCHEDULE (1 << 0)
|
||||
#define MAIN_EVENT_SEND_AUDIO (1 << 1)
|
||||
#define MAIN_EVENT_WAKE_WORD_DETECTED (1 << 2)
|
||||
#define MAIN_EVENT_VAD_CHANGE (1 << 3)
|
||||
#define MAIN_EVENT_ERROR (1 << 4)
|
||||
#define MAIN_EVENT_ACTIVATION_DONE (1 << 5)
|
||||
#define MAIN_EVENT_CLOCK_TICK (1 << 6)
|
||||
#define MAIN_EVENT_NETWORK_CONNECTED (1 << 7)
|
||||
#define MAIN_EVENT_NETWORK_DISCONNECTED (1 << 8)
|
||||
#define MAIN_EVENT_TOGGLE_CHAT (1 << 9)
|
||||
#define MAIN_EVENT_START_LISTENING (1 << 10)
|
||||
#define MAIN_EVENT_STOP_LISTENING (1 << 11)
|
||||
#define MAIN_EVENT_STATE_CHANGED (1 << 12)
|
||||
#define MAIN_EVENT_PLAYBACK_DRAINED (1 << 13)
|
||||
|
||||
#define MAIN_EVENT_SCHEDULE (1 << 0)
|
||||
#define MAIN_EVENT_SEND_AUDIO (1 << 1)
|
||||
#define MAIN_EVENT_WAKE_WORD_DETECTED (1 << 2)
|
||||
#define MAIN_EVENT_VAD_CHANGE (1 << 3)
|
||||
#define MAIN_EVENT_ERROR (1 << 4)
|
||||
#define MAIN_EVENT_CHECK_NEW_VERSION_DONE (1 << 5)
|
||||
|
||||
enum AecMode {
|
||||
kAecOff,
|
||||
@@ -36,29 +47,81 @@ public:
|
||||
static Application instance;
|
||||
return instance;
|
||||
}
|
||||
// 删除拷贝构造函数和赋值运算符
|
||||
// Delete copy constructor and assignment operator
|
||||
Application(const Application&) = delete;
|
||||
Application& operator=(const Application&) = delete;
|
||||
|
||||
void Start();
|
||||
DeviceState GetDeviceState() const { return device_state_; }
|
||||
/**
|
||||
* Initialize the application
|
||||
* This sets up display, audio, network callbacks, etc.
|
||||
* Network connection starts asynchronously.
|
||||
*/
|
||||
void Initialize();
|
||||
|
||||
/**
|
||||
* Run the main event loop
|
||||
* This function runs in the main task and never returns.
|
||||
* It handles all events including network, state changes, and user interactions.
|
||||
*/
|
||||
void Run();
|
||||
|
||||
DeviceState GetDeviceState() const { return state_machine_.GetState(); }
|
||||
bool IsVoiceDetected() const { return audio_service_.IsVoiceDetected(); }
|
||||
void Schedule(std::function<void()> callback);
|
||||
void SetDeviceState(DeviceState state);
|
||||
|
||||
/**
|
||||
* Request state transition
|
||||
* Returns true if transition was successful
|
||||
*/
|
||||
bool SetDeviceState(DeviceState state);
|
||||
|
||||
/**
|
||||
* Schedule a callback to be executed in the main task
|
||||
*/
|
||||
void Schedule(std::function<void()>&& callback);
|
||||
|
||||
/**
|
||||
* Alert with status, message, emotion and optional sound
|
||||
*/
|
||||
void Alert(const char* status, const char* message, const char* emotion = "", const std::string_view& sound = "");
|
||||
void DismissAlert();
|
||||
|
||||
void AbortSpeaking(AbortReason reason);
|
||||
|
||||
/**
|
||||
* Toggle chat state (event-based, thread-safe)
|
||||
* Sends MAIN_EVENT_TOGGLE_CHAT to be handled in Run()
|
||||
*/
|
||||
void ToggleChatState();
|
||||
|
||||
/**
|
||||
* Start listening (event-based, thread-safe)
|
||||
* Sends MAIN_EVENT_START_LISTENING to be handled in Run()
|
||||
*/
|
||||
void StartListening();
|
||||
|
||||
/**
|
||||
* Stop listening (event-based, thread-safe)
|
||||
* Sends MAIN_EVENT_STOP_LISTENING to be handled in Run()
|
||||
*/
|
||||
void StopListening();
|
||||
|
||||
void Reboot();
|
||||
void WakeWordInvoke(const std::string& wake_word);
|
||||
bool UpgradeFirmware(const std::string& url, const std::string& version = "");
|
||||
bool CanEnterSleepMode();
|
||||
void SendMcpMessage(const std::string& payload);
|
||||
void RegisterMcpBroadcastCallback(std::function<void(const std::string&)> callback);
|
||||
void SetAecMode(AecMode mode);
|
||||
AecMode GetAecMode() const { return aec_mode_; }
|
||||
void PlaySound(const std::string_view& sound);
|
||||
AudioService& GetAudioService() { return audio_service_; }
|
||||
|
||||
/**
|
||||
* Reset protocol resources (thread-safe)
|
||||
* Can be called from any task to release resources allocated after network connected
|
||||
* This includes closing audio channel, resetting protocol and ota objects
|
||||
*/
|
||||
void ResetProtocol();
|
||||
|
||||
private:
|
||||
Application();
|
||||
@@ -69,23 +132,67 @@ private:
|
||||
std::unique_ptr<Protocol> protocol_;
|
||||
EventGroupHandle_t event_group_ = nullptr;
|
||||
esp_timer_handle_t clock_timer_handle_ = nullptr;
|
||||
volatile DeviceState device_state_ = kDeviceStateUnknown;
|
||||
DeviceStateMachine state_machine_;
|
||||
ListeningMode listening_mode_ = kListeningModeAutoStop;
|
||||
AecMode aec_mode_ = kAecOff;
|
||||
std::string last_error_message_;
|
||||
AudioService audio_service_;
|
||||
std::unique_ptr<Ota> ota_;
|
||||
|
||||
std::function<void(const std::string&)> mcp_broadcast_callback_;
|
||||
|
||||
bool has_server_time_ = false;
|
||||
bool aborted_ = false;
|
||||
bool assets_version_checked_ = false;
|
||||
bool play_popup_on_listening_ = false; // Flag to play popup sound after state changes to listening
|
||||
bool pending_listening_start_ = false; // Waiting for playback to drain before starting listening (auto mode)
|
||||
int clock_ticks_ = 0;
|
||||
TaskHandle_t check_new_version_task_handle_ = nullptr;
|
||||
TaskHandle_t activation_task_handle_ = nullptr;
|
||||
|
||||
void MainEventLoop();
|
||||
void OnWakeWordDetected();
|
||||
void CheckNewVersion(Ota& ota);
|
||||
|
||||
// Event handlers
|
||||
void HandleStateChangedEvent();
|
||||
void HandleToggleChatEvent();
|
||||
void HandleStartListeningEvent();
|
||||
void HandleStopListeningEvent();
|
||||
void HandleNetworkConnectedEvent();
|
||||
void HandleNetworkDisconnectedEvent();
|
||||
void HandleActivationDoneEvent();
|
||||
void HandleWakeWordDetectedEvent();
|
||||
void ContinueOpenAudioChannel(ListeningMode mode);
|
||||
void BeginWakeWordInvoke(const std::string& wake_word);
|
||||
void ContinueWakeWordInvoke(const std::string& wake_word);
|
||||
void StartListeningAudio();
|
||||
void ConfigureWakeWordForListening();
|
||||
|
||||
// Activation task (runs in background)
|
||||
void ActivationTask();
|
||||
|
||||
// Helper methods
|
||||
void CheckAssetsVersion();
|
||||
void CheckNewVersion();
|
||||
void InitializeProtocol();
|
||||
void ShowActivationCode(const std::string& code, const std::string& message);
|
||||
void OnClockTimer();
|
||||
void SetListeningMode(ListeningMode mode);
|
||||
ListeningMode GetDefaultListeningMode() const;
|
||||
|
||||
// State change handler called by state machine
|
||||
void OnStateChanged(DeviceState old_state, DeviceState new_state);
|
||||
};
|
||||
|
||||
|
||||
class TaskPriorityReset {
|
||||
public:
|
||||
TaskPriorityReset(BaseType_t priority) {
|
||||
original_priority_ = uxTaskPriorityGet(NULL);
|
||||
vTaskPrioritySet(NULL, priority);
|
||||
}
|
||||
~TaskPriorityReset() {
|
||||
vTaskPrioritySet(NULL, original_priority_);
|
||||
}
|
||||
|
||||
private:
|
||||
BaseType_t original_priority_;
|
||||
};
|
||||
|
||||
#endif // _APPLICATION_H_
|
||||
|
||||
+674
@@ -0,0 +1,674 @@
|
||||
#include "assets.h"
|
||||
#include "application.h"
|
||||
#include "board.h"
|
||||
#include "display.h"
|
||||
#include "emote_display.h"
|
||||
#include "expression_emote.h"
|
||||
#include "lvgl_theme.h"
|
||||
#if HAVE_LVGL
|
||||
#include <spi_flash_mmap.h>
|
||||
#include "display/lcd_display.h"
|
||||
#include "display/lvgl_display/lvgl_display.h"
|
||||
#endif
|
||||
|
||||
#include <esp_heap_caps.h>
|
||||
#include <esp_log.h>
|
||||
#include <esp_timer.h>
|
||||
#include <cbin_font.h>
|
||||
#include <noto_font_bundle.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#define TAG "Assets"
|
||||
#define PARTITION_LABEL "assets"
|
||||
|
||||
struct mmap_assets_table {
|
||||
char asset_name[32]; /*!< Name of the asset */
|
||||
uint32_t asset_size; /*!< Size of the asset */
|
||||
uint32_t asset_offset; /*!< Offset of the asset */
|
||||
uint16_t asset_width; /*!< Width of the asset */
|
||||
uint16_t asset_height; /*!< Height of the asset */
|
||||
};
|
||||
|
||||
Assets::Assets() {
|
||||
UseBuiltInTextFontCapability();
|
||||
#if HAVE_LVGL
|
||||
strategy_ = std::make_unique<Assets::LvglStrategy>();
|
||||
#else
|
||||
strategy_ = std::make_unique<Assets::EmoteStrategy>();
|
||||
#endif
|
||||
// Initialize the partition
|
||||
InitializePartition();
|
||||
}
|
||||
|
||||
Assets::~Assets() { UnApplyPartition(); }
|
||||
|
||||
bool Assets::FindPartition(Assets* assets) {
|
||||
assets->partition_ = esp_partition_find_first(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY,
|
||||
PARTITION_LABEL);
|
||||
if (assets->partition_ == nullptr) {
|
||||
ESP_LOGI(TAG, "No assets partition found");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Assets::Apply(bool refresh_display_theme) {
|
||||
return strategy_ ? strategy_->Apply(this, refresh_display_theme) : false;
|
||||
}
|
||||
|
||||
bool Assets::InitializePartition() {
|
||||
return strategy_ ? strategy_->InitializePartition(this) : false;
|
||||
}
|
||||
|
||||
void Assets::UnApplyPartition() {
|
||||
UseBuiltInTextFontCapability();
|
||||
if (strategy_) {
|
||||
strategy_->UnApplyPartition(this);
|
||||
}
|
||||
}
|
||||
|
||||
void Assets::UseBuiltInTextFontCapability() {
|
||||
text_font_capability_ = {
|
||||
.glyph_push = true,
|
||||
.bundle = NOTO_FONT_BUNDLE_ID,
|
||||
.charset = "basic",
|
||||
.size = TEXT_FONT_SIZE,
|
||||
.bpp = TEXT_FONT_BPP,
|
||||
};
|
||||
}
|
||||
|
||||
void Assets::DisableTextFontGlyphPush() { text_font_capability_ = {}; }
|
||||
|
||||
bool Assets::GetAssetData(const std::string& name, void*& ptr, size_t& size) {
|
||||
return strategy_ ? strategy_->GetAssetData(this, name, ptr, size) : false;
|
||||
}
|
||||
|
||||
bool Assets::LoadSrmodelsFromIndex(Assets* assets, cJSON* root) {
|
||||
void* ptr = nullptr;
|
||||
size_t size = 0;
|
||||
bool need_delete_root = false;
|
||||
|
||||
// If root is not provided, parse index.json
|
||||
if (root == nullptr) {
|
||||
if (!assets->GetAssetData("index.json", ptr, size)) {
|
||||
ESP_LOGE(TAG, "The index.json file is not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
root = cJSON_ParseWithLength(static_cast<char*>(ptr), size);
|
||||
if (root == nullptr) {
|
||||
ESP_LOGE(TAG, "The index.json file is not valid");
|
||||
return false;
|
||||
}
|
||||
need_delete_root = true;
|
||||
}
|
||||
|
||||
cJSON* srmodels = cJSON_GetObjectItem(root, "srmodels");
|
||||
if (cJSON_IsString(srmodels)) {
|
||||
std::string srmodels_file = srmodels->valuestring;
|
||||
if (assets->GetAssetData(srmodels_file, ptr, size)) {
|
||||
if (assets->models_list_ != nullptr) {
|
||||
esp_srmodel_deinit(assets->models_list_);
|
||||
assets->models_list_ = nullptr;
|
||||
}
|
||||
assets->models_list_ = srmodel_load(static_cast<uint8_t*>(ptr));
|
||||
if (assets->models_list_ != nullptr) {
|
||||
auto& app = Application::GetInstance();
|
||||
app.GetAudioService().SetModelsList(assets->models_list_);
|
||||
if (need_delete_root) {
|
||||
cJSON_Delete(root);
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed to load srmodels.bin");
|
||||
}
|
||||
} else {
|
||||
ESP_LOGE(TAG, "The srmodels file %s is not found", srmodels_file.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
if (need_delete_root) {
|
||||
cJSON_Delete(root);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#if HAVE_LVGL
|
||||
uint32_t Assets::LvglStrategy::CalculateChecksum(const char* data, uint32_t length) {
|
||||
uint32_t checksum = 0;
|
||||
for (uint32_t i = 0; i < length; i++) {
|
||||
checksum += data[i];
|
||||
}
|
||||
return checksum & 0xFFFF;
|
||||
}
|
||||
|
||||
bool Assets::LvglStrategy::InitializePartition(Assets* assets) {
|
||||
assets->partition_valid_ = false;
|
||||
assets_.clear();
|
||||
|
||||
if (!Assets::FindPartition(assets)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int free_pages = spi_flash_mmap_get_free_pages(SPI_FLASH_MMAP_DATA);
|
||||
uint32_t storage_size = free_pages * 64 * 1024;
|
||||
ESP_LOGI(TAG, "The storage free size is %ld KB", storage_size / 1024);
|
||||
ESP_LOGI(TAG, "The partition size is %ld KB", assets->partition_->size / 1024);
|
||||
if (storage_size < assets->partition_->size) {
|
||||
ESP_LOGE(TAG, "The free size %ld KB is less than assets partition required %ld KB",
|
||||
storage_size / 1024, assets->partition_->size / 1024);
|
||||
return false;
|
||||
}
|
||||
|
||||
esp_err_t err =
|
||||
esp_partition_mmap(assets->partition_, 0, assets->partition_->size, ESP_PARTITION_MMAP_DATA,
|
||||
(const void**)&mmap_root_, &mmap_handle_);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to mmap assets partition: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
assets->partition_valid_ = true;
|
||||
|
||||
uint32_t stored_files = *(uint32_t*)(mmap_root_ + 0);
|
||||
uint32_t stored_chksum = *(uint32_t*)(mmap_root_ + 4);
|
||||
uint32_t stored_len = *(uint32_t*)(mmap_root_ + 8);
|
||||
|
||||
if (stored_len > assets->partition_->size - 12) {
|
||||
ESP_LOGD(TAG, "The stored_len (0x%lx) is greater than the partition size (0x%lx) - 12",
|
||||
stored_len, assets->partition_->size);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto start_time = esp_timer_get_time();
|
||||
uint32_t calculated_checksum = CalculateChecksum(mmap_root_ + 12, stored_len);
|
||||
auto end_time = esp_timer_get_time();
|
||||
ESP_LOGI(TAG, "The checksum calculation time is %d ms", int((end_time - start_time) / 1000));
|
||||
|
||||
if (calculated_checksum != stored_chksum) {
|
||||
ESP_LOGE(TAG, "The calculated checksum (0x%lx) does not match the stored checksum (0x%lx)",
|
||||
calculated_checksum, stored_chksum);
|
||||
return false;
|
||||
}
|
||||
|
||||
checksum_valid_ = true;
|
||||
|
||||
for (uint32_t i = 0; i < stored_files; i++) {
|
||||
auto item = (const mmap_assets_table*)(mmap_root_ + 12 + i * sizeof(mmap_assets_table));
|
||||
auto asset = Asset{.size = static_cast<size_t>(item->asset_size),
|
||||
.offset = static_cast<size_t>(
|
||||
12 + sizeof(mmap_assets_table) * stored_files + item->asset_offset)};
|
||||
assets_[item->asset_name] = asset;
|
||||
}
|
||||
return checksum_valid_;
|
||||
}
|
||||
|
||||
void Assets::LvglStrategy::UnApplyPartition(Assets* assets) {
|
||||
if (mmap_handle_ != 0) {
|
||||
esp_partition_munmap(mmap_handle_);
|
||||
mmap_handle_ = 0;
|
||||
mmap_root_ = nullptr;
|
||||
}
|
||||
checksum_valid_ = false;
|
||||
assets_.clear();
|
||||
(void)assets; // Unused parameter
|
||||
}
|
||||
|
||||
bool Assets::LvglStrategy::GetAssetData(Assets* assets, const std::string& name, void*& ptr,
|
||||
size_t& size) {
|
||||
auto asset = assets_.find(name);
|
||||
if (asset == assets_.end()) {
|
||||
return false;
|
||||
}
|
||||
auto data = (const char*)(mmap_root_ + asset->second.offset);
|
||||
if (data[0] != 'Z' || data[1] != 'Z') {
|
||||
ESP_LOGE(TAG, "The asset %s is not valid with magic %02x%02x", name.c_str(), data[0],
|
||||
data[1]);
|
||||
return false;
|
||||
}
|
||||
|
||||
ptr = static_cast<void*>(const_cast<char*>(data + 2));
|
||||
size = asset->second.size;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Assets::LvglStrategy::Apply(Assets* assets, bool refresh_display_theme) {
|
||||
void* ptr = nullptr;
|
||||
size_t size = 0;
|
||||
if (!assets->GetAssetData("index.json", ptr, size)) {
|
||||
ESP_LOGE(TAG, "The index.json file is not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
cJSON* root = cJSON_ParseWithLength(static_cast<char*>(ptr), size);
|
||||
if (root == nullptr) {
|
||||
ESP_LOGE(TAG, "The index.json file is not valid");
|
||||
return false;
|
||||
}
|
||||
|
||||
cJSON* version = cJSON_GetObjectItem(root, "version");
|
||||
if (cJSON_IsNumber(version)) {
|
||||
if (version->valuedouble > 1) {
|
||||
ESP_LOGE(TAG, "The assets version %d is not supported, please upgrade the firmware",
|
||||
version->valueint);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Assets::LoadSrmodelsFromIndex(assets, root);
|
||||
|
||||
auto& theme_manager = LvglThemeManager::GetInstance();
|
||||
auto light_theme = theme_manager.GetTheme("light");
|
||||
auto dark_theme = theme_manager.GetTheme("dark");
|
||||
|
||||
cJSON* font = cJSON_GetObjectItem(root, "text_font");
|
||||
if (cJSON_IsString(font)) {
|
||||
std::string fonts_text_file = font->valuestring;
|
||||
if (assets->GetAssetData(fonts_text_file, ptr, size)) {
|
||||
auto text_font = std::make_shared<LvglCBinFont>(ptr);
|
||||
auto display = dynamic_cast<LvglDisplay*>(Board::GetInstance().GetDisplay());
|
||||
if (text_font->font() == nullptr || display == nullptr ||
|
||||
!display->SetTextFont(text_font)) {
|
||||
ESP_LOGW(TAG, "Ignoring invalid text font asset %s", fonts_text_file.c_str());
|
||||
} else {
|
||||
assets->DisableTextFontGlyphPush();
|
||||
|
||||
cJSON* metadata = cJSON_GetObjectItem(root, "text_font_meta");
|
||||
cJSON* charset = cJSON_GetObjectItem(metadata, "charset");
|
||||
cJSON* font_size = cJSON_GetObjectItem(metadata, "size");
|
||||
cJSON* font_bpp = cJSON_GetObjectItem(metadata, "bpp");
|
||||
cJSON* bundle = cJSON_GetObjectItem(metadata, "bundle");
|
||||
bool supports_glyph_push =
|
||||
cJSON_IsString(charset) &&
|
||||
(std::strcmp(charset->valuestring, "basic") == 0 ||
|
||||
std::strcmp(charset->valuestring, "common") == 0) &&
|
||||
cJSON_IsNumber(font_size) && font_size->valueint > 0 &&
|
||||
font_size->valuedouble == font_size->valueint && cJSON_IsNumber(font_bpp) &&
|
||||
font_bpp->valuedouble == font_bpp->valueint &&
|
||||
(font_bpp->valueint == 1 || font_bpp->valueint == 4) &&
|
||||
font_bpp->valueint == text_font->bpp() && cJSON_IsString(bundle) &&
|
||||
bundle->valuestring[0] != '\0' && std::strlen(bundle->valuestring) <= 64;
|
||||
if (supports_glyph_push) {
|
||||
assets->text_font_capability_ = {
|
||||
.glyph_push = true,
|
||||
.bundle = bundle->valuestring,
|
||||
.charset = charset->valuestring,
|
||||
.size = font_size->valueint,
|
||||
.bpp = font_bpp->valueint,
|
||||
};
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Loaded custom text font without compatible glyph push metadata");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ESP_LOGE(TAG, "The font file %s is not found", fonts_text_file.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
cJSON* emoji_collection = cJSON_GetObjectItem(root, "emoji_collection");
|
||||
if (cJSON_IsArray(emoji_collection)) {
|
||||
auto custom_emoji_collection = std::make_shared<EmojiCollection>();
|
||||
int emoji_count = cJSON_GetArraySize(emoji_collection);
|
||||
for (int i = 0; i < emoji_count; i++) {
|
||||
cJSON* emoji = cJSON_GetArrayItem(emoji_collection, i);
|
||||
if (cJSON_IsObject(emoji)) {
|
||||
cJSON* name = cJSON_GetObjectItem(emoji, "name");
|
||||
cJSON* file = cJSON_GetObjectItem(emoji, "file");
|
||||
cJSON* eaf = cJSON_GetObjectItem(emoji, "eaf");
|
||||
if (cJSON_IsString(name) && cJSON_IsString(file) && (NULL == eaf)) {
|
||||
if (!assets->GetAssetData(file->valuestring, ptr, size)) {
|
||||
ESP_LOGE(TAG, "Emoji %s image file %s is not found", name->valuestring,
|
||||
file->valuestring);
|
||||
continue;
|
||||
}
|
||||
custom_emoji_collection->AddEmoji(name->valuestring,
|
||||
new LvglRawImage(ptr, size));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (light_theme != nullptr) {
|
||||
light_theme->set_emoji_collection(custom_emoji_collection);
|
||||
}
|
||||
if (dark_theme != nullptr) {
|
||||
dark_theme->set_emoji_collection(custom_emoji_collection);
|
||||
}
|
||||
Board::GetInstance().GetDisplay()->SetEmojiCollection(custom_emoji_collection);
|
||||
}
|
||||
|
||||
cJSON* skin = cJSON_GetObjectItem(root, "skin");
|
||||
if (cJSON_IsObject(skin)) {
|
||||
cJSON* light_skin = cJSON_GetObjectItem(skin, "light");
|
||||
if (cJSON_IsObject(light_skin) && light_theme != nullptr) {
|
||||
cJSON* text_color = cJSON_GetObjectItem(light_skin, "text_color");
|
||||
cJSON* background_color = cJSON_GetObjectItem(light_skin, "background_color");
|
||||
cJSON* background_image = cJSON_GetObjectItem(light_skin, "background_image");
|
||||
if (cJSON_IsString(text_color)) {
|
||||
light_theme->set_text_color(LvglTheme::ParseColor(text_color->valuestring));
|
||||
}
|
||||
if (cJSON_IsString(background_color)) {
|
||||
light_theme->set_background_color(
|
||||
LvglTheme::ParseColor(background_color->valuestring));
|
||||
light_theme->set_chat_background_color(
|
||||
LvglTheme::ParseColor(background_color->valuestring));
|
||||
}
|
||||
if (cJSON_IsString(background_image)) {
|
||||
if (!assets->GetAssetData(background_image->valuestring, ptr, size)) {
|
||||
ESP_LOGE(TAG, "The background image file %s is not found",
|
||||
background_image->valuestring);
|
||||
return false;
|
||||
}
|
||||
auto background_image = std::make_shared<LvglCBinImage>(ptr);
|
||||
light_theme->set_background_image(background_image);
|
||||
}
|
||||
}
|
||||
cJSON* dark_skin = cJSON_GetObjectItem(skin, "dark");
|
||||
if (cJSON_IsObject(dark_skin) && dark_theme != nullptr) {
|
||||
cJSON* text_color = cJSON_GetObjectItem(dark_skin, "text_color");
|
||||
cJSON* background_color = cJSON_GetObjectItem(dark_skin, "background_color");
|
||||
cJSON* background_image = cJSON_GetObjectItem(dark_skin, "background_image");
|
||||
if (cJSON_IsString(text_color)) {
|
||||
dark_theme->set_text_color(LvglTheme::ParseColor(text_color->valuestring));
|
||||
}
|
||||
if (cJSON_IsString(background_color)) {
|
||||
dark_theme->set_background_color(
|
||||
LvglTheme::ParseColor(background_color->valuestring));
|
||||
dark_theme->set_chat_background_color(
|
||||
LvglTheme::ParseColor(background_color->valuestring));
|
||||
}
|
||||
if (cJSON_IsString(background_image)) {
|
||||
if (!assets->GetAssetData(background_image->valuestring, ptr, size)) {
|
||||
ESP_LOGE(TAG, "The background image file %s is not found",
|
||||
background_image->valuestring);
|
||||
return false;
|
||||
}
|
||||
auto background_image = std::make_shared<LvglCBinImage>(ptr);
|
||||
dark_theme->set_background_image(background_image);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (refresh_display_theme) {
|
||||
auto display = Board::GetInstance().GetDisplay();
|
||||
ESP_LOGI(TAG, "Refreshing display theme...");
|
||||
|
||||
auto current_theme = display->GetTheme();
|
||||
if (current_theme != nullptr) {
|
||||
display->SetTheme(current_theme);
|
||||
}
|
||||
|
||||
// Parse hide_subtitle configuration
|
||||
cJSON* hide_subtitle = cJSON_GetObjectItem(root, "hide_subtitle");
|
||||
if (cJSON_IsBool(hide_subtitle)) {
|
||||
bool hide = cJSON_IsTrue(hide_subtitle);
|
||||
auto lcd_display = dynamic_cast<LcdDisplay*>(display);
|
||||
if (lcd_display != nullptr) {
|
||||
lcd_display->SetHideSubtitle(hide);
|
||||
ESP_LOGI(TAG, "Set hide_subtitle to %s", hide ? "true" : "false");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_Delete(root);
|
||||
return true;
|
||||
}
|
||||
#endif // HAVE_LVGL
|
||||
|
||||
bool Assets::EmoteStrategy::InitializePartition(Assets* assets) {
|
||||
assets->partition_valid_ = false;
|
||||
|
||||
if (!Assets::FindPartition(assets)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
esp_err_t ret = ESP_ERR_INVALID_STATE;
|
||||
auto display = Board::GetInstance().GetDisplay();
|
||||
auto* emote_display = dynamic_cast<emote::EmoteDisplay*>(display);
|
||||
if (emote_display && emote_display->GetEmoteHandle() != nullptr) {
|
||||
const emote_data_t data = {
|
||||
.type = EMOTE_SOURCE_PARTITION,
|
||||
.source =
|
||||
{
|
||||
.partition_label = PARTITION_LABEL,
|
||||
},
|
||||
.flags =
|
||||
{
|
||||
.mmap_enable = true, // must be true here!!!
|
||||
},
|
||||
};
|
||||
ret = emote_mount_assets(emote_display->GetEmoteHandle(), &data);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Emote display is not initialized");
|
||||
}
|
||||
assets->partition_valid_ = ((ret == ESP_OK) ? true : false);
|
||||
return assets->partition_valid_;
|
||||
}
|
||||
|
||||
void Assets::EmoteStrategy::UnApplyPartition(Assets* assets) {
|
||||
auto display = Board::GetInstance().GetDisplay();
|
||||
auto* emote_display = dynamic_cast<emote::EmoteDisplay*>(display);
|
||||
if (emote_display && emote_display->GetEmoteHandle() != nullptr) {
|
||||
emote_unmount_assets(emote_display->GetEmoteHandle());
|
||||
}
|
||||
(void)assets; // Unused parameter
|
||||
}
|
||||
|
||||
bool Assets::EmoteStrategy::GetAssetData(Assets* assets, const std::string& name, void*& ptr,
|
||||
size_t& size) {
|
||||
auto display = Board::GetInstance().GetDisplay();
|
||||
auto* emote_display = dynamic_cast<emote::EmoteDisplay*>(display);
|
||||
if (emote_display && emote_display->GetEmoteHandle() != nullptr) {
|
||||
const uint8_t* data = nullptr;
|
||||
size_t data_size = 0;
|
||||
if (ESP_OK == emote_get_asset_data_by_name(emote_display->GetEmoteHandle(), name.c_str(),
|
||||
&data, &data_size)) {
|
||||
ptr = const_cast<void*>(static_cast<const void*>(data));
|
||||
size = data_size;
|
||||
return true;
|
||||
}
|
||||
ESP_LOGE(TAG, "Failed to get asset data by name: %s", name.c_str());
|
||||
return false;
|
||||
}
|
||||
(void)assets; // Unused parameter
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Assets::EmoteStrategy::Apply(Assets* assets, bool refresh_display_theme) {
|
||||
Assets::LoadSrmodelsFromIndex(assets);
|
||||
|
||||
auto display = Board::GetInstance().GetDisplay();
|
||||
auto* emote_display = dynamic_cast<emote::EmoteDisplay*>(display);
|
||||
|
||||
if (emote_display && emote_display->GetEmoteHandle() != nullptr) {
|
||||
emote_load_assets(emote_display->GetEmoteHandle());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Assets::Download(std::string url,
|
||||
std::function<void(int progress, size_t speed)> progress_callback) {
|
||||
ESP_LOGI(TAG, "Downloading new version of assets from %s", url.c_str());
|
||||
|
||||
auto network = Board::GetInstance().GetNetwork();
|
||||
auto http = network->CreateHttp(0);
|
||||
|
||||
if (!http->Open("GET", url)) {
|
||||
ESP_LOGE(TAG, "Failed to open HTTP connection");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (http->GetStatusCode() != 200) {
|
||||
ESP_LOGE(TAG, "Failed to get assets, status code: %d", http->GetStatusCode());
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t content_length = http->GetBodyLength();
|
||||
|
||||
if (content_length == 0) {
|
||||
ESP_LOGE(TAG, "Failed to get content length");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (content_length > partition_->size) {
|
||||
ESP_LOGE(TAG, "Assets file size (%u) is larger than partition size (%lu)", content_length,
|
||||
partition_->size);
|
||||
return false;
|
||||
}
|
||||
|
||||
constexpr size_t HEADER_SIZE = 12;
|
||||
|
||||
if (content_length < HEADER_SIZE) {
|
||||
ESP_LOGE(TAG, "Content length (%u) is smaller than header size (%u)", content_length,
|
||||
HEADER_SIZE);
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t SECTOR_SIZE = esp_partition_get_main_flash_sector_size();
|
||||
using BufferPtr = std::unique_ptr<char, decltype(&heap_caps_free)>;
|
||||
|
||||
BufferPtr buffer(static_cast<char*>(heap_caps_malloc(SECTOR_SIZE, MALLOC_CAP_INTERNAL)),
|
||||
&heap_caps_free);
|
||||
|
||||
if (!buffer) {
|
||||
ESP_LOGE(TAG, "Failed to allocate buffer");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Unapply the partition
|
||||
UnApplyPartition();
|
||||
|
||||
size_t sectors_to_erase = (content_length + SECTOR_SIZE - 1) / SECTOR_SIZE;
|
||||
size_t total_erase_size = sectors_to_erase * SECTOR_SIZE;
|
||||
ESP_LOGI(TAG,
|
||||
"Sector size: %u, content length: %u, "
|
||||
"sectors to erase: %u, total erase size: %u",
|
||||
SECTOR_SIZE, content_length, sectors_to_erase, total_erase_size);
|
||||
|
||||
size_t total_written = 0;
|
||||
size_t recent_written = 0;
|
||||
size_t current_sector = 0;
|
||||
|
||||
int64_t last_calc_time = esp_timer_get_time();
|
||||
|
||||
uint8_t header_buf[HEADER_SIZE];
|
||||
size_t header_collected = 0;
|
||||
bool success = false;
|
||||
while (true) {
|
||||
int ret = http->Read(buffer.get(), SECTOR_SIZE);
|
||||
if (ret < 0) {
|
||||
ESP_LOGE(TAG, "Failed to read HTTP data: %s", esp_err_to_name(ret));
|
||||
break;
|
||||
}
|
||||
|
||||
if (ret == 0) {
|
||||
// End of data
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
|
||||
size_t buf_pos = 0;
|
||||
|
||||
// Collect header
|
||||
if (header_collected < HEADER_SIZE) {
|
||||
size_t need = HEADER_SIZE - header_collected;
|
||||
size_t take = std::min(static_cast<size_t>(ret), need);
|
||||
memcpy(header_buf + header_collected, buffer.get(), take);
|
||||
header_collected += take;
|
||||
buf_pos += take;
|
||||
}
|
||||
|
||||
// Write payload
|
||||
if ((size_t)ret > buf_pos) {
|
||||
size_t write_len = (size_t)ret - buf_pos;
|
||||
size_t write_end_offset = HEADER_SIZE + total_written + write_len;
|
||||
size_t needed_sectors = (write_end_offset + SECTOR_SIZE - 1) / SECTOR_SIZE;
|
||||
// Erase sectors
|
||||
bool erase_failed = false;
|
||||
while (current_sector < needed_sectors) {
|
||||
size_t sector_start = current_sector * SECTOR_SIZE;
|
||||
size_t sector_end = sector_start + SECTOR_SIZE;
|
||||
if (sector_end > partition_->size) {
|
||||
ESP_LOGE(TAG, "Sector end (%u) exceeds partition size (%lu)", sector_end,
|
||||
partition_->size);
|
||||
erase_failed = true;
|
||||
break;
|
||||
}
|
||||
ESP_LOGD(TAG, "Erasing sector %u (offset: %u, size: %u)", current_sector,
|
||||
sector_start, SECTOR_SIZE);
|
||||
esp_err_t err = esp_partition_erase_range(partition_, sector_start, SECTOR_SIZE);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to erase sector %u at offset %u: %s", current_sector,
|
||||
sector_start, esp_err_to_name(err));
|
||||
erase_failed = true;
|
||||
break;
|
||||
}
|
||||
current_sector++;
|
||||
}
|
||||
|
||||
if (erase_failed) {
|
||||
break;
|
||||
}
|
||||
|
||||
esp_err_t err = esp_partition_write(partition_, HEADER_SIZE + total_written,
|
||||
buffer.get() + buf_pos, write_len);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to write to assets partition at offset %u: %s",
|
||||
(unsigned int)(HEADER_SIZE + total_written), esp_err_to_name(err));
|
||||
break;
|
||||
}
|
||||
|
||||
total_written += write_len;
|
||||
recent_written += write_len;
|
||||
}
|
||||
|
||||
// Calculate progress
|
||||
if (esp_timer_get_time() - last_calc_time >= 1000000 ||
|
||||
(header_collected + total_written) == content_length) {
|
||||
size_t progress = (header_collected + total_written) * 100 / content_length;
|
||||
size_t speed = recent_written;
|
||||
ESP_LOGI(TAG, "Progress: %u%% (%u/%u), Speed: %u B/s, Sectors erased: %u", progress,
|
||||
(unsigned int)(header_collected + total_written), (unsigned int)content_length,
|
||||
(unsigned int)speed, (unsigned int)current_sector);
|
||||
|
||||
if (progress_callback) {
|
||||
progress_callback(progress, speed);
|
||||
}
|
||||
last_calc_time = esp_timer_get_time();
|
||||
recent_written = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the downloaded size matches the expected size
|
||||
if (success && (header_collected + total_written != content_length)) {
|
||||
ESP_LOGE(TAG, "Downloaded size (%u) does not match expected size (%u)",
|
||||
(unsigned int)(header_collected + total_written), (unsigned int)content_length);
|
||||
success = false;
|
||||
}
|
||||
|
||||
// Write header
|
||||
if (success) {
|
||||
esp_err_t err = esp_partition_write(partition_, 0, header_buf, HEADER_SIZE);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to write assets header to partition: %s", esp_err_to_name(err));
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
ESP_LOGE(TAG, "Assets download failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG,
|
||||
"Header written, assets download completed, total written: %u bytes, total sectors "
|
||||
"erased: %u",
|
||||
(unsigned int)(header_collected + total_written), (unsigned int)current_sector);
|
||||
|
||||
// Re-initialize the assets partition
|
||||
if (!InitializePartition()) {
|
||||
ESP_LOGE(TAG, "Failed to re-initialize assets partition");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
#ifndef ASSETS_H
|
||||
#define ASSETS_H
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <esp_partition.h>
|
||||
#include <cJSON.h>
|
||||
#include <model_path.h>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#if HAVE_LVGL
|
||||
#include <spi_flash_mmap.h>
|
||||
#endif
|
||||
|
||||
struct Asset {
|
||||
size_t size;
|
||||
size_t offset;
|
||||
};
|
||||
|
||||
struct TextFontCapability {
|
||||
bool glyph_push = false;
|
||||
std::string bundle;
|
||||
std::string charset;
|
||||
int size = 0;
|
||||
int bpp = 0;
|
||||
};
|
||||
|
||||
class Assets {
|
||||
public:
|
||||
static Assets& GetInstance() {
|
||||
static Assets instance;
|
||||
return instance;
|
||||
}
|
||||
~Assets();
|
||||
|
||||
bool Download(std::string url,
|
||||
std::function<void(int progress, size_t speed)> progress_callback);
|
||||
bool Apply(bool refresh_display_theme = true);
|
||||
bool GetAssetData(const std::string& name, void*& ptr, size_t& size);
|
||||
|
||||
inline bool partition_valid() const { return partition_valid_; }
|
||||
inline std::string default_assets_url() const { return default_assets_url_; }
|
||||
inline TextFontCapability text_font_capability() const { return text_font_capability_; }
|
||||
|
||||
private:
|
||||
Assets();
|
||||
Assets(const Assets&) = delete;
|
||||
Assets& operator=(const Assets&) = delete;
|
||||
|
||||
bool InitializePartition();
|
||||
void UnApplyPartition();
|
||||
static bool FindPartition(Assets* assets);
|
||||
static bool LoadSrmodelsFromIndex(Assets* assets, cJSON* root = nullptr);
|
||||
void UseBuiltInTextFontCapability();
|
||||
void DisableTextFontGlyphPush();
|
||||
|
||||
class AssetStrategy {
|
||||
public:
|
||||
virtual ~AssetStrategy() = default;
|
||||
virtual bool Apply(Assets* assets, bool refresh_display_theme = true) = 0;
|
||||
virtual bool InitializePartition(Assets* assets) = 0;
|
||||
virtual void UnApplyPartition(Assets* assets) = 0;
|
||||
virtual bool GetAssetData(Assets* assets, const std::string& name, void*& ptr,
|
||||
size_t& size) = 0;
|
||||
};
|
||||
|
||||
class LvglStrategy : public AssetStrategy {
|
||||
public:
|
||||
bool Apply(Assets* assets, bool refresh_display_theme = true) override;
|
||||
bool InitializePartition(Assets* assets) override;
|
||||
void UnApplyPartition(Assets* assets) override;
|
||||
bool GetAssetData(Assets* assets, const std::string& name, void*& ptr,
|
||||
size_t& size) override;
|
||||
|
||||
private:
|
||||
static uint32_t CalculateChecksum(const char* data, uint32_t length);
|
||||
std::map<std::string, Asset> assets_;
|
||||
esp_partition_mmap_handle_t mmap_handle_ = 0;
|
||||
const char* mmap_root_ = nullptr;
|
||||
bool checksum_valid_ = false;
|
||||
};
|
||||
|
||||
class EmoteStrategy : public AssetStrategy {
|
||||
public:
|
||||
bool Apply(Assets* assets, bool refresh_display_theme = true) override;
|
||||
bool InitializePartition(Assets* assets) override;
|
||||
void UnApplyPartition(Assets* assets) override;
|
||||
bool GetAssetData(Assets* assets, const std::string& name, void*& ptr,
|
||||
size_t& size) override;
|
||||
};
|
||||
|
||||
// Strategy instance
|
||||
std::unique_ptr<AssetStrategy> strategy_;
|
||||
|
||||
protected:
|
||||
const esp_partition_t* partition_ = nullptr;
|
||||
bool partition_valid_ = false;
|
||||
std::string default_assets_url_;
|
||||
TextFontCapability text_font_capability_;
|
||||
srmodel_list_t* models_list_ = nullptr;
|
||||
};
|
||||
|
||||
#endif
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"language": {
|
||||
"type": "ar-SA"
|
||||
},
|
||||
"strings": {
|
||||
"WARNING": "تحذير",
|
||||
"INFO": "معلومات",
|
||||
"ERROR": "خطأ",
|
||||
"VERSION": "الإصدار ",
|
||||
"LOADING_PROTOCOL": "الاتصال بالخادم...",
|
||||
"INITIALIZING": "التهيئة...",
|
||||
"PIN_ERROR": "يرجى إدخال بطاقة SIM",
|
||||
"REG_ERROR": "لا يمكن الوصول إلى الشبكة، يرجى التحقق من حالة بطاقة البيانات",
|
||||
"DETECTING_MODULE": "اكتشاف الوحدة...",
|
||||
"REGISTERING_NETWORK": "انتظار الشبكة...",
|
||||
"CHECKING_NEW_VERSION": "فحص الإصدار الجديد...",
|
||||
"CHECK_NEW_VERSION_FAILED": "فشل فحص الإصدار الجديد، سيتم المحاولة خلال %d ثانية: %s",
|
||||
"SWITCH_TO_WIFI_NETWORK": "التبديل إلى Wi-Fi...",
|
||||
"SWITCH_TO_4G_NETWORK": "التبديل إلى 4G...",
|
||||
"STANDBY": "في الانتظار",
|
||||
"CONNECT_TO": "الاتصال بـ ",
|
||||
"CONNECTING": "جاري الاتصال...",
|
||||
"CONNECTED_TO": "متصل بـ ",
|
||||
"LISTENING": "الاستماع...",
|
||||
"SPEAKING": "التحدث...",
|
||||
"SERVER_NOT_FOUND": "البحث عن خدمة متاحة",
|
||||
"SERVER_NOT_CONNECTED": "لا يمكن الاتصال بالخدمة، يرجى المحاولة لاحقاً",
|
||||
"SERVER_TIMEOUT": "انتهت مهلة الاستجابة",
|
||||
"SERVER_ERROR": "فشل الإرسال، يرجى التحقق من الشبكة",
|
||||
"CONNECT_TO_HOTSPOT": "اتصل الهاتف بنقطة الاتصال ",
|
||||
"ACCESS_VIA_BROWSER": "،الوصول عبر المتصفح ",
|
||||
"WIFI_CONFIG_MODE": "وضع تكوين الشبكة",
|
||||
"ENTERING_WIFI_CONFIG_MODE": "الدخول في وضع تكوين الشبكة...",
|
||||
"SCANNING_WIFI": "فحص Wi-Fi...",
|
||||
"NEW_VERSION": "إصدار جديد ",
|
||||
"OTA_UPGRADE": "تحديث OTA",
|
||||
"UPGRADING": "تحديث النظام...",
|
||||
"UPGRADE_FAILED": "فشل التحديث",
|
||||
"ACTIVATION": "تفعيل الجهاز",
|
||||
"BATTERY_LOW": "البطارية منخفضة",
|
||||
"BATTERY_CHARGING": "جاري الشحن",
|
||||
"BATTERY_FULL": "البطارية ممتلئة",
|
||||
"BATTERY_NEED_CHARGE": "البطارية منخفضة، يرجى الشحن",
|
||||
"VOLUME": "الصوت ",
|
||||
"MUTED": "صامت",
|
||||
"MAX_VOLUME": "أقصى صوت",
|
||||
"RTC_MODE_OFF": "AEC مُوقف",
|
||||
"RTC_MODE_ON": "AEC مُشغل",
|
||||
"DOWNLOAD_ASSETS_FAILED": "فشل في تنزيل الموارد",
|
||||
"LOADING_ASSETS": "جاري تحميل الموارد...",
|
||||
"PLEASE_WAIT": "يرجى الانتظار...",
|
||||
"FOUND_NEW_ASSETS": "تم العثور على موارد جديدة: %s",
|
||||
"HELLO_MY_FRIEND": "مرحباً، صديقي!",
|
||||
"CONNECTION_SUCCESSFUL": "تم الاتصال بنجاح",
|
||||
"FLIGHT_MODE_OFF": "وضع الطيران معطل",
|
||||
"FLIGHT_MODE_ON": "وضع الطيران قيد التشغيل",
|
||||
"MODEM_INIT_ERROR": "فشل تهيئة المودم"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user