├── .gitignore ├── .yarn └── releases │ └── yarn-3.6.3.cjs ├── .yarnrc.yml ├── README.md ├── assets ├── icon-128x128.png ├── icon-16x16.png ├── icon-256x256.png ├── icon-32x32.png ├── icon-512x512.png └── icon-64x64.png ├── babel.config.js ├── blog ├── authors.yml └── hellothere.md ├── docs ├── API │ ├── Accelerometer │ │ └── index.md │ ├── CloudAdapter │ │ └── index.md │ ├── Email │ │ └── index.md │ ├── Env Sensor │ │ └── index.md │ ├── Github │ │ └── index.md │ ├── Haptic Output │ │ └── index.md │ ├── Keycap Button │ │ └── index.md │ ├── Light Sensor │ │ └── index.md │ ├── Magnet Sensor │ │ └── index.md │ ├── RGB Ring │ │ └── index.md │ ├── RGB Strip │ │ └── index.md │ ├── Relay │ │ └── index.md │ ├── Rotary Button │ │ └── index.md │ ├── Servo │ │ └── index.md │ ├── Slider │ │ └── index.md │ └── Ultrasonic Sensor │ │ └── index.md ├── app │ ├── 20240313112754.jpg │ ├── 20240313114554.jpg │ └── index.md ├── hardware │ ├── elite60.md │ ├── grapebit.mdx │ ├── index.md │ ├── modules │ │ ├── 01update_firmware.md │ │ ├── 02acc.mdx │ │ ├── 03neopixel.mdx │ │ ├── 04servo.mdx │ │ └── 05aht20.mdx │ └── numkeypad.md ├── intro.md └── skills │ ├── accelerometer │ ├── index.md │ ├── main.ts │ └── skill.json │ ├── distance-siren │ ├── index.md │ ├── main.ts │ └── skill.json │ ├── encoder-rgb │ ├── index.md │ ├── main.ts │ └── skill.json │ ├── getSkills.ts │ ├── index.mdx │ ├── keyboard-animation │ ├── index.md │ ├── main.ts │ └── skill.json │ ├── mqttclient │ ├── index.md │ ├── main.ts │ └── skill.json │ ├── openapp │ ├── index.md │ ├── main.ts │ └── skill.json │ ├── openurl │ ├── index.md │ ├── main.ts │ └── skill.json │ └── slider-volume │ ├── index.md │ ├── main.ts │ └── skill.json ├── docusaurus.config.ts ├── jest.config.js ├── package-lock.json ├── package.json ├── plugin-custom-webpack └── index.ts ├── plugin-generate-skills └── index.ts ├── sidebars.ts ├── skills ├── chatwith.png ├── chatwith_llm.js ├── keyboard_anim.js ├── keyboard_control.js ├── oled_display.js ├── openurl_by_key.js ├── urlopen.png ├── vscode.png └── vscode_shortcut.js ├── src ├── components │ ├── Builder │ │ ├── KeyboardSkills.ts │ │ ├── skill.tsx │ │ ├── skillbuild.module.css │ │ ├── skillbuild.tsx │ │ └── skillconfig.tsx │ ├── Card │ │ ├── Card.tsx │ │ ├── card.module.css │ │ └── index.tsx │ ├── DevsDownload │ │ ├── DevsHost.ts │ │ ├── devs.module.css │ │ ├── index.tsx │ │ └── services.json │ ├── Firmware │ │ ├── espflash.tsx │ │ └── jdflash.tsx │ ├── Hardware │ │ ├── Elite60.css │ │ ├── Elite60.tsx │ │ ├── NumPad.css │ │ └── NumPad.tsx │ ├── HomepageFeatures │ │ ├── index.tsx │ │ └── styles.module.css │ ├── Hostapp │ │ ├── llms.tsx │ │ ├── settings.tsx │ │ └── status.tsx │ ├── Keymap │ │ ├── elite60.tsx │ │ ├── keyboard.css │ │ └── numpad.tsx │ └── codeEditor │ │ ├── index.tsx │ │ └── styles.module.css ├── css │ └── custom.css ├── global.d.ts ├── lib │ ├── DevsHost.ts │ ├── SkillBuild.ts │ ├── codeparse.tsx │ └── services.json ├── pages │ ├── device.tsx │ ├── editor.tsx │ ├── hostapp.module.css │ ├── hostapp.tsx │ ├── hostchat.css │ ├── hostchat.tsx │ ├── index.module.css │ ├── index.tsx │ ├── keymap.css │ ├── keymap.tsx │ ├── markdown-page.md │ ├── showcase.module.css │ ├── showcase.tsx │ └── test.tsx ├── remark │ └── render-skill.ts ├── store │ ├── devsStore.ts │ ├── jacdacStore.ts │ └── skillsStore.ts └── test │ ├── skillbuild.test.ts │ └── testskill.js ├── static ├── .nojekyll ├── assets │ └── .keep ├── firmwares │ ├── app-acc-da213.uf2 │ ├── app-env-aht20.uf2 │ ├── app-neopixel.uf2 │ ├── app-servo.uf2 │ ├── devicescript-esp32c3-kitten_grapebit_c3-0x0.bin │ ├── devicescript-rp2040-kitten_mkc.uf2 │ └── devicescript-rp2040-kitten_numpad.uf2 ├── img │ ├── Maskgroup.png │ ├── Rectangle92.png │ ├── Rectangle93.png │ ├── Rectangle94.png │ ├── Rectangle95.png │ ├── agilewhisker.png │ ├── aircraftWar.mp4 │ ├── ambient.png │ ├── blog │ │ └── modular.gif │ ├── cloud.png │ ├── coomu.png │ ├── docusaurus-social-card.jpg │ ├── docusaurus.png │ ├── elite.svg │ ├── event.png │ ├── favicon.ico │ ├── fourth-bg.svg │ ├── gif_1.gif │ ├── gif_2.gif │ ├── gif_3.gif │ ├── gif_4.gif │ ├── iot.png │ ├── jacdac1.png │ ├── jacdacmodules.svg │ ├── keyboard.svg │ ├── keypad.svg │ ├── monitor.png │ ├── pc.svg │ ├── quickstart │ │ ├── elite60.png │ │ ├── keyboard-sample-1.gif │ │ ├── keyboard-sample-2.png │ │ ├── keypad.png │ │ ├── vscode-1.png │ │ ├── vscode-2.png │ │ ├── vscode-3.png │ │ └── vscode-4.png │ ├── rgb.svg │ ├── screenPic.jpg │ ├── sliderVolume.mp4 │ ├── ultrasonic.mp4 │ ├── undraw_docusaurus_mountain.svg │ ├── undraw_docusaurus_react.svg │ ├── undraw_docusaurus_tree.svg │ └── video.mp4 └── js │ └── .keep ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | .vscode 19 | .nvmrc 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | # Yarn 3 files 25 | .pnp.* 26 | .yarn/* 27 | !.yarn/patches 28 | !.yarn/plugins 29 | !.yarn/releases 30 | !.yarn/sdks 31 | !.yarn/versions 32 | 33 | # generated skills page 34 | skills_page/ 35 | # static skills code 36 | static/js/ 37 | static/assets/ -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | yarnPath: .yarn/releases/yarn-3.6.3.cjs 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | Using SSH: 30 | 31 | ``` 32 | $ USE_SSH=true yarn deploy 33 | ``` 34 | 35 | Not using SSH: 36 | 37 | ``` 38 | $ GIT_USER= yarn deploy 39 | ``` 40 | 41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 42 | -------------------------------------------------------------------------------- /assets/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/assets/icon-128x128.png -------------------------------------------------------------------------------- /assets/icon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/assets/icon-16x16.png -------------------------------------------------------------------------------- /assets/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/assets/icon-256x256.png -------------------------------------------------------------------------------- /assets/icon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/assets/icon-32x32.png -------------------------------------------------------------------------------- /assets/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/assets/icon-512x512.png -------------------------------------------------------------------------------- /assets/icon-64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/assets/icon-64x64.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /blog/authors.yml: -------------------------------------------------------------------------------- 1 | riven: 2 | name: Riven Yang 3 | title: Engineer at Kittenbot 4 | url: https://github.com/xmeow 5 | image_url: https://github.com/xmeow.png 6 | 7 | hogan: 8 | name: Hogan Li 9 | title: Product Manager at Kittenbot 10 | url: https://github.com/hogan96 11 | image_url: https://github.com/hogan96.png 12 | 13 | ccfive: 14 | name: Ccfive 15 | title: CTO at Kittenbot 16 | url: https://github.com/smfox10 17 | image_url: https://github.com/smfox10.png 18 | 19 | -------------------------------------------------------------------------------- /blog/hellothere.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: first-blog-post 3 | title: Hello There 4 | authors: riven 5 | tags: [introduction] 6 | --- 7 | 8 | 9 | ## Exploring the Genesis of Innovation 10 | 11 | We are Kittenbot, a hardware company deeply invested in the field of STEM education. Over the past year, we had the privilege of exploring Microsoft's invention of the Jacdac protocol and the DeviceScript language. Delving into these innovations, we became convinced that the optimal application for DeviceScript lies within PC peripherals. This realization not only marked the beginning of a new journey in product innovation for us but also illuminated a path that integrates modern technology with user needs. We identified an opportunity to create a mechanical keyboard that not only aligns with the ethos of STEM education but also meets everyday usability requirements. This idea ignited our team's creativity, propelling us into the design and development of a brand-new, expandable mechanical keyboard. 12 | 13 | ## Our Design Philosophy: The Software-Defined Keyboard 14 | At the heart of our innovative approach lies a guiding principle: the creation of a software-defined keyboard. This philosophy is driven by our dedication to merging the realms of hardware customization with software flexibility. We envision a keyboard that transcends traditional physical constraints, offering users an unprecedented level of control and personalization. By harnessing the power of software, our keyboard transforms from a mere input device into a dynamic tool that adapts to the unique needs and preferences of each user. 15 | 16 | In embodying this philosophy, we commit to a design that prioritizes innovation, adaptability, and individuality. Our goal is to craft a keyboard that not only meets the highest standards of professional performance but also offers extensive modularity through peripheral modules. Whether for gaming enthusiasts, developers, or designers, our software-defined keyboard opens up a world of possibilities, enabling users to tailor their experience to their specific demands and interests. This vision of a keyboard as a platform for customization and expansion is what sets our design apart, marking a new era in how we interact with our digital environments. 17 | Maskgroup 18 | 19 | ## Features and Modular Overview 20 | Our meticulously designed keyboard not only provides precise tactile feedback but also introduces a series of expandable modules. These modules effortlessly connect with the main body of the keyboard, offering additional functionalities and features. From enhanced audio control modules designed for audiophiles to macro key modules for programmers and gamers, and even quick-access social media keys for the digital socialite, each addition is crafted to allow users to customize their keyboard to match their personal needs and interests. 21 | modular 22 | 23 | ## Continuous Iteration and Improvement 24 | Our design process is an exploratory journey where we continuously iterate and refine our designs based on user feedback and new ideas. Each redesign is a testament to our commitment to perfection, striving not just to meet current user demands but to inspire possibilities. We are dedicated to an ongoing pursuit of innovation, ensuring our keyboard not only answers the needs of today but also anticipates the trends of tomorrow. 25 | 26 | ## Looking to the Future 27 | With the introduction of more module variants, our expandable mechanical keyboard will continue to evolve, offering an unmatched level of customization. We believe that this keyboard will enable every individual to find their unique expression, whether in work or play. 28 | 29 | We are excited for you to join us on this journey with our newly designed expandable mechanical keyboard, to explore the limitless possibilities it brings. Join us in this innovation adventure, as we embark on a new era where technology meets personalization. 30 | 31 | ## Invitation for Your Feedback 32 | We warmly invite you to experience our mechanical keyboard and share your thoughts and suggestions with us. Your feedback is our greatest motivator for continuous progress and innovation. Stay tuned to our social media platforms for more updates, and join our community to exchange insights with fellow innovators. 33 | 34 | Together, let's create the future. -------------------------------------------------------------------------------- /docs/API/Accelerometer/index.md: -------------------------------------------------------------------------------- 1 | # Accelerometer 2 | 3 | A 3-axis accelerometer. 4 | 5 | An accelerometer module should translate acceleration values as follows: 6 | 7 | | Orientation | X value (g) | Y value (g) | Z value (g) | 8 | | --------------------- | ----------- | ----------- | ----------- | 9 | | Module lying flat | 0 | 0 | -1 | 10 | | Module on left edge | -1 | 0 | 0 | 11 | | Module on bottom edge | 0 | 1 | 0 | 12 | 13 | ```typescript 14 | import { Accelerometer } from "@devicescript/core" 15 | 16 | const accelerometer = new Accelerometer() 17 | ``` 18 | 19 | ## Registers 20 | 21 | ### reading 22 | 23 | Indicates the current forces acting on accelerometer. 24 | 25 | * track incoming values 26 | 27 | ```typescript 28 | import { Accelerometer } from "@devicescript/core" 29 | 30 | const accelerometer = new Accelerometer() 31 | 32 | accelerometer.reading.subscribe(async (value) => { 33 | ... 34 | }) 35 | ``` 36 | 37 | ### readingError 38 | 39 | Error on the reading value. 40 | 41 | * read only 42 | 43 | ```typeScript 44 | import { Accelerometer } from "@devicescript/core" 45 | 46 | const accelerometer = new Accelerometer() 47 | 48 | const value = await accelerometer.readingError.read() 49 | ``` 50 | 51 | * track incoming values 52 | 53 | ```typescript 54 | import { Accelerometer } from "@devicescript/core" 55 | 56 | const accelerometer = new Accelerometer() 57 | 58 | accelerometer.readingError.subscribe(async (value) => { 59 | ... 60 | }) 61 | ``` 62 | 63 | ### readingRange 64 | 65 | Configures the range forces detected. The value will be "rounded up" to one of `max_forces_supported`. 66 | 67 | * read and write 68 | 69 | ```typeScript 70 | import { Accelerometer } from "@devicescript/core" 71 | 72 | const accelerometer = new Accelerometer() 73 | // ... 74 | const value = await accelerometer.readingRange.read() 75 | await accelerometer.readingRange.write(value) 76 | ``` 77 | 78 | * track incoming values 79 | 80 | ```typescript 81 | import { Accelerometer } from "@devicescript/core" 82 | 83 | const accelerometer = new Accelerometer() 84 | 85 | accelerometer.readingRange.subscribe(async (value) => { 86 | ... 87 | }) 88 | ``` 89 | 90 | --- 91 | 92 | ## Events 93 | 94 | ### tiltUp 95 | 96 | Emitted when accelerometer is tilted in the given direction. 97 | 98 | ```typescript 99 | accelerometer.tiltUp.subscribe(() => { 100 | 101 | }) 102 | ``` 103 | 104 | ### tiltDown 105 | 106 | Emitted when accelerometer is tilted in the given direction. 107 | 108 | ```typescript 109 | accelerometer.tiltDown.subscribe(()=>{ 110 | 111 | }) 112 | ``` 113 | 114 | ### tiltLeft 115 | 116 | Emitted when accelerometer is tilted in the given direction. 117 | 118 | ```typescript 119 | accelerometer.tiltLeft.subscribe(()=>{ 120 | 121 | }) 122 | ``` 123 | 124 | ### tiltRight 125 | 126 | Emitted when accelerometer is tilted in the given direction. 127 | 128 | ```typescript 129 | accelerometer.tiltRight.subscribe(()=>{ 130 | 131 | }) 132 | ``` 133 | 134 | ### faceUp 135 | 136 | Emitted when accelerometer is laying flat in the given direction. 137 | 138 | ```typescript 139 | accelerometer.faceUp.subscribe(()=>{ 140 | 141 | }) 142 | ``` 143 | 144 | ### faceDown 145 | 146 | Emitted when accelerometer is laying flat in the given direction. 147 | 148 | ```typescript 149 | accelerometer.faceDown.subscribe(()=>{ 150 | 151 | }) 152 | ``` 153 | 154 | ### freefall 155 | 156 | Emitted when total force acting on accelerometer is much less than 1g. 157 | 158 | ```ts 159 | accelerometer.freefall.subscribe(()=>{ 160 | 161 | }) 162 | ``` 163 | 164 | ### shake 165 | 166 | Emitted when forces change violently a few times. 167 | 168 | ```ts 169 | accelerometer.shake.subscribe(()=>{ 170 | 171 | }) 172 | ``` 173 | 174 | ### force2g 175 | 176 | Emitted when force in any direction exceeds given threshold. 177 | 178 | ```ts 179 | accelerometer.force2g.subscribe(()=>{ 180 | 181 | }) 182 | ``` 183 | 184 | ### force3g 185 | 186 | Emitted when force in any direction exceeds given threshold. 187 | 188 | ```ts 189 | accelerometer.force3g.subscribe(()=>{ 190 | 191 | }) 192 | ``` 193 | 194 | ### force6g 195 | 196 | Emitted when force in any direction exceeds given threshold. 197 | 198 | ```ts 199 | accelerometer.force6g.subscribe(()=>{ 200 | 201 | }) 202 | ``` 203 | 204 | ### force8g 205 | 206 | Emitted when force in any direction exceeds given threshold. 207 | 208 | ```ts 209 | accelerometer.force8g.subscribe(()=>{ 210 | 211 | }) 212 | ``` 213 | 214 | --- 215 | 216 | ## help 217 | 218 | [The "deviceScript" contains more information that can be helpful in answering questions related to the device. You can refer to it for better understanding and troubleshooting.](https://microsoft.github.io/devicescript/) 219 | -------------------------------------------------------------------------------- /docs/API/CloudAdapter/index.md: -------------------------------------------------------------------------------- 1 | # Cloud Adapter 2 | 3 | Supports cloud connections to upload and download data. 4 | 5 | ```typescript 6 | import * as ds from "@devicescript/core" 7 | 8 | const client = new ds.MQTTClient() 9 | ``` 10 | 11 | --- 12 | 13 | ## Registers 14 | 15 | ### uploadJson(topic:string, json:string) 16 | 17 | Upload a JSON-encoded message to the cloud. 18 | 19 | ```typeScript 20 | import * as ds from "@devicescript/core" 21 | 22 | const mqtt = new ds.MQTTClient() 23 | 24 | await mqtt.uploadJson('jacdac','') // topic:string, json:string 25 | ``` 26 | 27 | ### uploadBinary(topic:string, bytes:Uint8Array) 28 | 29 | Upload a binary message to the cloud. 30 | 31 | ```typeScript 32 | import * as ds from "@devicescript/core" 33 | 34 | const mqtt = new ds.MQTTClient() 35 | 36 | await mqtt.uploadBinary('jacdac',Uint8Array) // topic:string, bytes:Uint8Array 37 | 38 | ``` 39 | 40 | --- 41 | 42 | ## Events 43 | 44 | ### onJson([topic:string, json:string]) 45 | 46 | Upload a JSON-encoded message to the cloud. 47 | 48 | ```typescript 49 | mqtt.onJson.subscribe(async (json:[topic:string,json:string]) => { 50 | ... 51 | }) 52 | ``` 53 | 54 | ### onBinary([topic:string, payload:Uint8Array]) 55 | 56 | Emitted when cloud send us a binary message. 57 | 58 | ```typescript 59 | mqtt.onBinary.subscribe(async (json:[topic:string,payload:Uint8Array]) => { 60 | ... 61 | }) 62 | ``` 63 | 64 | ## help 65 | 66 | [The "deviceScript" contains more information that can be helpful in answering questions related to the device. You can refer to it for better understanding and troubleshooting.](https://microsoft.github.io/devicescript/) 67 | -------------------------------------------------------------------------------- /docs/API/Email/index.md: -------------------------------------------------------------------------------- 1 | # Email 2 | 3 | Notification for receiving new emails. 4 | 5 | ```typescript 6 | import * as ds from "@devicescript/core" 7 | 8 | const email = new ds.EmailClient() 9 | ``` 10 | 11 | --- 12 | 13 | ## Registers 14 | 15 | ### openListen(body: string) 16 | 17 | Open the new email listener. 18 | 19 | ```typeScript 20 | import * as ds from "@devicescript/core" 21 | 22 | const email = new ds.EmailClient() 23 | const body = JSON.stringify({ 24 | email: '', // email address 25 | password: '', // email password 26 | }) 27 | 28 | await email.openListen(body) 29 | ``` 30 | 31 | ### closeListen() 32 | 33 | Turn off the new email listener. 34 | 35 | 36 | ```typeScript 37 | import * as ds from "@devicescript/core" 38 | 39 | const email = new ds.EmailClient() 40 | 41 | await email.closeListen() 42 | ``` 43 | 44 | --- 45 | 46 | ## Events 47 | 48 | ### listen 49 | 50 | When a new email is detected. 51 | 52 | ```typescript 53 | email.listen.subscribe(() => { 54 | ... 55 | }) 56 | ``` 57 | 58 | ## help 59 | 60 | [The "deviceScript" contains more information that can be helpful in answering questions related to the device. You can refer to it for better understanding and troubleshooting.](https://microsoft.github.io/devicescript/) 61 | -------------------------------------------------------------------------------- /docs/API/Env Sensor/index.md: -------------------------------------------------------------------------------- 1 | # Env Sensor 2 | 3 | Sensor for measuring external environmental humidity and temperature. 4 | 5 | ```typescript 6 | import { Humidity,Temperature } from "@devicescript/core" 7 | 8 | const humidity = new Humidity() 9 | const temperature = new Temperature() 10 | ``` 11 | 12 | --- 13 | 14 | ## Registers 15 | 16 | ### reading 17 | 18 | The temperature. 19 | 20 | * read only 21 | 22 | ```typeScript 23 | import { Humidity,Temperature } from "@devicescript/core" 24 | 25 | const temperature = new Temperature() 26 | const humidity = new Humidity() 27 | 28 | const temp = await temperature.reading.read() 29 | const hum = await humidity.reading.read() 30 | ``` 31 | 32 | * track incoming values 33 | 34 | ```typescript 35 | import { Humidity,Temperature } from "@devicescript/core" 36 | 37 | const temperature = new Temperature() 38 | const humidity = new Humidity() 39 | 40 | temperature.reading.subscribe(async (value) => { 41 | ... 42 | }) 43 | humidity.reading.subscribe(async (value) => { 44 | ... 45 | }) 46 | ``` 47 | 48 | ### minReading 49 | 50 | Lowest temperature that can be reported. 51 | 52 | * read only 53 | 54 | ```typeScript 55 | import { Humidity,Temperature } from "@devicescript/core" 56 | 57 | const temperature = new Temperature() 58 | const humidity = new Humidity() 59 | 60 | const temp = await temperature.minReading.read() 61 | const hum = await humidity.minReading.read() 62 | ``` 63 | 64 | ### maxReading 65 | 66 | Highest temperature that can be reported. 67 | 68 | * read only 69 | 70 | ```typeScript 71 | import { Humidity,Temperature } from "@devicescript/core" 72 | 73 | const temperature = new Temperature() 74 | const humidity = new Humidity() 75 | 76 | const temp = await temperature.maxReading.read() 77 | const hum = await humidity.maxReading.read() 78 | ``` 79 | 80 | ### readingError 81 | 82 | The real temperature is between `temperature - temperature_error` and `temperature + temperature_error`. 83 | 84 | * read only 85 | 86 | ```typeScript 87 | import { Humidity,Temperature } from "@devicescript/core" 88 | 89 | const temperature = new Temperature() 90 | const humidity = new Humidity() 91 | 92 | const temp = await temperature.readingError.read() 93 | const hum = await humidity.readingError.read() 94 | ``` 95 | 96 | * track incoming values 97 | 98 | ```typescript 99 | import { Humidity,Temperature } from "@devicescript/core" 100 | 101 | const temperature = new Temperature() 102 | const humidity = new Humidity() 103 | 104 | temperature.readingError.subscribe(async (value) => { 105 | ... 106 | }) 107 | humidity.readingError.subscribe(async (value) => { 108 | ... 109 | }) 110 | ``` 111 | 112 | ### variant 113 | 114 | Specifies the type of thermometer. 115 | 116 | * read only 117 | 118 | ```typeScript 119 | import { Humidity,Temperature } from "@devicescript/core" 120 | 121 | const temperature = new Temperature() 122 | const humidity = new Humidity() 123 | 124 | const temp = await temperature.variant.read() 125 | const hum = await humidity.variant.read() 126 | ``` 127 | 128 | --- 129 | 130 | ## help 131 | 132 | [The "deviceScript" contains more information that can be helpful in answering questions related to the device. You can refer to it for better understanding and troubleshooting.](https://microsoft.github.io/devicescript/) 133 | -------------------------------------------------------------------------------- /docs/API/Github/index.md: -------------------------------------------------------------------------------- 1 | # Github 2 | 3 | Notification for receiving new emails. 4 | 5 | ```typescript 6 | import * as ds from "@devicescript/core" 7 | 8 | const github = new ds.GithubClient() 9 | ``` 10 | 11 | --- 12 | 13 | ## Registers 14 | 15 | ### requestStatus(body: string) 16 | 17 | Requesting to check the commit status of the GitHub repository. 18 | 19 | ```typeScript 20 | import * as ds from "@devicescript/core" 21 | 22 | const github = new ds.GithubClient() 23 | const body = JSON.stringify({ 24 | owner: "", // Repository owner 25 | repo: "", // Repository name 26 | commitId: "", // commit hash 27 | token: "" // github token 28 | }) 29 | await github.requestStatus(body) 30 | ``` 31 | 32 | --- 33 | 34 | ## Events 35 | 36 | ### listenStatus 37 | 38 | Listen for the commit status returned by GitHub. 39 | 40 | ```typescript 41 | github.listenStatus.subscribe((status) => { 42 | if(status === 'success') { 43 | ... 44 | }else if(status === 'failure') { 45 | ... 46 | }else if(status === 'pending'){ 47 | ... 48 | } 49 | 50 | }) 51 | ``` 52 | 53 | ## help 54 | 55 | [The "deviceScript" contains more information that can be helpful in answering questions related to the device. You can refer to it for better understanding and troubleshooting.](https://microsoft.github.io/devicescript/) 56 | -------------------------------------------------------------------------------- /docs/API/Haptic Output/index.md: -------------------------------------------------------------------------------- 1 | # Haptic Output 2 | 3 | A vibration motor. 4 | 5 | ```ts 6 | import { VibrationMotor } from "@devicescript/core" 7 | 8 | const vibrationMotor = new VibrationMotor() 9 | ``` 10 | 11 | 12 | --- 13 | 14 | 15 | 16 | ## Commands 17 | 18 | ### vibrate 19 | 20 | Starts a sequence of vibration and pauses. To stop any existing vibration, send an empty payload. 21 | 22 | ```ts 23 | vibrationMotor.vibrate(duration:number, intensity:number):Promise 24 | ``` 25 | 26 | 27 | ## Registers 28 | 29 | ### maxVibrations 30 | 31 | The maximum number of vibration sequences supported in a single packet. 32 | 33 | * read only 34 | 35 | ```typescript 36 | import { VibrationMotor } from "@devicescript/core" 37 | 38 | const vibrationMotor = new VibrationMotor() 39 | 40 | const value = await vibrationMotor.maxVibrations.read() 41 | ``` 42 | 43 | --- 44 | 45 | 46 | 47 | ## help 48 | 49 | [The "deviceScript" contains more information that can be helpful in answering questions related to the device. You can refer to it for better understanding and troubleshooting.](https://microsoft.github.io/devicescript/) 50 | -------------------------------------------------------------------------------- /docs/API/Keycap Button/index.md: -------------------------------------------------------------------------------- 1 | # Button 2 | 3 | A push-button, which returns to inactive position when not operated anymore. 4 | 5 | ```typescript 6 | import { Button } from "@devicescript/core" 7 | 8 | const button = new Button() 9 | ``` 10 | 11 | 12 | 13 | --- 14 | 15 | 16 | 17 | ## Registers 18 | 19 | ### reading 20 | 21 | Indicates the pressure state of the button, where `0` is open. 22 | 23 | * read only 24 | 25 | ```typeScript 26 | import { Button } from "@devicescript/core" 27 | 28 | const button = new Button() 29 | 30 | const value = await button.reading.read() 31 | ``` 32 | 33 | * track incoming values 34 | 35 | ```typescript 36 | import { Button } from "@devicescript/core" 37 | 38 | const button = new Button() 39 | // ... 40 | button.reading.subscribe(async (value) => { 41 | ... 42 | }) 43 | ``` 44 | 45 | 46 | 47 | ### analog 48 | 49 | Indicates if the button provides analog `pressure` readings. 50 | 51 | * read only 52 | 53 | ```typeScript 54 | import { Button } from "@devicescript/core" 55 | 56 | const button = new Button() 57 | 58 | const value = await button.analog.read() 59 | ``` 60 | 61 | 62 | ### pressed 63 | 64 | Determines if the button is pressed currently. 65 | 66 | If the event `down` or `hold` is observed, `pressed` becomes true; if `up` is observed, `pressed` becomes false. The client should initialize `pressed` to false. 67 | 68 | * read only 69 | 70 | ```typeScript 71 | import { Button } from "@devicescript/core" 72 | 73 | const button = new Button() 74 | 75 | const value = await button.pressed.read() 76 | ``` 77 | 78 | * track incoming values 79 | 80 | ```typescript 81 | import { Button } from "@devicescript/core" 82 | 83 | const button = new Button() 84 | // ... 85 | button.pressed.subscribe(async (value) => { 86 | ... 87 | }) 88 | ``` 89 | 90 | --- 91 | 92 | ## Events 93 | 94 | ### down 95 | 96 | Emitted when button goes from inactive to active. 97 | 98 | ```typescript 99 | button.down.subscribe(() => { 100 | 101 | }) 102 | ``` 103 | 104 | 105 | ### up 106 | 107 | Emitted when button goes from active to inactive. The 'time' parameter records the amount of time between the down and up events. 108 | 109 | ```typescript 110 | button.up.subscribe(() => { 111 | 112 | }) 113 | ``` 114 | 115 | 116 | ### hold 117 | 118 | Emitted when the press time is greater than 500ms, and then at least every 500ms as long as the button remains pressed. The 'time' parameter records the the amount of time that the button has been held (since the down event). 119 | 120 | ```typescript 121 | button.hold.subscribe(() => { 122 | 123 | }) 124 | ``` 125 | 126 | 127 | --- 128 | 129 | ## help 130 | 131 | [The "deviceScript" contains more information that can be helpful in answering questions related to the device. You can refer to it for better understanding and troubleshooting.](https://microsoft.github.io/devicescript/) 132 | -------------------------------------------------------------------------------- /docs/API/Light Sensor/index.md: -------------------------------------------------------------------------------- 1 | # Light Sensor 2 | 3 | A sensor that measures strength and polarity of magnetic field. 4 | 5 | ```typescript 6 | import { LightLevel } from "@devicescript/core" 7 | 8 | const lightLevel = new LightLevel() 9 | ``` 10 | 11 | --- 12 | 13 | ## Registers 14 | 15 | ### reading 16 | 17 | Detect light level 18 | 19 | * read only 20 | 21 | ```typeScript 22 | import { LightLevel } from "@devicescript/core" 23 | 24 | const lightLevel = new LightLevel() 25 | 26 | const value = await lightLevel.reading.read() 27 | ``` 28 | 29 | * track incoming values 30 | 31 | ```typescript 32 | import { LightLevel } from "@devicescript/core" 33 | 34 | const lightLevel = new LightLevel() 35 | // ... 36 | lightLevel.reading.subscribe(async (value) => { 37 | ... 38 | }) 39 | ``` 40 | 41 | ### readingError 42 | 43 | Absolute estimated error of the reading value 44 | 45 | * read only 46 | 47 | ```typeScript 48 | import { LightLevel } from "@devicescript/core" 49 | 50 | const lightLevel = new LightLevel() 51 | 52 | const value = await lightLevel.readingError.read() 53 | ``` 54 | 55 | * track incoming values 56 | 57 | ```typescript 58 | import { LightLevel } from "@devicescript/core" 59 | 60 | const lightLevel = new LightLevel() 61 | 62 | lightLevel.readingError.subscribe(async (value) => { 63 | ... 64 | }) 65 | ``` 66 | 67 | ### variant 68 | 69 | The type of physical sensor. 70 | 71 | * read only 72 | 73 | ```typescript 74 | import { LightLevel } from "@devicescript/core" 75 | 76 | const lightLevel = new LightLevel() 77 | 78 | const value = await lightLevel.variant.read() 79 | ``` 80 | 81 | --- 82 | 83 | ## help 84 | 85 | [The "deviceScript" contains more information that can be helpful in answering questions related to the device. You can refer to it for better understanding and troubleshooting.](https://microsoft.github.io/devicescript/) 86 | -------------------------------------------------------------------------------- /docs/API/Magnet Sensor/index.md: -------------------------------------------------------------------------------- 1 | # Magnet Sensor 2 | 3 | A sensor that measures strength and polarity of magnetic field. 4 | 5 | ```ts 6 | import { MagneticFieldLevel } from "@devicescript/core" 7 | 8 | const magneticFieldLevel = new MagneticFieldLevel() 9 | ``` 10 | 11 | --- 12 | 13 | ## Registers 14 | 15 | ### reading 16 | 17 | Indicates the strength of magnetic field between -1 and 1. When no magnet is present the value should be around 0. For analog sensors, when the north pole of the magnet is on top of the module and closer than south pole, then the value should be positive. For digital sensors, the value should either `0` or `1`, regardless of polarity. 18 | 19 | * read only 20 | 21 | ```typeScript 22 | import { MagneticFieldLevel } from "@devicescript/core" 23 | 24 | const magneticFieldLevel = new MagneticFieldLevel() 25 | 26 | const value = await magneticFieldLevel.reading.read() 27 | ``` 28 | 29 | * track incoming values 30 | 31 | ```typescript 32 | import { MagneticFieldLevel } from "@devicescript/core" 33 | 34 | const magneticFieldLevel = new MagneticFieldLevel() 35 | // ... 36 | magneticFieldLevel.reading.subscribe(async (value) => { 37 | ... 38 | }) 39 | ``` 40 | 41 | ### detected 42 | 43 | Determines if the magnetic field is present. If the event `active` is observed, `detected` is true; if `inactive` is observed, `detected` is false. 44 | 45 | * read only 46 | 47 | ```typeScript 48 | import { MagneticFieldLevel } from "@devicescript/core" 49 | 50 | const magneticFieldLevel = new MagneticFieldLevel() 51 | 52 | const value = await magneticFieldLevel.detected.read() 53 | ``` 54 | 55 | * track incoming values 56 | 57 | ```typescript 58 | import { MagneticFieldLevel } from "@devicescript/core" 59 | 60 | const magneticFieldLevel = new MagneticFieldLevel() 61 | // ... 62 | magneticFieldLevel.detected.subscribe(async (value) => { 63 | ... 64 | }) 65 | ``` 66 | 67 | ### variant 68 | 69 | Determines which magnetic poles the sensor can detected, and whether or not it can measure their strength or just presence. 70 | 71 | * read only 72 | 73 | ```typescript 74 | import { MagneticFieldLevel } from "@devicescript/core" 75 | 76 | const magneticFieldLevel = new MagneticFieldLevel() 77 | // ... 78 | const value = await magneticFieldLevel.variant.read() 79 | ``` 80 | 81 | --- 82 | 83 | ## Events 84 | 85 | ### active 86 | 87 | Emitted when strong-enough magnetic field is detected. 88 | 89 | ```typescript 90 | magneticFieldLevel.active.subscribe(()=>{ 91 | 92 | }) 93 | ``` 94 | 95 | ### inactive 96 | 97 | Emitted when strong-enough magnetic field is no longer detected. 98 | 99 | ```typescript 100 | magneticFieldLevel.inactive.subscribe(() => { 101 | 102 | }) 103 | ``` 104 | 105 | --- 106 | 107 | ## help 108 | 109 | [The "deviceScript" contains more information that can be helpful in answering questions related to the device. You can refer to it for better understanding and troubleshooting.](https://microsoft.github.io/devicescript/) 110 | -------------------------------------------------------------------------------- /docs/API/RGB Ring/index.md: -------------------------------------------------------------------------------- 1 | # RGB Ring 2 | 3 | A controller for displays of individually controlled RGB LEDs. 4 | 5 | For 64 or less LEDs, the service should support the pack the pixels in the pixels register. Beyond this size, the register should return an empty payload as the amount of data exceeds the size of a packet. Typically services that use more than 64 LEDs will run on the same MCU and will maintain the pixels buffer internally. 6 | 7 | ```typescript 8 | import { Led } from "@devicescript/core" 9 | 10 | const led = new Led() 11 | ``` 12 | 13 | ***Additional runtime support is provided for `Led` by importing the `@devicescript/runtime` package.*** 14 | 15 | ***Please refer to the [LEDs developer documentation](https://microsoft.github.io/devicescript/developer/leds).*** 16 | 17 | ```typescript 18 | import { Led } from "@devicescript/core" 19 | import "@devicescript/runtime" 20 | 21 | const led = new Led() 22 | ``` 23 | 24 | --- 25 | 26 | ## Registers 27 | 28 | ### pixels 29 | 30 | A buffer of 24bit RGB color entries for each LED, in R, G, B order. When writing, if the buffer is too short, the remaining pixels are set to `#000000`; If the buffer is too long, the write may be ignored, or the additional pixels may be ignored. If the number of pixels is greater than `max_pixels_length`, the read should return an empty payload. 31 | 32 | * track incoming values 33 | 34 | ```typescript 35 | import { Led } from "@devicescript/core" 36 | 37 | const led = new Led() 38 | 39 | led.pixels.subscribe(async (value) => { 40 | ... 41 | }) 42 | ``` 43 | 44 | ### intensity 45 | 46 | Set the luminosity of the strip. At `0` the power to the strip is completely shut down. 47 | 48 | * read and write 49 | 50 | ```typeScript 51 | import { Led } from "@devicescript/core" 52 | 53 | const led = new Led() 54 | 55 | const value = await led.intensity.read() 56 | await led.intensity.write(value) 57 | ``` 58 | 59 | * track incoming values 60 | 61 | ```typescript 62 | import { Led } from "@devicescript/core" 63 | 64 | const led = new Led() 65 | 66 | led.intensity.subscribe(async (value) => { 67 | ... 68 | }) 69 | ``` 70 | 71 | ### actualBrightness 72 | 73 | This is the luminosity actually applied to the strip. May be lower than `brightness` if power-limited by the `max_power` register. It will rise slowly (few seconds) back to `brightness` is limits are no longer required. 74 | 75 | * read only 76 | 77 | ```typeScript 78 | import { Led } from "@devicescript/core" 79 | 80 | const led = new Led() 81 | 82 | const value = await led.actualBrightness.read() 83 | ``` 84 | 85 | * track incoming values 86 | 87 | ```typescript 88 | import { Led } from "@devicescript/core" 89 | 90 | const led = new Led() 91 | 92 | led.actualBrightness.subscribe(async (value) => { 93 | ... 94 | }) 95 | ``` 96 | 97 | ### numPixels 98 | 99 | Specifies the number of pixels in the strip. 100 | 101 | * read only 102 | 103 | ```typeScript 104 | import { Led } from "@devicescript/core" 105 | 106 | const led = new Led() 107 | 108 | const value = await led.numPixels.read() 109 | ``` 110 | 111 | ### numColumns 112 | 113 | If the LED pixel strip is a matrix, specifies the number of columns. 114 | 115 | * read only 116 | 117 | ```typeScript 118 | import { Led } from "@devicescript/core" 119 | 120 | const led = new Led() 121 | // ... 122 | const value = await led.numColumns.read() 123 | ``` 124 | 125 | ### maxPower 126 | 127 | Limit the power drawn by the light-strip (and controller). 128 | 129 | * read and write 130 | 131 | ```typeScript 132 | import { Led } from "@devicescript/core" 133 | 134 | const led = new Led() 135 | 136 | const value = await led.maxPower.read() 137 | await led.maxPower.write(value) 138 | ``` 139 | 140 | * track incoming values 141 | 142 | ```typescript 143 | import { Led } from "@devicescript/core" 144 | 145 | const led = new Led() 146 | 147 | led.maxPower.subscribe(async (value) => { 148 | ... 149 | }) 150 | ``` 151 | 152 | ### ledsPerPixel 153 | 154 | If known, specifies the number of LEDs in parallel on this device. The actual number of LEDs is `num_pixels * leds_per_pixel`. 155 | 156 | * read only 157 | 158 | ```typeScript 159 | import { Led } from "@devicescript/core" 160 | 161 | const led = new Led() 162 | 163 | const value = await led.ledsPerPixel.read() 164 | ``` 165 | 166 | ### waveLength 167 | 168 | If monochrome LED, specifies the wave length of the LED. Register is missing for RGB LEDs. 169 | 170 | * read only 171 | 172 | ```typeScript 173 | import { Led } from "@devicescript/core" 174 | 175 | const led = new Led() 176 | 177 | const value = await led.waveLength.read() 178 | ``` 179 | 180 | ### luminousIntensity 181 | 182 | The luminous intensity of all the LEDs, at full brightness, in micro candella. 183 | 184 | * read only 185 | 186 | ```typeScript 187 | import { Led } from "@devicescript/core" 188 | 189 | const led = new Led() 190 | 191 | const value = await led.luminousIntensity.read() 192 | ``` 193 | 194 | ### variant 195 | 196 | Specifies the shape of the light strip. 197 | 198 | * read only 199 | 200 | ```typeScript 201 | import { Led } from "@devicescript/core" 202 | 203 | const led = new Led() 204 | 205 | const value = await led.variant.read() 206 | ``` 207 | 208 | --- 209 | 210 | ## help 211 | 212 | [The "deviceScript" contains more information that can be helpful in answering questions related to the device. You can refer to it for better understanding and troubleshooting.](https://microsoft.github.io/devicescript/) 213 | -------------------------------------------------------------------------------- /docs/API/Relay/index.md: -------------------------------------------------------------------------------- 1 | # Relay 2 | 3 | A switching relay. 4 | 5 | The contacts should be labelled `NO` (normally open), `COM` (common), and `NC` (normally closed). When relay is energized it connects `NO` and `COM`. When relay is not energized it connects `NC` and `COM`. Some relays may be missing `NO` or `NC` contacts. When relay module is not powered, or is in bootloader mode, it is not energized (connects `NC` and `COM`). 6 | 7 | ```ts 8 | import { Relay } from "@devicescript/core" 9 | 10 | const relay = new Relay() 11 | ``` 12 | 13 | 14 | --- 15 | 16 | 17 | 18 | ## Registers 19 | 20 | ### enabled 21 | 22 | Indicates whether the relay circuit is currently energized or not. 23 | 24 | * read and write 25 | 26 | ```typeScript 27 | import { Relay } from "@devicescript/core" 28 | 29 | const relay = new Relay() 30 | 31 | const value = await relay.enabled.read() 32 | await relay.enabled.write(value) 33 | ``` 34 | 35 | * track incoming values 36 | 37 | ```typescript 38 | import { Relay } from "@devicescript/core" 39 | 40 | const relay = new Relay() 41 | 42 | relay.enabled.subscribe(async (value) => { 43 | ... 44 | }) 45 | ``` 46 | 47 | 48 | ### variant 49 | 50 | Describes the type of relay used. 51 | 52 | * read only 53 | 54 | ```typeScript 55 | import { Relay } from "@devicescript/core" 56 | 57 | const relay = new Relay() 58 | 59 | const value = await relay.variant.read() 60 | ``` 61 | 62 | 63 | ### maxSwitchingCurrent 64 | 65 | Maximum switching current for a resistive load. 66 | 67 | * read only 68 | 69 | ```typeScript 70 | import { Relay } from "@devicescript/core" 71 | 72 | const relay = new Relay() 73 | 74 | const value = await relay.maxSwitchingCurrent.read() 75 | ``` 76 | 77 | --- 78 | 79 | 80 | 81 | ## help 82 | 83 | [The "deviceScript" contains more information that can be helpful in answering questions related to the device. You can refer to it for better understanding and troubleshooting.](https://microsoft.github.io/devicescript/) 84 | -------------------------------------------------------------------------------- /docs/API/Rotary Button/index.md: -------------------------------------------------------------------------------- 1 | # Rotary Button 2 | 3 | An incremental rotary encoder - converts angular motion of a shaft to digital signal. 4 | 5 | ```typescript 6 | import { RotaryEncoder } from "@devicescript/core" 7 | 8 | const rotaryEncoder = new RotaryEncoder() 9 | ``` 10 | 11 | --- 12 | 13 | ## Registers 14 | 15 | ### reading 16 | 17 | Upon device reset starts at `0` (regardless of the shaft position). Increases by `1` for a clockwise "click", by `-1` for counter-clockwise. 18 | 19 | * read only 20 | 21 | ```typeScript 22 | import { RotaryEncoder } from "@devicescript/core" 23 | 24 | const rotaryEncoder = new RotaryEncoder() 25 | 26 | const value = await rotaryEncoder.reading.read() 27 | ``` 28 | 29 | * track incoming values 30 | 31 | ```typescript 32 | import { RotaryEncoder } from "@devicescript/core" 33 | 34 | const rotaryEncoder = new RotaryEncoder() 35 | // ... 36 | rotaryEncoder.reading.subscribe(async (value) => { 37 | ... 38 | }) 39 | ``` 40 | 41 | 42 | ### clicksPerTurn 43 | 44 | This specifies by how much `position` changes when the crank does 360 degree turn. Typically 12 or 24. 45 | 46 | * read only 47 | 48 | ```typeScript 49 | import { RotaryEncoder } from "@devicescript/core" 50 | 51 | const rotaryEncoder = new RotaryEncoder() 52 | 53 | const value = await rotaryEncoder.clicksPerTurn.read() 54 | ``` 55 | 56 | 57 | ### clicker 58 | 59 | The encoder is combined with a clicker. If this is the case, the clicker button service should follow this service in the service list of the device. 60 | 61 | * read only 62 | 63 | ```typeScript 64 | import { RotaryEncoder } from "@devicescript/core" 65 | 66 | const rotaryEncoder = new RotaryEncoder() 67 | 68 | const value = await rotaryEncoder.clicker.read() 69 | ``` 70 | 71 | --- 72 | 73 | 74 | 75 | ## help 76 | 77 | [The "deviceScript" contains more information that can be helpful in answering questions related to the device. You can refer to it for better understanding and troubleshooting.](https://microsoft.github.io/devicescript/) 78 | -------------------------------------------------------------------------------- /docs/API/Servo/index.md: -------------------------------------------------------------------------------- 1 | # Servo 2 | 3 | Servo is a small motor with arm that can be pointing at a specific direction. Typically a servo angle is between 0° and 180° where 90° is the middle resting position. 4 | 5 | The `min_pulse/max_pulse` may be read-only if the servo is permanently affixed to its Jacdac controller. 6 | 7 | ```typescript 8 | import { Servo } from "@devicescript/core" 9 | 10 | const servo = new Servo() 11 | ``` 12 | 13 | --- 14 | 15 | ## Registers 16 | 17 | ### angle 18 | 19 | Specifies the angle of the arm (request). 20 | 21 | * read and write 22 | 23 | ```typeScript 24 | import { Servo } from "@devicescript/core" 25 | 26 | const servo = new Servo() 27 | 28 | const value = await servo.angle.read() 29 | await servo.angle.write(value) 30 | ``` 31 | 32 | * track incoming values 33 | 34 | ```typescript 35 | import { Servo } from "@devicescript/core" 36 | 37 | const servo = new Servo() 38 | 39 | servo.angle.subscribe(async (value) => { 40 | ... 41 | }) 42 | ``` 43 | 44 | 45 | ### enabled 46 | 47 | Turn the power to the servo on/off. 48 | 49 | * read and write 50 | 51 | ```typeScript 52 | import { Servo } from "@devicescript/core" 53 | 54 | const servo = new Servo() 55 | 56 | const value = await servo.enabled.read() 57 | await servo.enabled.write(value) 58 | ``` 59 | 60 | * track incoming values 61 | 62 | ```typescript 63 | import { Servo } from "@devicescript/core" 64 | 65 | const servo = new Servo() 66 | 67 | servo.enabled.subscribe(async (value) => { 68 | ... 69 | }) 70 | ``` 71 | 72 | 73 | ### offset 74 | 75 | Correction applied to the angle to account for the servo arm drift. 76 | 77 | * read and write 78 | 79 | ```typeScript 80 | import { Servo } from "@devicescript/core" 81 | 82 | const servo = new Servo() 83 | 84 | const value = await servo.offset.read() 85 | await servo.offset.write(value) 86 | ``` 87 | 88 | * track incoming values 89 | 90 | ```typescript 91 | import { Servo } from "@devicescript/core" 92 | 93 | const servo = new Servo() 94 | 95 | servo.offset.subscribe(async (value) => { 96 | ... 97 | }) 98 | ``` 99 | 100 | 101 | ### minValue 102 | 103 | Lowest angle that can be set, typically 0 °. 104 | 105 | * read only 106 | 107 | ```typeScript 108 | import { Servo } from "@devicescript/core" 109 | 110 | const servo = new Servo() 111 | 112 | const value = await servo.minValue.read() 113 | ``` 114 | 115 | 116 | ### minPulse 117 | 118 | The length of pulse corresponding to lowest angle. 119 | 120 | * read and write 121 | 122 | ```typeScript 123 | import { Servo } from "@devicescript/core" 124 | 125 | const servo = new Servo() 126 | 127 | const value = await servo.minPulse.read() 128 | await servo.minPulse.write(value) 129 | ``` 130 | 131 | * track incoming values 132 | 133 | ```typescript 134 | import { Servo } from "@devicescript/core" 135 | 136 | const servo = new Servo() 137 | 138 | servo.minPulse.subscribe(async (value) => { 139 | ... 140 | }) 141 | ``` 142 | 143 | 144 | ### maxValue 145 | 146 | Highest angle that can be set, typically 180°. 147 | 148 | * read only 149 | 150 | ```typeScript 151 | import { Servo } from "@devicescript/core" 152 | 153 | const servo = new Servo() 154 | // ... 155 | const value = await servo.maxValue.read() 156 | ``` 157 | 158 | 159 | ### maxPulse 160 | 161 | The length of pulse corresponding to highest angle. 162 | 163 | * read and write 164 | 165 | ```typeScript 166 | import { Servo } from "@devicescript/core" 167 | 168 | const servo = new Servo() 169 | 170 | const value = await servo.maxPulse.read() 171 | await servo.maxPulse.write(value) 172 | ``` 173 | 174 | * track incoming values 175 | 176 | ```typescript 177 | import { Servo } from "@devicescript/core" 178 | 179 | const servo = new Servo() 180 | 181 | servo.maxPulse.subscribe(async (value) => { 182 | ... 183 | }) 184 | ``` 185 | 186 | 187 | ### stallTorque 188 | 189 | The servo motor will stop rotating when it is trying to move a `stall_torque` weight at a radial distance of `1.0` cm. 190 | 191 | * read only 192 | 193 | ```typeScript 194 | import { Servo } from "@devicescript/core" 195 | 196 | const servo = new Servo() 197 | 198 | const value = await servo.stallTorque.read() 199 | ``` 200 | 201 | 202 | ### responseSpeed 203 | 204 | Time to move 60°. 205 | 206 | * read only 207 | 208 | ```typeScript 209 | import { Servo } from "@devicescript/core" 210 | 211 | const servo = new Servo() 212 | 213 | const value = await servo.responseSpeed.read() 214 | ``` 215 | 216 | 217 | ### reading 218 | 219 | The current physical position of the arm, if the device has a way to sense the position. 220 | 221 | * read only 222 | 223 | ```typeScript 224 | import { Servo } from "@devicescript/core" 225 | 226 | const servo = new Servo() 227 | 228 | const value = await servo.reading.read() 229 | ``` 230 | 231 | * track incoming values 232 | 233 | ```typescript 234 | import { Servo } from "@devicescript/core" 235 | 236 | const servo = new Servo() 237 | 238 | servo.reading.subscribe(async (value) => { 239 | ... 240 | }) 241 | ``` 242 | 243 | --- 244 | 245 | 246 | 247 | ## help 248 | 249 | [The "deviceScript" contains more information that can be helpful in answering questions related to the device. You can refer to it for better understanding and troubleshooting.](https://microsoft.github.io/devicescript/) 250 | -------------------------------------------------------------------------------- /docs/API/Slider/index.md: -------------------------------------------------------------------------------- 1 | # Slider 2 | 3 | A slider or rotary potentiometer. 4 | 5 | ```typescript 6 | import { Potentiometer } from "@devicescript/core" 7 | 8 | const potentiometer = new Potentiometer() 9 | ``` 10 | 11 | 12 | --- 13 | 14 | 15 | 16 | ## Registers 17 | 18 | ### reading 19 | 20 | The relative position of the slider. 21 | 22 | * read only 23 | 24 | ```typeScript 25 | import { Potentiometer } from "@devicescript/core" 26 | 27 | const potentiometer = new Potentiometer() 28 | 29 | const value = await potentiometer.reading.read() 30 | ``` 31 | 32 | * track incoming values 33 | 34 | ```typescript 35 | import { Potentiometer } from "@devicescript/core" 36 | 37 | const potentiometer = new Potentiometer() 38 | 39 | potentiometer.reading.subscribe(async (value) => { 40 | ... 41 | }) 42 | ``` 43 | 44 | 45 | ### variant 46 | 47 | Specifies the physical layout of the potentiometer. 48 | 49 | * read only 50 | 51 | ```typescript 52 | import { Potentiometer } from "@devicescript/core" 53 | 54 | const potentiometer = new Potentiometer() 55 | // ... 56 | const value = await potentiometer.variant.read() 57 | ``` 58 | 59 | --- 60 | 61 | 62 | 63 | ## help 64 | 65 | [The "deviceScript" contains more information that can be helpful in answering questions related to the device. You can refer to it for better understanding and troubleshooting.](https://microsoft.github.io/devicescript/) 66 | -------------------------------------------------------------------------------- /docs/API/Ultrasonic Sensor/index.md: -------------------------------------------------------------------------------- 1 | # Ultrasonic Sensor 2 | 3 | A sensor that determines the distance of an object without any physical contact involved. 4 | 5 | ```ts 6 | import { Distance } from "@devicescript/core" 7 | 8 | const distance = new Distance() 9 | ``` 10 | 11 | 12 | --- 13 | 14 | 15 | 16 | ## Registers 17 | 18 | ### reading 19 | 20 | Current distance from the object. 21 | 22 | * read only 23 | 24 | ```typeScript 25 | import { Distance } from "@devicescript/core" 26 | 27 | const distance = new Distance() 28 | 29 | const value = await distance.reading.read() 30 | ``` 31 | 32 | * track incoming values 33 | 34 | ```typescript 35 | import { Distance } from "@devicescript/core" 36 | 37 | const distance = new Distance() 38 | 39 | distance.reading.subscribe(async (value) => { 40 | ... 41 | }) 42 | ``` 43 | 44 | 45 | 46 | ### readingError 47 | 48 | Absolute error on the reading value. 49 | 50 | * read only 51 | 52 | ```typeScript 53 | import { Distance } from "@devicescript/core" 54 | 55 | const distance = new Distance() 56 | 57 | const value = await distance.readingError.read() 58 | ``` 59 | 60 | * track incoming values 61 | 62 | ```typescript 63 | import { Distance } from "@devicescript/core" 64 | 65 | const distance = new Distance() 66 | 67 | distance.readingError.subscribe(async (value) => { 68 | ... 69 | }) 70 | ``` 71 | 72 | 73 | ### minReading 74 | 75 | Minimum measurable distance. 76 | 77 | * read only 78 | 79 | ```typeScript 80 | import { Distance } from "@devicescript/core" 81 | 82 | const distance = new Distance() 83 | 84 | const value = await distance.maxReading.read() 85 | ``` 86 | 87 | 88 | ### maxReading 89 | 90 | Maximum measurable distance. 91 | 92 | * read only 93 | 94 | ```typeScript 95 | import { Distance } from "@devicescript/core" 96 | 97 | const distance = new Distance() 98 | 99 | const value = await distance.minReading.read() 100 | ``` 101 | 102 | 103 | ### variant 104 | 105 | Determines the type of sensor used. 106 | 107 | * read only 108 | 109 | ```typeScript 110 | import { Distance } from "@devicescript/core" 111 | 112 | const distance = new Distance() 113 | 114 | const value = await distance.variant.read() 115 | ``` 116 | 117 | 118 | --- 119 | 120 | 121 | 122 | ## help 123 | 124 | [The "deviceScript" contains more information that can be helpful in answering questions related to the device. You can refer to it for better understanding and troubleshooting.](https://microsoft.github.io/devicescript/) 125 | -------------------------------------------------------------------------------- /docs/app/20240313112754.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/docs/app/20240313112754.jpg -------------------------------------------------------------------------------- /docs/app/20240313114554.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/docs/app/20240313114554.jpg -------------------------------------------------------------------------------- /docs/app/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | title: Host App 4 | --- 5 | 6 | ## Introduction 7 | 8 | We also provide a host app for the AgileWhisker Keyboard. The host app is a desktop application that exposes the system abilities to jacdac devices. It is a bridge between the keyboard and the system. The host app is written in typescript and is open source. You can find the source code on [GitHub](https://github.com/kittenbot/agilewhisker-desktop) 9 | 10 | ## Features 11 | 12 | - [x] System information, including CPU, memory, disk, network usage or temperature. 13 | - [x] PC events, including open url, application launch, execute command, etc. 14 | - [x] Build-in MQTT broker, you can use it as a cloud provider in device script. 15 | - [x] Github api, you can use it to get the latest status of your repository. 16 | - [ ] Screen capture and OCR, you can use it to get the text from the screen. 17 | 18 | More features and ideas are welcome, please feel free to open an issue on GitHub. 19 | 20 | ## Usage 21 | 22 | - Download the latest version of the host app from the [release page](https://github.com/kittenbot/agilewhisker-desktop) and install it. 23 | 24 | - Start the host app, you will find a new icon in the system tray. 25 | 26 | - Click the icon to open the host app window. You can toggle the service from the window. 27 | 28 | ![](20240313112754.jpg) 29 | 30 | - Any service you toggle on will be available in the device script. You will also find the host app in the available devices list. 31 | 32 | ![](20240313114554.jpg) 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /docs/hardware/elite60.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 0 3 | --- 4 | 5 | # Elite60 Keyboard 6 | 7 | ## Upgrading the firmware 8 | 9 | 1. Download the lastest [UF2 firmware](/firmwares/devicescript-rp2040-kitten_mkc.uf2) to your computer. 10 | 11 | 2. Connect the keyboard to computure. Hold the `BOOT` button for at least 2 seconds, then release it. It will appear as a USB drive named `RPI-RP2`. 12 | 13 | 3. Drag the firmware file to the `RPI-RP2` drive. 14 | 15 | ## FAQ 16 | 17 | ### How to reset the keyboard? 18 | 19 | A reset operation which will clean the devicescript and reset the key mapping to the default state. 20 | 21 | - Hold the `Encoder` button at the top right corner of the keyboard, then press the `RESET` button at the bottom of the keyboard. 22 | - The first RGB LED in the top left corner will turn yellow then blue. The keyboard will reset. -------------------------------------------------------------------------------- /docs/hardware/grapebit.mdx: -------------------------------------------------------------------------------- 1 | import ESPFlashCard from "@site/src/components/Firmware/espflash"; 2 | 3 | # Grapebit 4 | 5 | ## Upgrade firmware 6 | 1. Please connect the board to your computer. 7 | 8 | 2. Hold down the `B` button and click the `RST` button in the middle of board. 9 | 10 | 3. Then click the `Download` button. 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/hardware/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hardware 3 | sidebar_position: 4 4 | --- 5 | -------------------------------------------------------------------------------- /docs/hardware/modules/01update_firmware.md: -------------------------------------------------------------------------------- 1 | # 01Update Firmware 2 | 3 | ## Introduction 4 | 5 | Some Jacdac modules have the capability to update firmware. They are based on the STM32G0 chip. Therefore, the following four modules support firmware updates: 6 | 7 | 1. Jacdac-Accelerometer 8 | 2. Jacdac-RGB Strip 9 | 3. Jacdac-Servo 10 | 4. Jacdac-Env Sensor 11 | 12 | ![jacdacG0](https://learn.kittenbot.cn/2024md_pic/202402271847277.png) 13 | 14 | 15 | 16 | ## Update Procedure 17 | 18 | 1. Firstly, use the motherboard (which can be a Microbit, keyboard, Grapebit, etc.) to connect the module that requires firmware update. 19 | 20 | > The motherboard firmware should include Jacdac support. 21 | 22 | ![image-20240302142245528](https://learn.kittenbot.cn/2024md_pic/202403021422688.png) 23 | 24 | 2. Open the corresponding module update page and click on "Connect." 25 | 26 | ![image-20240302143153280](https://learn.kittenbot.cn/2024md_pic/202403021431324.png) 27 | 28 | 3. Click on "Update" and wait for the firmware update to complete. 29 | 30 | ![image-20240302142539733](https://learn.kittenbot.cn/2024md_pic/202403021425793.png) 31 | 32 | 33 | 34 | ## FAQ 35 | 36 | Q:The hardware is unable to establish a connection? 37 | 38 | A:1.Check if other webpages are able to establish a connection with the hardware (such as https://makecode.microbit.org/ or https://microsoft.github.io/jacdac-docs/dashboard/). 39 | 40 | 2.The hardware needs to have the Jacdac firmware embedded. 41 | -------------------------------------------------------------------------------- /docs/hardware/modules/02acc.mdx: -------------------------------------------------------------------------------- 1 | import JDFlashCard from "@site/src/components/Firmware/jdflash"; 2 | 3 | # 02Jacdac-Accelerometer 4 | 5 | ![accelerometerv10](https://learn.kittenbot.cn/2024md_pic/202402271852890.jpg) 6 | 7 | 8 | ## Upgrade the firmware 9 | 10 | -------------------------------------------------------------------------------- /docs/hardware/modules/03neopixel.mdx: -------------------------------------------------------------------------------- 1 | import JDFlashCard from "@site/src/components/Firmware/jdflash"; 2 | 3 | # 03Jacdac-RGB Strip 4 | 5 | ![rgbstripv10](https://learn.kittenbot.cn/2024md_pic/202402271853101.jpg) 6 | 7 | 8 | ## Upgrade the firmware 9 | 10 | -------------------------------------------------------------------------------- /docs/hardware/modules/04servo.mdx: -------------------------------------------------------------------------------- 1 | import JDFlashCard from "@site/src/components/Firmware/jdflash"; 2 | 3 | # 04Jacdac-Servo 4 | 5 | ![servov10](https://learn.kittenbot.cn/2024md_pic/202402271853617.jpg) 6 | 7 | 8 | ## Upgrade the firmware 9 | 10 | -------------------------------------------------------------------------------- /docs/hardware/modules/05aht20.mdx: -------------------------------------------------------------------------------- 1 | import JDFlashCard from "@site/src/components/Firmware/jdflash"; 2 | 3 | # 05Jacdac-Env Sensor 4 | 5 | ![envsensorv10](https://learn.kittenbot.cn/2024md_pic/202402271853366.jpg) 6 | 7 | 8 | ## Upgrade the firmware 9 | 10 | -------------------------------------------------------------------------------- /docs/hardware/numkeypad.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Num Keypad 3 | sidebar_position: 1 4 | --- 5 | 6 | # Num Keypad 7 | 8 | 9 | ## Upgrading the firmware 10 | 11 | 1. Download the lastest [UF2 firmware](/firmwares/devicescript-rp2040-kitten_numpad.uf2) to your computer. 12 | 13 | 2. Connect the keyboard to computure. Hold the `BOOT` button for at least 2 seconds, then release it. It will appear as a USB drive named `RPI-RP2`. 14 | 15 | 3. Drag the firmware file to the `RPI-RP2` drive. -------------------------------------------------------------------------------- /docs/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | title: Quick Start 4 | --- 5 | 6 | ## Introduction 7 | 8 | KittenBot’s AgileWhisker Keyboard is a customizable mechanical keyboard that empowers you to unlock new possibilities for PC interactions using DeviceScript. It can be connected to various [Jacdac](https://microsoft.github.io/jacdac-docs/) electronic modules, enabling you to build your own unique application modes based on your creativity. 9 | 10 | ## Why AgileWhisker? 11 | 12 | We believe the best application of DeviceScript is for PC peripherals. Have you ever imagined being able to control everything around you with the keyboard in front of your computer? 13 | 14 | With the AgileWhisker Keyboard, the keyboard and its connected Jacdac functional modules will be further empowered by the Agent desktop software, transforming into dynamic tools that adapt to each user's unique needs and preferences, and is extended with the integration of Jacdac modules. 15 | 16 | We aim to allow software engineers unfamiliar with hardware development to quickly master the skills of writing interactive effects to meet their personal needs and interests. 17 | 18 | ## Features & Specifications 19 | 20 | - Microcontroller: RP2040 21 | - Programmable RGB LEDs on each key 22 | - Jacdac protocol electronic module expansion 23 | - Scripting Language: DeviceScript 24 | - Open-source Agent app provided for continuous expansion of advanced software services for the keyboard 25 | - USB Type-C for firmware flashing and upgrades 26 | - **Elite60 Keyboard** 27 | Elite60 28 | - Dimensions: 293x110x38 mm 29 | - Number of keys: 64 30 | - Number of knobs: 1 31 | - Expandable Jacdac Modules how many and what connector 32 | 33 | - **Keypad** 34 | Elite60 35 | - Dimensions: 83x123x38 mm 36 | - Number of keys: 17 37 | - Number of knobs: 2 38 | - Expandable Jacdac Modules how many and what connector 39 | - 128x64 px OLED 40 | 41 | ## Getting Started with VSCode and DeviceScript 42 | 43 | 1. First, we will need to install the necessary DeviceScript extension in VSCode. 44 | Getting Started with VSCode 1 45 |

46 | 47 | 2. Create a new project and open it (or you can also start with our demo [project](https://github.com/KittenBot/devs-keyboard)). 48 | Getting Started with VSCode 2 49 | Getting Started with VSCode 3 50 |

51 | 52 | 3. After connecting the keyboard using USB, connect the device in the project. 53 | Getting Started with VSCode 4 54 |

55 | 56 | 57 | ## Keyboard Sample 58 | 59 | The flowing colorful lights of a mechanical keyboard are vibrant, but their display modes are limited. With TypeScript editing, we can completely customize a set of flashing combinations that we like. In the following example, we will create a simple RGB that lights up randomly following a key press. 60 | Keyboard Sample 1 61 |

62 | 63 | 1. In our project, write the program in the .ts file (you can give the this file a more intuitive name). First, import the core libraries we need to use. 64 | 65 | ```typescript 66 | import { KeyboardClient, LedStripLightType, LedVariant, gpio } from "@devicescript/core"; 67 | import { startLed } from "@devicescript/drivers"; 68 | import { hid2idx } from "./utils/ledmap"; 69 | 70 | //初始化键盘内led灯 71 | const led = await startLed({ 72 | length: 13*5, 73 | columns: 5, 74 | variant: LedVariant.Strip, 75 | hwConfig: { type: LedStripLightType.WS2812B_GRB, pin: gpio(7) }, 76 | }) 77 | await led.showAll(0) //0为灭灯状态 设置全灭灯 78 | 79 | //初始化键盘 80 | const kb = new KeyboardClient() 81 | //获取等待像素 82 | const pixels = await led.buffer() 83 | 84 | //用于存放最后一次亮灯的位置 85 | let lastLightNum = 0 86 | //用于存放最后一次亮灯的自动灭灯事件 87 | let lastClearLight: number = null 88 | ``` 89 |
90 | 91 | 2. get the keys position through the HidKeyboard class in the core library. 92 | 93 | ```typescript 94 | //监听按键被按下事件,长按会一直触发 95 | kb.down.subscribe(async (key) => { 96 | //hid2idx[key]:被按下按键的位置 97 | if (hid2idx[key]) { 98 | //code 99 | } 100 | } 101 | ``` 102 |
103 | 104 | 3. Complete the code. 105 | 106 | ```typescript 107 | //监听按键被按下事件,长按会一直触发 108 | kb.down.subscribe(async (key) => { 109 | //hid2idx[key]:被按下按键的位置 110 | if (hid2idx[key]) { 111 | if(lastClearLight && lastLightNum===hid2idx[key]){ 112 | //若长按按键则清除灭灯事件 113 | clearTimeout(lastClearLight) 114 | }else{ 115 | let lastLight = hid2idx[key] 116 | //设置 被按下按钮 发光的颜色为红色 117 | pixels.setAt(hid2idx[key], 0xff0000) 118 | // advanced: 将发光的颜色设置为 随机颜色 119 | // pixels.setAt(hid2idx[key], getRandomColor()) 120 | await led.show(); 121 | } 122 | 123 | //设置自动灭灯事件 124 | lastClearLight = setTimeout(async()=>{ 125 | pixels.setAt(hid2idx[key], 0) 126 | await led.show(); 127 | lastClearLight = null 128 | },400) 129 | } 130 | }) 131 | ``` 132 |
133 | 134 | 1. Run the program to test the effect. 135 | Keyboard Sample 1 136 |

-------------------------------------------------------------------------------- /docs/skills/accelerometer/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Accelerometer 3 | --- 4 | ## Introduction 5 | 6 | The skill utilizes an accelerometer to monitor the user's movement input and translates it to the mouse. Additionally, the skill incorporates a button module that translates button presses into left-click actions on the mouse. 7 | 8 | Here is an example of using this skill to play the game "Plane Warfare": 9 | 10 | 11 | 12 | ## Hardware 13 | 14 | - An accelerometer module 15 | - A button module 16 | 17 | ## Try it 18 | 19 | ```devs 20 | 21 | ``` 22 | -------------------------------------------------------------------------------- /docs/skills/accelerometer/main.ts: -------------------------------------------------------------------------------- 1 | import { Accelerometer, PCEvent,Button } from "@devicescript/core"; 2 | 3 | const accelerometer = new Accelerometer() 4 | const pc = new PCEvent() 5 | const btn = new Button() 6 | 7 | // 移动平均滤波的窗口大小 8 | const windowSize = 5; 9 | // 初始化加速度计值数组 10 | const accelerometerValues = [[0,0],[0,0],[0,0],[0,0],[0,0]] 11 | 12 | // 更新加速度计值 13 | function updateAccelerometerValues(newValues: number[]) { 14 | accelerometerValues.shift(); 15 | accelerometerValues.push(newValues); 16 | } 17 | 18 | // 平均加速度计值 19 | function getAverageAccelerometerValues() { 20 | let sumX = 0; 21 | let sumY = 0; 22 | 23 | for (let i = 0; i < windowSize; i++) { 24 | sumX += accelerometerValues[i][0]; 25 | sumY += accelerometerValues[i][1]; 26 | } 27 | 28 | const averageX = sumX / windowSize; 29 | const averageY = sumY / windowSize; 30 | 31 | return [averageX, averageY]; 32 | } 33 | 34 | accelerometer.reading.subscribe(async(value)=>{ 35 | updateAccelerometerValues([value[0],value[1]]) 36 | const averageValues = getAverageAccelerometerValues() 37 | await pc.moveMouse(`${averageValues[0]},${averageValues[1]}`) 38 | }) 39 | 40 | //按钮按下松开,模拟鼠标按下松开 41 | btn.down.subscribe(async()=>{ 42 | await pc.clickMouse('down') 43 | }) 44 | btn.up.subscribe(async()=>{ 45 | await pc.clickMouse('up') 46 | }) -------------------------------------------------------------------------------- /docs/skills/accelerometer/skill.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Accelerometer", 3 | "title": "Accelerometer", 4 | "img": "rgb.svg", 5 | "author": "KittenBot", 6 | "describe": "This skill uses the accelerometer and buttons to control mouse movement and clicks respectively." 7 | } -------------------------------------------------------------------------------- /docs/skills/distance-siren/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Distance Sirens 3 | --- 4 | ## Introduction 5 | 6 | 7 | 8 | ## Hardware 9 | 10 | - An ultrasonic module 11 | - A RGB Ring Module 12 | 13 | ## Try it 14 | 15 | ```devs 16 | 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/skills/distance-siren/main.ts: -------------------------------------------------------------------------------- 1 | import { Distance,Led,Buzzer,sleep } from "@devicescript/core"; 2 | import { Palette } from "@devicescript/graphics"; 3 | import { fillPalette } from "@devicescript/runtime"; 4 | 5 | const ultrasonic = new Distance() 6 | const led = new Led() 7 | const pixels = await led.buffer() 8 | let isFlashing = false 9 | 10 | const flash = async()=>{ 11 | fillPalette(pixels, new Palette(hex`ff0000`)) 12 | await led.show() 13 | await sleep(500) 14 | fillPalette(pixels, new Palette(hex`000000`)) 15 | await led.show() 16 | } 17 | 18 | setInterval(async()=>{ 19 | if(isFlashing) return 20 | let distance = await ultrasonic.reading.read() 21 | if(distance<0.75){ 22 | await flash() 23 | } 24 | },1000) -------------------------------------------------------------------------------- /docs/skills/distance-siren/skill.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Distance Siren", 3 | "title": "Distance Siren", 4 | "img": "rgb.svg", 5 | "author": "KittenBot", 6 | "describe": "This skill is used to monitor whether people are getting too close while using the computer" 7 | } -------------------------------------------------------------------------------- /docs/skills/encoder-rgb/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Encoder RGB 3 | --- 4 | 5 | ## Introduction 6 | 7 | This skill use a encoder to control the RGB LED 8 | 9 | ## Hardware 10 | 11 | - A rotary encoder 12 | - A RGB Ring LED 13 | 14 | ## Try it 15 | 16 | ```devs 17 | ``` -------------------------------------------------------------------------------- /docs/skills/encoder-rgb/main.ts: -------------------------------------------------------------------------------- 1 | import * as ds from "@devicescript/core" 2 | import { fillRainbow, fillPalette } from "@devicescript/runtime" 3 | import { Palette } from "@devicescript/graphics" 4 | 5 | // Use rotary encoder to rotate the rgb led 6 | // 使用旋转编码器旋转rgb led 7 | 8 | const encoder = new ds.RotaryEncoder() 9 | const led = new ds.Led() 10 | const btn = new ds.Button() 11 | 12 | const pixels = await led.buffer() 13 | 14 | pixels.setAt(0, 0xff0000) 15 | await led.show() 16 | 17 | let pos = await encoder.reading.read() 18 | 19 | encoder.reading.subscribe(async (v) => { 20 | if (v !== pos) { 21 | if (v > pos){ 22 | await pixels.rotate(1) 23 | } else { 24 | await pixels.rotate(-1) 25 | } 26 | pos = v 27 | await led.show() 28 | } 29 | }) 30 | 31 | let effect = 0 32 | btn.down.subscribe(async () => { 33 | // toggle different effects 34 | effect = (effect + 1) % 3 35 | if (effect === 0){ 36 | pixels.clear() 37 | pixels.setAt(0, 0xff0000) 38 | await led.show() 39 | } else if (effect === 1) { 40 | fillRainbow(pixels, { 41 | brightness: 64 42 | }) 43 | await led.show() 44 | } else { 45 | const palette = new Palette(hex`1f0000 001f00 00001f 1f1f00 1f001f 00001f 1f1f1f 000000 1f0000 001f00 00001f 1f1f00 1f001f`) 46 | fillPalette(pixels, palette) 47 | await led.show() 48 | } 49 | 50 | }) 51 | -------------------------------------------------------------------------------- /docs/skills/encoder-rgb/skill.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Encoder Rgb", 3 | "title": "Encoder Rgb", 4 | "img": "rgb.svg", 5 | "author": "KittenBot", 6 | "describe": "This skill use a encoder to control the RGB LED" 7 | } -------------------------------------------------------------------------------- /docs/skills/getSkills.ts: -------------------------------------------------------------------------------- 1 | type skillType = { 2 | docId : string 3 | href : string 4 | label : string 5 | type : string 6 | unlisted : boolean 7 | } 8 | 9 | const skills = (items:skillType[])=>{ 10 | return new Promise(async(resolve)=>{ 11 | const skillsJson = [] 12 | const getSkills = async()=>{ 13 | const skillItems = items 14 | for(let i = 0;i 14 | ``` -------------------------------------------------------------------------------- /docs/skills/keyboard-animation/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Keyboard Animation 3 | --- 4 | 5 | ## Introduction 6 | 7 | This skill use a extra button to toggle the animation of the keyboard 8 | 9 | ## Hardware 10 | 11 | - A button module 12 | - Jacdac Mechanical Keyboard 13 | 14 | ## Try it 15 | 16 | ```devs 17 | ``` 18 | 19 | -------------------------------------------------------------------------------- /docs/skills/keyboard-animation/main.ts: -------------------------------------------------------------------------------- 1 | import { LedStripLightType, LedVariant, gpio, Button, sleep } from "@devicescript/core" 2 | import { startLed } from "@devicescript/drivers" 3 | import { fillFade } from "@devicescript/runtime" 4 | 5 | // This is a simple animation that runs on the LED strip. 6 | // Please select one of the patterns below and uncomment it. 7 | 8 | // 这个例子使用一个按键模块来切换动画效果 9 | 10 | const led = await startLed({ 11 | length: 13*5, 12 | columns: 5, 13 | variant: LedVariant.Strip, 14 | hwConfig: { type: LedStripLightType.WS2812B_GRB, pin: gpio(7) }, 15 | }) 16 | await led.showAll(0) 17 | await sleep(1000) 18 | const pixels = await led.buffer() 19 | const btn = new Button() 20 | 21 | pixels.setAt(0, 0xff0000) 22 | fillFade(pixels, 0.2) 23 | await led.show() 24 | let curr = 0 25 | let currAnimation = 0 26 | 27 | const pattern_snake = (len: number, t: number) => { 28 | for (let i=0;i> 1)) % 64; 30 | if (x < 10){ 31 | pixels.setAt(curr, 0x1f0000) 32 | } else if (x >= 15 && x < 25){ 33 | pixels.setAt(curr, 0x001f00) 34 | } else if (x >= 30 && x < 40){ 35 | pixels.setAt(curr, 0x00001f) 36 | } else { 37 | pixels.setAt(curr, 0) 38 | } 39 | curr = (curr + 1) % pixels.length 40 | } 41 | } 42 | 43 | const pattern_sparkle = (len: number, t: number) => { 44 | if (t % 8) 45 | return 46 | for (let i=0;i { 53 | if (t % 8) 54 | return 55 | for (let i=0;i { 62 | const max = 100 63 | t %= max 64 | for (let i=0;i= max) 68 | t = 0 69 | } 70 | } 71 | 72 | btn.down.subscribe(async () => { 73 | currAnimation = (currAnimation + 1) % 4 74 | }) 75 | 76 | let loop = 0; 77 | while(1){ 78 | // change the pattern here 79 | switch(currAnimation){ 80 | case 0: 81 | pattern_snake(64, loop++) 82 | break 83 | case 1: 84 | pattern_sparkle(64, loop++) 85 | break 86 | case 2: 87 | pattern_random(64, loop++) 88 | break 89 | case 3: 90 | pattern_greys(64, loop++) 91 | break 92 | } 93 | 94 | await led.show() 95 | await sleep(10) 96 | } -------------------------------------------------------------------------------- /docs/skills/keyboard-animation/skill.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Keyboard Animation", 3 | "img": "keyboard.svg", 4 | "author": "KittenBot", 5 | "describe": "This skill use a extra button to toggle the animation of the keyboard" 6 | } -------------------------------------------------------------------------------- /docs/skills/mqttclient/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Mqtt Client 3 | --- 4 | ## Introduction 5 | 6 | 7 | ## Try it 8 | 9 | ```devs 10 | 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/skills/mqttclient/main.ts: -------------------------------------------------------------------------------- 1 | import * as ds from "@devicescript/core" 2 | 3 | 4 | const cd = await new ds.CloudAdapter() 5 | 6 | cd.onJson.subscribe((data) => { 7 | console.log("data: "+ data) 8 | }) 9 | 10 | setInterval(async () => { 11 | await cd.uploadJson("jacdac", JSON.stringify({ "hello": "world"})) 12 | }, 3000) -------------------------------------------------------------------------------- /docs/skills/mqttclient/skill.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MqttClient", 3 | "title": "MqttClient", 4 | "img": "pc.svg", 5 | "author": "KittenBot", 6 | "describe": "This skill act as a MQTT client, it can connect to a MQTT server and publish/subscribe messages." 7 | } -------------------------------------------------------------------------------- /docs/skills/openapp/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Open APP 3 | --- 4 | 5 | ## Introduction 6 | 7 | This skill open a application when a button is pressed. You can bind different applications to different buttons. 8 | 9 | Put your full application path in the `APP` variable in the config tab. 10 | 11 | Make sure you have started the PC Controller service in the host application. Once the service is started, you will find the `AW agent` in the list of available devices. 12 | 13 | ## Hardware 14 | 15 | - A button module 16 | 17 | ## Try it 18 | 19 | ```devs 20 | ``` 21 | 22 | -------------------------------------------------------------------------------- /docs/skills/openapp/main.ts: -------------------------------------------------------------------------------- 1 | import * as ds from "@devicescript/core" 2 | 3 | import { startHidMouse } from "@devicescript/servers" 4 | 5 | // Textviewer: use button to open a webpage, then use slider to control mouse wheel 6 | // 文本浏览器: 使用按键打开网页,之后使用slider模块控制鼠标滚轮 7 | 8 | const cd = await new ds.PCEvent() 9 | const btn = new ds.Button() 10 | const slider = new ds.Potentiometer() 11 | const mouse = startHidMouse({}) 12 | 13 | btn.down.subscribe(async () => { 14 | await cd.startApp("$APP") 15 | }) 16 | 17 | while(1){ 18 | let v = await slider.reading.read() 19 | if (v < 0.2){ 20 | await mouse.wheel(-10, 100) 21 | } else if (v > 0.8){ 22 | await mouse.wheel(10, 100) 23 | } 24 | await ds.sleep(300) 25 | } -------------------------------------------------------------------------------- /docs/skills/openapp/skill.json: -------------------------------------------------------------------------------- 1 | { 2 | "params": { 3 | "APP": "/Applications/Notion.app" 4 | }, 5 | "title": "Open APP", 6 | "img": "pc.svg", 7 | "author": "KittenBot", 8 | "describe": "This skill will start an application on the computer." 9 | } -------------------------------------------------------------------------------- /docs/skills/openurl/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Open URL 3 | --- 4 | 5 | ## Introduction 6 | 7 | This skill open an URL when a button is pressed. Which is useful for opening a web page or a file. 8 | 9 | ## How it works 10 | 11 | This skill uses the `open_url` function from the [PC Controller](https://microsoft.github.io/jacdac-docs/services/pccontroller/) class in the device script core library to open the URL. 12 | 13 | You can customize the URL by changing the `$URL` variable in the config tab. Which will be replaced when compiled to program and uploaded to the device. 14 | 15 | 16 | ## Hardware 17 | 18 | - A button module 19 | 20 | ## Try it 21 | 22 | ```devs 23 | ``` 24 | 25 | -------------------------------------------------------------------------------- /docs/skills/openurl/main.ts: -------------------------------------------------------------------------------- 1 | import * as ds from "@devicescript/core" 2 | 3 | import { startHidMouse } from "@devicescript/servers" 4 | 5 | // Textviewer: use button to open a webpage 6 | // 文本浏览器: 使用按键打开网页 7 | 8 | const cd = await new ds.PCEvent() 9 | const btn = new ds.Button() 10 | 11 | btn.down.subscribe(async () => { 12 | await cd.openUrl("$URL") 13 | }) 14 | -------------------------------------------------------------------------------- /docs/skills/openurl/skill.json: -------------------------------------------------------------------------------- 1 | { 2 | "params": { 3 | "URL": "https://microsoft.github.io/jacdac-docs/clients/makecode/" 4 | }, 5 | "title": "Open URL", 6 | "img": "pc.svg", 7 | "author": "KittenBot", 8 | "describe": "This skill open a URL in the browser." 9 | } -------------------------------------------------------------------------------- /docs/skills/slider-volume/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Slider Volume 3 | --- 4 | ## Introduction 5 | 6 | 7 | 8 | ## Hardware 9 | 10 | - An slider module 11 | 12 | ## Try it 13 | 14 | ```devs 15 | 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/skills/slider-volume/main.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HidKeyboardAction as ACT, 3 | HidKeyboardSelector as KEY, 4 | Potentiometer 5 | } from "@devicescript/core"; 6 | import { startHidKeyboard } from "@devicescript/servers"; 7 | 8 | const potentiometer = new Potentiometer() 9 | const keyboard = startHidKeyboard( {} ) 10 | 11 | potentiometer.reading.subscribe( async ( value ) => { 12 | if ( value > 0.9 ) { 13 | await keyboard.key( KEY.VolumeUp, 0, ACT.Press ) 14 | } else if ( value < 0.1 ) { 15 | await keyboard.key( KEY.VolumeDown, 0, ACT.Press ) 16 | } 17 | } ) -------------------------------------------------------------------------------- /docs/skills/slider-volume/skill.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Slider Volume", 3 | "title": "Slider Volume", 4 | "img": "rgb.svg", 5 | "author": "KittenBot", 6 | "describe": "This skill uses a slider to control the volume" 7 | } -------------------------------------------------------------------------------- /docusaurus.config.ts: -------------------------------------------------------------------------------- 1 | // import {themes as prismThemes} from 'prism-react-renderer'; 2 | import type {Config} from '@docusaurus/types'; 3 | import type * as Preset from '@docusaurus/preset-classic'; 4 | import renderSkill from './src/remark/render-skill'; 5 | import pluginSkill from './plugin-generate-skills' 6 | import pluginWebpack from './plugin-custom-webpack' 7 | 8 | const config: Config = { 9 | title: 'AgileWhisker', 10 | tagline: 'AgileWhisker is set of revolutionary PC peripheral widgets that can be programmed with JavaScript.', 11 | favicon: 'img/favicon.ico', 12 | 13 | // Set the production url of your site here 14 | url: 'https://w.kittenbot.cc', 15 | // Set the // pathname under which your site is served 16 | // For GitHub pages deployment, it is often '//' 17 | baseUrl: '/', 18 | 19 | // GitHub pages deployment config. 20 | // If you aren't using GitHub pages, you don't need these. 21 | organizationName: 'Kittenbot', // Usually your GitHub org/user name. 22 | projectName: 'AgileWhisker', // Usually your repo name. 23 | 24 | onBrokenLinks: 'throw', 25 | onBrokenMarkdownLinks: 'warn', 26 | 27 | staticDirectories: [ 28 | 'static', 29 | ], 30 | 31 | // Even if you don't use internationalization, you can use this field to set 32 | // useful metadata like html lang. For example, if your site is Chinese, you 33 | // may want to replace "en" with "zh-Hans". 34 | i18n: { 35 | defaultLocale: 'en', 36 | locales: ['en'], 37 | }, 38 | 39 | presets: [ 40 | [ 41 | 'classic', 42 | { 43 | docs: { 44 | sidebarPath: './sidebars.ts', 45 | beforeDefaultRemarkPlugins: [renderSkill], 46 | // Please change this to your repo. 47 | // Remove this to remove the "edit this page" links. 48 | editUrl: 49 | 'https://github.com/KittenBot/agilewhisker-web/tree/main/', 50 | }, 51 | blog: { 52 | showReadingTime: true, 53 | // Please change this to your repo. 54 | // Remove this to remove the "edit this page" links. 55 | editUrl: 56 | 'https://github.com/KittenBot/agilewhisker-web/tree/main/blog/', 57 | }, 58 | theme: { 59 | customCss: './src/css/custom.css', 60 | }, 61 | } satisfies Preset.Options, 62 | ], 63 | ], 64 | 65 | themeConfig: { 66 | algolia: { 67 | apiKey: '3afbce6f67d86e100945c94519cd0ff5', 68 | indexName: 'w-kittenbot', 69 | appId: 'XDBZ44J9EH', 70 | contextualSearch: false, 71 | }, 72 | // Replace with your project's social card 73 | image: 'img/screenPic.jpg', 74 | colorMode: { 75 | defaultMode: 'dark', 76 | disableSwitch: true, 77 | respectPrefersColorScheme: false, 78 | }, 79 | navbar: { 80 | title: 'AgileWhisker', 81 | logo: { 82 | alt: 'agile-whisker', 83 | src: 'img/agilewhisker.png', 84 | }, 85 | items: [ 86 | { 87 | type: 'docSidebar', 88 | sidebarId: 'tutorialSidebar', 89 | position: 'left', 90 | label: 'Docs', 91 | }, 92 | {to: '/docs/app', label: 'App', position: 'left'}, 93 | {to: '/blog', label: 'Blog', position: 'left'}, 94 | // {to: '/showcase',label: 'Showcase',position:'left' }, 95 | { 96 | href: 'https://www.crowdsupply.com/kittenbot/agilewhisker-keyboard', 97 | label: 'CrowdSupply', 98 | position: 'right' 99 | }, 100 | { 101 | href: 'https://github.com/kittenbot/agilewhisker-web', 102 | label: 'GitHub', 103 | position: 'right' 104 | }, 105 | ], 106 | }, 107 | footer: { 108 | style: 'dark', 109 | links: [ 110 | { 111 | title: 'Docs', 112 | items: [ 113 | { 114 | label: 'Tutorial', 115 | to: '/docs/intro', 116 | }, 117 | ], 118 | }, 119 | { 120 | title: 'Community', 121 | items: [ 122 | { 123 | label: 'Discord', 124 | href: 'https://discord.gg/QSFZknE8', 125 | }, 126 | { 127 | label: 'Twitter', 128 | href: 'https://twitter.com/kittenbot1', 129 | }, 130 | ], 131 | }, 132 | { 133 | title: 'More', 134 | items: [ 135 | { 136 | label: 'Blog', 137 | to: '/blog', 138 | }, 139 | { 140 | label: 'GitHub', 141 | href: 'https://github.com/kittenbot/agilewhisker-web', 142 | }, 143 | ], 144 | }, 145 | ], 146 | copyright: `Copyright © ${new Date().getFullYear()} Kittenbot, Inc. Built with Docusaurus.`, 147 | }, 148 | // prism: { 149 | // theme: prismThemes.github, 150 | // darkTheme: prismThemes.dracula, 151 | // } 152 | } satisfies Preset.ThemeConfig, 153 | plugins: [ 154 | pluginWebpack, 155 | ['@docusaurus/plugin-content-pages', 156 | { 157 | id: 'skills', 158 | path: 'skills_page', 159 | routeBasePath: 'skills', 160 | include: ['**/*.{md,mdx}'], 161 | } 162 | ], 163 | [pluginSkill, { 164 | 165 | }], 166 | ] 167 | }; 168 | 169 | export default config; 170 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | testMatch: ['**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'], 5 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'] 6 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "agilewhisker-web", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc", 16 | "test": "jest" 17 | }, 18 | "dependencies": { 19 | "@ant-design/pro-chat": "^1.14.0", 20 | "@docusaurus/core": "3.0.0", 21 | "@docusaurus/preset-classic": "3.0.0", 22 | "@kittenbot/devs_compiler": "^2.16.1", 23 | "@mdx-js/react": "^3.0.0", 24 | "antd": "^5.11.1", 25 | "antd-style": "^3.6.2", 26 | "clsx": "^1.2.1", 27 | "crypto-js": "^4.2.0", 28 | "esptool-js": "^0.2.1", 29 | "jacdac-ts": "^1.33.7", 30 | "prism-react-renderer": "^2.1.0", 31 | "prismjs": "^1.29.0", 32 | "react": "^18.0.0", 33 | "react-dom": "^18.0.0", 34 | "react-markdown": "^9.0.1", 35 | "react-simple-code-editor": "^0.13.1", 36 | "react-simple-keyboard": "^3.7.40", 37 | "zustand": "^4.4.6" 38 | }, 39 | "devDependencies": { 40 | "@docusaurus/module-type-aliases": "3.0.0", 41 | "@docusaurus/tsconfig": "3.0.0", 42 | "@docusaurus/types": "3.0.0", 43 | "@types/jest": "^29.5.12", 44 | "@types/react": "^18.3.2", 45 | "buffer": "^6.0.3", 46 | "jest": "^29.7.0", 47 | "stream-browserify": "^3.0.0", 48 | "ts-jest": "^29.1.4", 49 | "typescript": "~5.2.2" 50 | }, 51 | "workspaces": [ 52 | "packages/*" 53 | ], 54 | "browserslist": { 55 | "production": [ 56 | ">0.5%", 57 | "not dead", 58 | "not op_mini all" 59 | ], 60 | "development": [ 61 | "last 3 chrome version", 62 | "last 3 firefox version", 63 | "last 5 safari version" 64 | ] 65 | }, 66 | "engines": { 67 | "node": ">=16.0" 68 | }, 69 | "packageManager": "yarn@3.6.3" 70 | } 71 | -------------------------------------------------------------------------------- /plugin-custom-webpack/index.ts: -------------------------------------------------------------------------------- 1 | // custom webpack config, mostly for antd pro-chat dependencies 2 | export default async function webpackFix(context, options) { 3 | return { 4 | name: 'custom-webpack', 5 | configureWebpack(config, isServer, utils) { 6 | return { 7 | resolve: { 8 | fallback: { 9 | buffer: require.resolve('buffer/'), 10 | "stream": require.resolve("stream-browserify") 11 | } 12 | }, 13 | plugins: [ 14 | new (require('webpack')).ProvidePlugin({ 15 | Buffer: ['buffer', 'Buffer'], 16 | }), 17 | ], 18 | }; 19 | }, 20 | }; 21 | }; -------------------------------------------------------------------------------- /plugin-generate-skills/index.ts: -------------------------------------------------------------------------------- 1 | import { SkillConfig, SkillCategory } from '@/lib/SkillBuild'; 2 | import fs from 'fs-extra'; 3 | import path from 'path'; 4 | 5 | export default async function skillGenerate(context, options) { 6 | 7 | // console.log('options', context, options); 8 | return { 9 | name: 'docusaurus-plugin-skills', 10 | async loadContent() { 11 | const _skills: Record = {} 12 | const skillsDirectory = path.join(__dirname, '..', 'skills'); 13 | const entries = fs.readdirSync(skillsDirectory, { withFileTypes: true }); 14 | 15 | const buildSkillPage = (jsCode, filename) => { 16 | let markdown = '' // markdown content 17 | 18 | const awagent = (content) => { 19 | let config: SkillConfig = { 20 | id: 'unknown', 21 | name: 'unknown', 22 | category: 'others', 23 | description: 'Description not available', 24 | } 25 | 26 | config = {...config, ...content} 27 | if (_skills[config.category] === undefined) { 28 | _skills[config.category] = { 29 | name: config.category, 30 | skills: {} 31 | } 32 | } 33 | if (config.thumbnail){ 34 | if (!config.thumbnail.startsWith('http') && fs.existsSync(path.join(skillsDirectory, config.thumbnail))) { 35 | // copy the thumbnail to static/assets 36 | fs.copyFileSync(path.join(skillsDirectory, config.thumbnail), path.join(__dirname, '..', 'static', 'assets', config.thumbnail)) 37 | config.thumbnail = `/assets/${config.thumbnail}` 38 | } 39 | } 40 | if (config.categoryThumbnail){ 41 | if (!config.categoryThumbnail.startsWith('http') && fs.existsSync(path.join(skillsDirectory, config.categoryThumbnail))) { 42 | // copy the thumbnail to static/assets 43 | fs.copyFileSync(path.join(skillsDirectory, config.categoryThumbnail), path.join(__dirname, '..', 'static', 'assets', config.categoryThumbnail)) 44 | if (!config.thumbnail){ 45 | config.thumbnail = `/assets/${config.categoryThumbnail}` 46 | } 47 | _skills[config.category].thumbnail = `/assets/${config.categoryThumbnail}` 48 | } 49 | } 50 | config.jsSrc = `/js/${filename}` 51 | _skills[config.category].skills[config.id] = config 52 | } 53 | 54 | const md = (content) => { 55 | markdown = content 56 | // append docusaurus markdown title 57 | if (!markdown.startsWith('---')) { 58 | markdown = `--- 59 | title: ${content.name} 60 | --- 61 | 62 | ${markdown}` 63 | } 64 | } 65 | 66 | 67 | const evaluateInContext = (js, context) => { 68 | return function() { return eval(js); }.call(context); 69 | } 70 | try { 71 | evaluateInContext(jsCode, { awagent, md }) 72 | } catch (e) { 73 | console.warn(e) 74 | } 75 | } 76 | 77 | entries 78 | .filter(entry => entry.isFile && entry.name.endsWith('.js')) 79 | .map(entry => { 80 | // copy the file to static/skills 81 | fs.copyFileSync(path.join(skillsDirectory, entry.name), path.join(__dirname, '..', 'static', 'js', entry.name)) 82 | const _txt = fs.readFileSync(path.join(skillsDirectory, entry.name), 'utf-8'); 83 | buildSkillPage(_txt, entry.name) 84 | }); 85 | 86 | return _skills; 87 | }, 88 | async contentLoaded({ content, actions }) { 89 | const _skills: Record = content 90 | console.log('contentLoaded', _skills) 91 | const { createData, addRoute } = actions; 92 | 93 | // skills_page for /skills/xxx docs page 94 | if (!fs.existsSync(path.join(__dirname, '..', 'skills_page'))) { 95 | fs.mkdirSync(path.join(__dirname, '..', 'skills_page')) 96 | } 97 | 98 | // TODO: markdown page for each skill or category ?? 99 | // for (const skill of content) { 100 | // const _mdName = skill.name.replace('.js', '.md') 101 | // _skills.push(Object.assign({}, skill.config, { path: `/skills/${_mdName}` })) 102 | // const _path = path.join(__dirname, '..', 'skills_page', _mdName) 103 | // const _mdContent = skill.markdown 104 | // // too much trouble to create a md render, just save to skills_page and let page plugin render it 105 | // if (fs.existsSync(_path)) { 106 | // console.log('file exists', _path) 107 | // // compare content and update if needed 108 | // const _oldContent = fs.readFileSync(_path, 'utf-8') 109 | // if (_oldContent === _mdContent) { 110 | // continue 111 | // } 112 | // fs.writeFileSync(_path, _mdContent) 113 | // } else { 114 | // fs.writeFileSync(_path, _mdContent) 115 | // } 116 | // } 117 | 118 | const skillsJsonPath = await createData('skills.json', JSON.stringify(_skills, null, 2)); 119 | 120 | addRoute({ 121 | path: '/builder', 122 | component: '@site/src/components/Builder/skillbuild.tsx', 123 | exact: true, 124 | modules: { 125 | skills: skillsJsonPath, 126 | } 127 | }) 128 | 129 | }, 130 | }; 131 | }; -------------------------------------------------------------------------------- /sidebars.ts: -------------------------------------------------------------------------------- 1 | import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; 2 | 3 | /** 4 | * Creating a sidebar enables you to: 5 | - create an ordered group of docs 6 | - render a sidebar for each doc of that group 7 | - provide next/previous navigation 8 | 9 | The sidebars can be generated from the filesystem, or explicitly defined here. 10 | 11 | Create as many sidebars as you want. 12 | */ 13 | const sidebars: SidebarsConfig = { 14 | // By default, Docusaurus generates a sidebar from the docs folder structure 15 | tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], 16 | 17 | // But you can create a sidebar manually 18 | /* 19 | tutorialSidebar: [ 20 | 'intro', 21 | 'hello', 22 | { 23 | type: 'category', 24 | label: 'Tutorial', 25 | items: ['tutorial-basics/create-a-document'], 26 | }, 27 | ], 28 | */ 29 | }; 30 | 31 | export default sidebars; 32 | -------------------------------------------------------------------------------- /skills/chatwith.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/skills/chatwith.png -------------------------------------------------------------------------------- /skills/chatwith_llm.js: -------------------------------------------------------------------------------- 1 | const template = ` 2 | import * as ds from "@devicescript/core" 3 | import {regKey} from './keyboarUtils' 4 | 5 | const cd = await new ds.PCEvent() 6 | 7 | regKey(ds.HidKeyboardSelector.#KEY#, async () => { 8 | await cd.runScript("llm://#LLM#?ocr=#OCR#&clip=#CLIP#") 9 | }); 10 | 11 | 12 | ` 13 | 14 | awagent({ 15 | id: 'chatwith_llm', 16 | category: 'PC Event', 17 | name: 'ChatWith', 18 | description: 'This skill listens for a key press and starts a chat with a LLM', 19 | thumbnail: 'chatwith.png', 20 | params: { 21 | 'KEY': { 22 | type: 'string', 23 | description: 'The key to listen for', 24 | default: 'A' 25 | }, 26 | 'LLM': { 27 | type: 'list', 28 | description: 'The LLM to chat with', 29 | default: 'robot2/Robot2_1', 30 | values: 'http://localhost:8081/llm-list' 31 | }, 32 | 'OCR': { 33 | type: 'boolean', 34 | describtion: 'Read user input from OCR', 35 | default: false 36 | }, 37 | 'CLIP': { 38 | type: 'boolean', 39 | describtion: 'Read user input from clipboard', 40 | default: false 41 | } 42 | }, 43 | devs: template, 44 | target: 'keybutton' 45 | 46 | }) -------------------------------------------------------------------------------- /skills/keyboard_anim.js: -------------------------------------------------------------------------------- 1 | 2 | const animationLib = ` 3 | import * as ds from "@devicescript/core" 4 | import { startLed } from "@devicescript/drivers" 5 | 6 | let curr = 0 7 | let _ledBuff 8 | const pattern_snake = (pixels: any, t: number) => { 9 | let len = pixels.length 10 | for (let i=0;i> 1)) % 64; 12 | if (x < 10){ 13 | pixels.setAt(curr, 0x1f0000) 14 | } else if (x >= 15 && x < 25){ 15 | pixels.setAt(curr, 0x001f00) 16 | } else if (x >= 30 && x < 40){ 17 | pixels.setAt(curr, 0x00001f) 18 | } else { 19 | pixels.setAt(curr, 0) 20 | } 21 | curr = (curr + 1) % pixels.length 22 | } 23 | } 24 | 25 | const pattern_sparkle = (pixels: any, t: number) => { 26 | let len = pixels.length 27 | if (t % 8) 28 | return 29 | for (let i=0;i { 36 | let len = pixels.length 37 | if (t % 8) 38 | return 39 | for (let i=0;i { 45 | let len = pixels.length 46 | const max = 100 47 | t %= max 48 | for (let i=0;i= max) 52 | t = 0 53 | } 54 | } 55 | 56 | async function initLED() { 57 | const led = await startLed({ 58 | length: 13*5, 59 | columns: 5, 60 | variant: ds.LedVariant.Strip, 61 | hwConfig: { type: ds.LedStripLightType.WS2812B_GRB, pin: ds.gpio(7) }, 62 | }) 63 | await led.showAll(0) 64 | return led 65 | } 66 | 67 | export { 68 | initLED, 69 | pattern_greys, 70 | pattern_random, 71 | pattern_snake, 72 | pattern_sparkle, 73 | } 74 | ` 75 | 76 | const template = ` 77 | import {initLED, $PATTERN} from './keyboardAnim' 78 | const led = await initLED() 79 | const pixels = await led.buffer() 80 | let _ledLloop = 0 81 | 82 | while (1) { 83 | _ledLloop += 1 84 | $PATTERN(pixels, _ledLloop) 85 | await led.show() 86 | await ds.sleep(10) 87 | } 88 | 89 | ` 90 | 91 | const codeTemplates = { 92 | 'snake': 'pattern_snake', 93 | 'sparkle': 'pattern_sparkle', 94 | 'random': 'pattern_random', 95 | 'greys': 'pattern_greys' 96 | } 97 | 98 | Object.entries(codeTemplates).forEach(([pattern, fun]) => { 99 | awagent({ 100 | id: `keyboard_animation_${pattern}`, 101 | category: 'Animation', 102 | name: pattern, 103 | description: `animation: ${pattern}`, 104 | devs: template.replace(/\$PATTERN/g, fun), 105 | target: 'animation', 106 | libs: { 107 | 'src/keyboardAnim.ts': animationLib 108 | } 109 | }) 110 | }) 111 | 112 | -------------------------------------------------------------------------------- /skills/keyboard_control.js: -------------------------------------------------------------------------------- 1 | const template =` 2 | import * as ds from "@devicescript/core" 3 | import {hidEnable} from './keyboarUtils' 4 | 5 | await hidEnable(#ENABLE#) 6 | ` 7 | 8 | awagent({ 9 | id: 'keyboard_hid_control', 10 | category: 'Keyboard', 11 | name: 'HID Scan control', 12 | description: 'Enable or disable the keyboard HID scan', 13 | params: { 14 | 'ENABLE': { 15 | type: 'boolean', 16 | description: 'Enable or disable the keyboard HID scan', 17 | default: true 18 | } 19 | }, 20 | devs: template, 21 | target: 'misc' 22 | }) -------------------------------------------------------------------------------- /skills/oled_display.js: -------------------------------------------------------------------------------- 1 | 2 | const template = ` 3 | import * as ds from "@devicescript/core" 4 | import {startOLED} from './keyboarUtils' 5 | 6 | const oled = await startOLED() 7 | 8 | oled.image.print("#TEXT#", 3, 1) 9 | await oled.show() 10 | ` 11 | 12 | awagent({ 13 | id: 'oled_display_text', 14 | category: 'OLED Display', 15 | name: 'Display Text', 16 | description: 'Display text on the OLED Display', 17 | params: { 18 | 'TEXT': { 19 | type: 'string', 20 | description: 'The text to display', 21 | default: 'Hello, World!' 22 | } 23 | }, 24 | devs: template, 25 | target: 'oled' 26 | }) 27 | 28 | 29 | -------------------------------------------------------------------------------- /skills/openurl_by_key.js: -------------------------------------------------------------------------------- 1 | 2 | const template = ` 3 | import * as ds from "@devicescript/core" 4 | import {regKey} from './keyboarUtils' 5 | 6 | const cd = await new ds.PCEvent() 7 | 8 | regKey(ds.HidKeyboardSelector.#KEY#, async () => { 9 | await cd.openUrl("#URL#") 10 | }); 11 | 12 | ` 13 | 14 | awagent({ 15 | id: 'key_press_openurl', 16 | category: 'PC Event', 17 | name: 'OpenUrl', 18 | description: 'This skill listens for a key press and opens a URL', 19 | thumbnail: 'urlopen.png', 20 | params: { 21 | 'KEY': { 22 | type: 'string', 23 | description: 'The key to listen for', 24 | default: 'A' 25 | }, 26 | 'URL': { 27 | type: 'string', 28 | description: 'The URL to open', 29 | default: 'https://w.kittenbot.cc' 30 | } 31 | }, 32 | devs: template, 33 | target: 'keybutton' 34 | 35 | }) 36 | -------------------------------------------------------------------------------- /skills/urlopen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/skills/urlopen.png -------------------------------------------------------------------------------- /skills/vscode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/skills/vscode.png -------------------------------------------------------------------------------- /skills/vscode_shortcut.js: -------------------------------------------------------------------------------- 1 | const template = ` 2 | import * as ds from "@devicescript/core" 3 | import {regKey} from './keyboarUtils' 4 | import { startHidKeyboard } from "@devicescript/servers" 5 | 6 | const cd = await new ds.PCEvent() 7 | const keyboard = startHidKeyboard({}) 8 | 9 | regKey(ds.HidKeyboardSelector.#KEY#, async () => { 10 | await keyboard.key($KEY, $MODIFIER, ds.HidKeyboardAction.Press) 11 | }); 12 | ` 13 | 14 | const vscode_shortcut = [ 15 | {id: 'openfile', name: 'Open File', key: 'N'}, 16 | {id: 'openfolder', name: 'Open Folder', key: 'O'}, 17 | {id: 'openproject', name: 'Open Project', key: 'R'}, 18 | {id: 'terminal', name: 'Toggle Terminal', key: 'GraveAccent'}, 19 | {id: 'sidebar', name: 'Toggle Sidebar', key: 'B'}, 20 | {id: 'searchall', name: 'Search All', key: 'F', modifiers: 'ctrl/shift'}, 21 | {id: 'comment', name: 'Comment Line', key: 'Slash'}, 22 | ] 23 | 24 | const modifiers = { 25 | 'ctrl': 'ds.HidKeyboardModifiers.LeftControl', 26 | 'alt/shift': 'ds.HidKeyboardModifiers.LeftAlt + ds.HidKeyboardModifiers.LeftShift', 27 | 'ctrl/shift': 'ds.HidKeyboardModifiers.LeftControl + ds.HidKeyboardModifiers.LeftShift', 28 | } 29 | 30 | for (const item of vscode_shortcut) { 31 | const _key = `ds.HidKeyboardSelector.${item.key}` 32 | const _modifier = modifiers[item.modifiers || 'ctrl'] 33 | const code = template.replace('$KEY', _key).replace('$MODIFIER', _modifier) 34 | awagent({ 35 | id: `vscode_shortcut_${item.id}`, 36 | category: 'VSCode', 37 | categoryThumbnail: 'vscode.png', 38 | name: item.name, 39 | description: `${item.name} in VSCode`, 40 | params: { 41 | 'KEY': { 42 | type: 'string', 43 | description: 'The key to listen for', 44 | default: "A" 45 | } 46 | }, 47 | devs: code, 48 | target: 'keybutton' 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /src/components/Builder/KeyboardSkills.ts: -------------------------------------------------------------------------------- 1 | import { Skill, SkillOpenUrl } from "./skill"; 2 | 3 | 4 | export enum SkillAccessType { 5 | BUTTON = 'BUTTON', 6 | ENCODER = 'ENCODER', 7 | OLED = 'OLED', 8 | } 9 | 10 | export interface SKillMenu { 11 | name: string; 12 | icon?: any; 13 | skills: SkillItem[]; 14 | description?: string; 15 | } 16 | 17 | export interface SkillItem { 18 | type: string; 19 | param?: any; 20 | icon?: any; 21 | accessType?: SkillAccessType; 22 | description: string; 23 | } 24 | 25 | const skillMenus: SKillMenu[] = [ 26 | { 27 | name: 'PC Events', 28 | icon: 'img/event.png', 29 | description: 'Trigger an event on PC', 30 | skills: [ 31 | { 32 | type: 'SkillOpenUrl', 33 | param: {'$URL': 'https://example.com'}, 34 | description: 'Open a URL', 35 | }, 36 | { 37 | type: 'SkillStartApp', 38 | param: {'$APP': '/path/to/app'}, 39 | description: 'Open a app', 40 | }, 41 | { 42 | type: 'SkillSendText', 43 | param: {'$TXT': 'Hello World'}, 44 | description: 'Send text to device', 45 | }, 46 | { 47 | type: 'SkillAiService', 48 | param: {'$SCRIPT': 'my_script', '$PARAMS': 'param1=value1¶m2=value2'}, 49 | description: 'Run AI script', 50 | } 51 | ], 52 | }, 53 | { 54 | name: 'Animation', 55 | icon: 'img/animation.png', 56 | skills: [ 57 | { 58 | type: 'PatternSnake', 59 | description: 'Snake', 60 | }, 61 | { 62 | type: 'PatternSparkle', 63 | description: 'Sparkle effect on random LEDs', 64 | }, 65 | { 66 | type: 'PatternRandom', 67 | description: 'Random color on random LEDs', 68 | }, 69 | ] 70 | }, 71 | { 72 | name: 'System Info OLED', 73 | icon: 'img/oled_sysinfo.png', 74 | description: 'Display system information to OLED', 75 | skills: [ 76 | { 77 | type: 'CpuRamUsage', 78 | description: 'Get CPU Usage', 79 | }, 80 | { 81 | type: 'NetworkInfo', 82 | description: 'Get device info', 83 | }, 84 | { 85 | type: 'SkillGetTransportInfo', 86 | description: 'Get transport info', 87 | }, 88 | ] 89 | }, 90 | { 91 | name: 'Media Control', 92 | icon: 'img/mediacontrol.png', 93 | description: 'Control media on PC', 94 | skills: [ 95 | { 96 | type: 'SkillMediaControl', 97 | param: { 98 | '$ACTION': 'play', 99 | '$CCW': 'volume_up', 100 | '$CW': 'volume_down', 101 | }, 102 | description: 'Media control by Encoder', 103 | }, 104 | ] 105 | }, 106 | 107 | 108 | ] 109 | 110 | export default skillMenus; -------------------------------------------------------------------------------- /src/components/Builder/skill.tsx: -------------------------------------------------------------------------------- 1 | 2 | export class SkillBuilder { 3 | libs: string[] = ['import * as ds from "@devicescript/core"']; 4 | instances: Record = {}; 5 | skills: Skill[] = [] 6 | 7 | addLib(lib: string) { 8 | if (lib.startsWith('import ') || lib.startsWith('from ')) { 9 | this.libs.push(lib) 10 | } else { 11 | this.libs.push(`from ${lib} import *`) 12 | } 13 | } 14 | 15 | addInstance(id: string, instance: string) { 16 | this.instances[id] = instance 17 | } 18 | 19 | buildAll() { 20 | for (const skill of this.skills) { 21 | skill.build(this) 22 | } 23 | let code = '' 24 | for (const lib of this.libs) { 25 | code += `${lib}\n` 26 | } 27 | for (const [id, instance] of Object.entries(this.instances)) { 28 | code += `const ${id} = ${instance}\n` 29 | } 30 | for (const skill of this.skills) { 31 | if (skill.code_generated && skill.parent === undefined) { 32 | code += skill.code_generated 33 | } 34 | } 35 | return code 36 | } 37 | 38 | } 39 | 40 | 41 | export abstract class Skill { 42 | static builder: SkillBuilder 43 | description: string; 44 | thumbnail: string; 45 | devs: string; // device script to run 46 | instance?: string; 47 | libs: string[] = [] // libraries to import in the devicescript builder 48 | params: Record = {}// parameters to replace in the script 49 | code_generated?: string // generated code 50 | parent?: Skill // parent skill 51 | 52 | constructor(public id: string, public name: string, public key0: string, public options: any = {}) { 53 | if (!Skill.builder) { 54 | Skill.builder = new SkillBuilder() 55 | } 56 | if (this.key0) { // the default parameter key, most skill has only one parameter 57 | this.key0 = key0 58 | if (typeof options === 'string') { 59 | this.params = { [this.key0]: options } 60 | this.options = {} 61 | } else if (options instanceof Skill) { 62 | this.params = { [this.key0]: options } 63 | this.options = {} 64 | } 65 | } else { 66 | this.params = { ...this.params, ...options.params } 67 | } 68 | Skill.builder.skills.push(this) 69 | } 70 | 71 | build(builder: SkillBuilder) { 72 | for (const lib of this.libs) { 73 | builder.addLib(lib) 74 | } 75 | if (this.instance) { 76 | builder.addInstance(this.id, this.instance) 77 | } 78 | let code = this.devs 79 | for (const [key, value] of Object.entries(this.params)) { 80 | const re = new RegExp(`\\${key}`, 'g') 81 | if (typeof value === 'string') { 82 | code = code.replace(re, value) 83 | } else if (value instanceof Skill) { 84 | const _event = value as Skill 85 | _event.parent = this 86 | code = code.replace(re, _event.getCode(this.params)) 87 | } 88 | } 89 | this.code_generated = code 90 | } 91 | 92 | getCode(params: Record = {}) { 93 | const _params = { ...this.params, ...params} 94 | let code = this.devs 95 | for (const [key, value] of Object.entries(_params)) { 96 | const re = new RegExp(`\\${key}`, 'g') 97 | code = code.replace(re, value) 98 | } 99 | return code 100 | } 101 | } 102 | 103 | export class SkillButton extends Skill { 104 | event: string; 105 | constructor(name: string, options: any = {}, event: string = 'down') { 106 | super(`button_${name}`, name, '$EVENT', options) 107 | this.instance = `new ds.Button()` 108 | this.devs = `${this.id}.${event}.subscribe(async()=>{\n $EVENT \n})` 109 | if (!this.params['$EVENT']) { 110 | this.params['$EVENT'] = 'console.log("button clicked")' 111 | } 112 | this.description = `${name}->${this.params['$EVENT'].description}` 113 | } 114 | } 115 | 116 | export class SkillOpenUrl extends Skill { 117 | constructor(name: string, options: any = {}) { 118 | super(`cd`, name, '$URL', options) 119 | this.instance = `await new ds.PCEvent()` 120 | this.devs = `await cd.openUrl("$URL")` 121 | this.description = `Open ${this.params['$URL']}` 122 | } 123 | } 124 | 125 | export class SkillStartApp extends Skill { 126 | constructor(name: string, options: any = {}) { 127 | super(`cd`, name, '$APP', options) 128 | this.instance = `await new ds.PCEvent()` 129 | this.devs = `await cd.openApp("$APP")` 130 | this.description = `Open ${this.params['$APP']}` 131 | } 132 | } 133 | 134 | export class SkillAiService extends Skill { 135 | constructor(name: string, options: any = {}) { 136 | super(`cd`, name, '$SCRIPT', options) 137 | this.instance = `await new ds.PCEvent()` 138 | // run ai script with name $SCRIPT, $PARAMS 139 | this.devs = `await cd.runScript("aiscript://$SCRIPT?$PARAMS")` 140 | this.description = `Run AI script` 141 | } 142 | } 143 | 144 | export class SkillSendText extends Skill { 145 | constructor(name: string, options: any = {}) { 146 | super(`cd`, name, '$TXT', options) 147 | this.instance = `await new ds.PCEvent()` 148 | this.devs = `await cd.sendText("$TXT")` 149 | this.description = `Send text` 150 | } 151 | } -------------------------------------------------------------------------------- /src/components/Builder/skillbuild.module.css: -------------------------------------------------------------------------------- 1 | .layout { 2 | height: 100vh; 3 | } 4 | 5 | .sider { 6 | background: #f0f0f0; /* Light background for a friendly look */ 7 | color: #333; 8 | } 9 | 10 | .menu { 11 | background: #f0f0f0; /* Light background for the menu */ 12 | color: #333; 13 | } 14 | 15 | .activeMenuItem { 16 | background: #d9e3f0 !important; /* Active menu item background for a friendly look */ 17 | } 18 | 19 | .deviceSection { 20 | position: absolute; 21 | bottom: 20px; 22 | width: 100%; 23 | } 24 | 25 | .deviceMenuTitle { 26 | width: 100%; 27 | text-align: left; 28 | background: #d9e3f0; /* Soft blue for a friendly look */ 29 | color: #333; 30 | border: none; 31 | } 32 | 33 | .deviceMenu { 34 | background: #e9f5ff; /* Soft blue for device menu */ 35 | color: #333; 36 | } 37 | 38 | .content { 39 | background: #ffffff; /* White background for a clean look */ 40 | color: #333; 41 | padding: 20px; 42 | display: flex; 43 | flex-direction: column; 44 | justify-content: space-between; 45 | } 46 | 47 | .topBar { 48 | display: flex; 49 | justify-content: space-between; 50 | align-items: center; 51 | } 52 | 53 | .searchInput { 54 | width: 200px; 55 | background: #e9f5ff; /* Soft blue for input */ 56 | color: #333; 57 | border: none; 58 | padding: 5px; 59 | } 60 | 61 | .topBarButton { 62 | background: #d9e3f0; /* Soft blue for button */ 63 | color: #333; 64 | border: none; 65 | margin-right: 8px; 66 | } 67 | 68 | .pagination { 69 | display: flex; 70 | justify-content: center; 71 | margin-top: 20px; 72 | } 73 | 74 | .skillCard { 75 | width: 300px; 76 | margin-top: 16px; 77 | } 78 | 79 | .instruction { 80 | text-align: center; 81 | margin-top: 20px; 82 | color: #666; 83 | } 84 | 85 | .rightSider { 86 | background: #f0f0f0; /* Light background for right sider */ 87 | color: #333; 88 | } -------------------------------------------------------------------------------- /src/components/Builder/skillconfig.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Modal, Form, Input, Button, Checkbox, Switch, AutoComplete } from 'antd'; 3 | import { SkillEvent, SkillParam } from '@/lib/SkillBuild'; 4 | import { useSkillsStore } from '../../store/skillsStore'; 5 | 6 | 7 | const SkillInputList = (props: { 8 | param: SkillParam 9 | }) => { 10 | const [options, setOptions] = useState([]) 11 | useEffect(() => { 12 | let values = props.param.values 13 | if (typeof values === 'string' && values.startsWith('http')){ 14 | fetch(values).then(res => res.json()).then(data => { 15 | setOptions(data.map((value: string) => ({ value }))) 16 | }) 17 | } else if (Array.isArray(values)){ 18 | setOptions(values.map((value: string) => ({ value }))) 19 | } 20 | }, []) 21 | 22 | return ( 23 | 26 | option.value.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1 27 | } 28 | /> 29 | ) 30 | } 31 | 32 | export const SkillConfigModal = (props: { 33 | evt: SkillEvent, 34 | handleChange: (params: SkillEvent) => void, 35 | handleDelete: (params: SkillEvent) => void 36 | }) => { 37 | const [form] = Form.useForm(); 38 | const { evt, handleChange, handleDelete } = props 39 | const { skills, getSkill } = useSkillsStore() 40 | const skill = getSkill(evt.id) 41 | 42 | const handleFinish = (values: any) => { 43 | const { params } = values 44 | // rebuild params 45 | const newParams: SkillEvent = Object.assign({}, evt) 46 | Object.entries(skill.params).forEach(([key, param]) => { 47 | if (!param.constant && params[key] !== undefined){ 48 | newParams.params[key] = params[key] 49 | } 50 | }) 51 | console.log(newParams) 52 | handleChange(newParams) 53 | }; 54 | 55 | const onClose = () => { 56 | handleChange(null) 57 | } 58 | 59 | const onDelete = () => { 60 | handleDelete(evt) 61 | } 62 | 63 | return ( 64 | 69 | Delete 70 | , 71 | , 74 | , 77 | ]} 78 | > 79 |

Configuration for {skill.name} @ {evt.key}

80 |
86 | {Object.entries(skill.params || {}).filter(([key, param]) => !param.constant && key !== 'KEY').map(([key, param]) => { 87 | console.log(key, param) 88 | return ( 96 | {param.type === 'boolean' && } 97 | {param.type === 'string' && } 98 | {param.type === 'list' && } 99 | ) 100 | })} 101 |
102 |
103 | ) 104 | } -------------------------------------------------------------------------------- /src/components/Card/Card.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styles from './card.module.css' 3 | import {UserOutlined} from '@ant-design/icons' 4 | 5 | const Card = ({href,image,title,describe,author}:{href:string,image:string,title:string,describe:string,author:string})=>{ 6 | return ( 7 | 21 | ) 22 | } 23 | 24 | export default Card -------------------------------------------------------------------------------- /src/components/Card/card.module.css: -------------------------------------------------------------------------------- 1 | #cardBg { 2 | display: flex; 3 | flex-direction: column; 4 | background-color: #212428; 5 | border: 1px solid rgb(47, 51, 56); 6 | font-weight: 400; 7 | justify-content: flex-start; 8 | overflow: hidden; 9 | transition: box-shadow .15s ease-out,transform .15s ease-out,opacity .15s ease-out; 10 | cursor: pointer; 11 | text-decoration: none; 12 | } 13 | 14 | #cardBg:hover{ 15 | box-shadow: 0 3px 1px -2px rgba(200, 200, 200, 0.2), 0 2px 2px 0 rgba(200, 200, 200,.14), 0 1px 5px 0 rgba(200, 200, 200,.12); 16 | } 17 | 18 | #cardContainer { 19 | display: flex; 20 | flex-direction: column; 21 | align-items: flex-start; 22 | row-gap: 2rem; 23 | padding: 1.5rem; 24 | box-sizing: border-box; 25 | margin-bottom: 0; 26 | } 27 | 28 | #cardHeader { 29 | display: flex; 30 | column-gap: 2rem; 31 | width: 100%; 32 | height: 80px; 33 | } 34 | 35 | #cardInfo { 36 | display: flex; 37 | flex-direction: column; 38 | justify-content: space-between; 39 | align-items: flex-start; 40 | } 41 | 42 | #cardInfo h1 { 43 | color: #FFF; 44 | text-align: start; 45 | font-size: 20px; 46 | font-style: normal; 47 | font-weight: 600; 48 | line-height: normal; 49 | margin-bottom: 0; 50 | } 51 | 52 | #cardAuthor { 53 | display: flex; 54 | justify-content: center; 55 | align-items: center; 56 | column-gap: 10px; 57 | color: #FFF; 58 | font-size: 14px; 59 | font-style: normal; 60 | font-weight: 400; 61 | line-height: normal; 62 | border-radius: 213px; 63 | background: #7247EE; 64 | padding: 6px 10px; 65 | } 66 | 67 | #cardIcon { 68 | width: 5rem; 69 | height: 5rem; 70 | border-radius: 50%; 71 | } 72 | 73 | #cardWithoutImg #cardTitle{ 74 | margin-bottom: 0.3em; 75 | } 76 | 77 | #cardTitle { 78 | color: #d2d2d2; 79 | font-size: 1rem; 80 | font-weight: 600; 81 | line-height: 1.4; 82 | } 83 | 84 | #cardContent { 85 | color: #FFF; 86 | font-size: 1rem; 87 | font-style: normal; 88 | font-weight: 400; 89 | line-height: normal; 90 | margin-bottom: 0; 91 | text-align: start; 92 | width: 370px; 93 | white-space: nowrap; /* 防止文本换行 */ 94 | overflow: hidden; /* 隐藏超出部分的文本 */ 95 | text-overflow: ellipsis; /* 显示省略号 */ 96 | } 97 | 98 | #cardsBox { 99 | display: grid; 100 | grid-template-columns: 408px 408px; 101 | grid-template-rows: 178px; 102 | gap: 10px; 103 | } -------------------------------------------------------------------------------- /src/components/Card/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import styles from './card.module.css' 3 | import {useCurrentSidebarCategory} from '@docusaurus/theme-common' 4 | import Card from './Card' 5 | 6 | export default function Cards({getSkills}): JSX.Element { 7 | const [skills,setSkills] = useState([]) 8 | const [loading,setLoading] = useState(true) 9 | 10 | const items = useCurrentSidebarCategory().items 11 | 12 | useEffect(() => { 13 | if (getSkills && loading) { 14 | getSkills(items).then(allSkills => { 15 | setSkills(allSkills); 16 | setLoading(false); 17 | }); 18 | } 19 | }, [getSkills, loading, items]) 20 | 21 | return ( 22 |
23 | {skills.map(skill=>{ 24 | const newKey = skill.name+(Math.random()*100).toFixed(6) 25 | return 26 | })} 27 |
28 | ) 29 | } -------------------------------------------------------------------------------- /src/components/DevsDownload/DevsHost.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Host, 3 | resolveBuildConfig, 4 | } from "@kittenbot/devs_compiler"; 5 | 6 | import extrea_services from './services.json' 7 | 8 | // Device script implementation for kitten extension 9 | export class DevsHost implements Host { 10 | private files: {}; 11 | private cfg: any; 12 | bytecode: Uint8Array; 13 | 14 | constructor(opt: any = {}) { 15 | this.files = opt.files || {}; 16 | this.cfg = resolveBuildConfig(); 17 | this.cfg.hwInfo = opt.hwInfo || { 18 | progName: "hello", 19 | progVersion: "0.0.1", 20 | }; 21 | this.cfg.addServices = extrea_services 22 | this.cfg.services = [...this.cfg.services, ...extrea_services] 23 | } 24 | 25 | write(filename: string, contents: string | Uint8Array): void { 26 | console.log("write", filename) 27 | this.files[filename] = contents; 28 | } 29 | read(filename: string): string { 30 | console.log("read", filename) 31 | if (this.files.hasOwnProperty(filename)) { 32 | return this.files[filename] as string; 33 | } 34 | throw new Error("No such file: " + filename); 35 | } 36 | resolvePath(p: string) { 37 | console.log("resolve", p) 38 | return p; 39 | } 40 | 41 | log(msg: string): void { 42 | console.log(msg); 43 | } 44 | 45 | verifyBytecode(buf: Uint8Array): void { 46 | // TODO: vm to verify bytecode?? 47 | this.bytecode = buf; 48 | return; 49 | } 50 | 51 | getConfig() { 52 | return this.cfg; 53 | } 54 | } -------------------------------------------------------------------------------- /src/components/DevsDownload/devs.module.css: -------------------------------------------------------------------------------- 1 | .formItem { 2 | color: #ffffff; 3 | } 4 | 5 | .formItemLabel { 6 | color: #ffffff; 7 | } 8 | -------------------------------------------------------------------------------- /src/components/Firmware/espflash.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useState } from "react"; 2 | import { Button, Card, Progress, message } from "antd"; 3 | import CryptoJS from "crypto-js"; 4 | import { ESPLoader, Transport } from "esptool-js"; 5 | import styles from '../HomepageFeatures/styles.module.css' 6 | 7 | const blob2BinaryString = ( blob: Blob ) => { 8 | return new Promise( ( resolve, reject ) => { 9 | const reader = new FileReader(); 10 | reader.onload = ( e ) => { 11 | resolve( e.target.result ); 12 | }; 13 | reader.onerror = ( e ) => { 14 | reject( e ); 15 | }; 16 | reader.readAsBinaryString( blob ); 17 | } ); 18 | } 19 | 20 | class ESPFlash { 21 | private _device: any; 22 | private _transport: Transport; 23 | private _chip: string; 24 | 25 | esploader: ESPLoader; 26 | 27 | constructor(public config: ESPFlashConfig, public onProgress) {} 28 | 29 | init = async () => { 30 | const terminal = { 31 | write: (data: any) => { 32 | console.log(data); 33 | }, 34 | writeLine: (data: any) => { 35 | console.log(data); 36 | }, 37 | clean: () => {}, 38 | }; 39 | 40 | const filters = [{ usbVendorId: this.config.vid }]; 41 | 42 | this._device = await (window.navigator as any).serial.requestPort({ 43 | filters, 44 | }); 45 | this._transport = new Transport(this._device); 46 | 47 | try { 48 | this.esploader = new ESPLoader(this._transport, 115200, terminal); 49 | 50 | this._chip = await this.esploader.main_fn(); 51 | } catch (error) { 52 | console.warn(error); 53 | } 54 | }; 55 | 56 | 57 | upload = async ( params?: any ) => { 58 | const parseAddr = ( addr?: string ) => { 59 | if ( addr === undefined ) { 60 | return 0x1000 61 | } 62 | if ( addr.startsWith( '0x' ) ) { 63 | return parseInt( addr, 16 ) 64 | } 65 | return parseInt( addr, 10 ) 66 | } 67 | 68 | await this.init() 69 | if ( this.config.fullerase ) { 70 | await this.erase() 71 | } 72 | const fileArray = [] 73 | if ( this.config.firmware ) { 74 | const addr = parseAddr( this.config.address ) 75 | const file = await fetch( `/${this.config.firmware}` ).then( res => res.blob() ) 76 | const txt = await blob2BinaryString( file ) 77 | fileArray.push( { 78 | address: addr, 79 | data: txt 80 | } ) 81 | } else if ( this.config.firmwares.length ) { 82 | for ( let i = 0; i < this.config.firmwares.length; i++ ) { 83 | const element = this.config.firmwares[i]; 84 | const addr = parseAddr( element.address ) 85 | const file = await fetch( `/${element.file}` ).then( res => res.blob() ) 86 | const txt = await blob2BinaryString( file ) 87 | fileArray.push( { 88 | address: addr, 89 | data: txt 90 | } ) 91 | } 92 | } 93 | try { 94 | await this.esploader.write_flash( fileArray, 'keep', undefined, undefined, false, true, 95 | ( filleIndex, wirtten, total ) => { 96 | const percentage = Math.trunc((filleIndex + wirtten)/total * 100) 97 | this.onProgress(percentage) 98 | }, 99 | ( image: any ) => { 100 | const result = CryptoJS.MD5( CryptoJS.enc.Latin1.parse( image ) ) 101 | return result.toString() 102 | } 103 | ) 104 | } catch ( error ) { 105 | throw error 106 | } finally { 107 | try { 108 | await this._transport.disconnect() 109 | } catch ( error ) { 110 | console.warn(error) 111 | } 112 | this.reset() 113 | } 114 | 115 | } 116 | 117 | erase = async () => { 118 | await this.esploader.erase_flash(); 119 | }; 120 | 121 | reset = async () => { 122 | await this._transport.connect(); 123 | await this._transport.setDTR(false); 124 | await new Promise((resolve) => setTimeout(resolve, 100)); 125 | await this._transport.setDTR(true); 126 | await new Promise((resolve) => setTimeout(resolve, 100)); 127 | await this._transport.disconnect(); 128 | }; 129 | 130 | cleanUp = () => { 131 | this._device = null; 132 | this._transport = null; 133 | this._chip = null; 134 | }; 135 | } 136 | 137 | interface ESPFlashConfig { 138 | vid: number; 139 | address?: string; 140 | fullerase?: boolean; 141 | firmware?: string; 142 | firmwares?: { address: string, file: string }[]; 143 | } 144 | 145 | export interface ESPFlashProps { 146 | board: string; 147 | version: string; 148 | config: ESPFlashConfig; 149 | } 150 | 151 | const ESPFlashCard = (props: ESPFlashProps) => { 152 | const [isDownloading, setIsDownloading] = useState(false); 153 | const [progress, setProgress] = useState(0); 154 | 155 | const handleDownload = async () => { 156 | const espflash = new ESPFlash(props.config, setProgress); 157 | try { 158 | setIsDownloading(true); 159 | await espflash.upload(); 160 | } catch (error) { 161 | const msg = error.message+''; 162 | if (msg.includes("Failed to execute 'requestPort'")) { 163 | message.error("No device found"); 164 | } else { 165 | message.error(msg); 166 | } 167 | } finally { 168 | setIsDownloading(false); 169 | } 170 | 171 | }; 172 | return ( 173 | 174 | 179 | {isDownloading ? ( 180 | 181 | ) : ( 182 | 185 | )} 186 | 187 | ); 188 | }; 189 | 190 | export default ESPFlashCard; 191 | -------------------------------------------------------------------------------- /src/components/Firmware/jdflash.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useState } from "react"; 2 | import { Button, Card, Progress, message } from "antd"; 3 | 4 | import { DisconnectState } from "../DevsDownload"; 5 | import { useJacdacStore } from "../../store/jacdacStore"; 6 | 7 | import { 8 | parseFirmwareFile, 9 | FirmwareBlob, 10 | FirmwareUpdater, 11 | PROGRESS, 12 | } from 'jacdac-ts' 13 | 14 | export interface JDFlashCardProps { 15 | name: string; 16 | url: string; 17 | version: string; 18 | } 19 | 20 | const JDFlashCard = (props: JDFlashCardProps) => { 21 | 22 | const [isDownloading, setIsDownloading] = useState(false); 23 | const [progress, setProgress] = useState(0); 24 | const {bus, webSerialConnected, webSocketConnected} = useJacdacStore() 25 | const isConnected = webSerialConnected || webSocketConnected 26 | const [firmwareBlob, setBlob] = useState(null); 27 | 28 | useEffect(() => { 29 | const fetchData = async () => { 30 | const response = await fetch(props.url); 31 | const blob = await response.blob(); 32 | const firmware = await parseFirmwareFile(blob); 33 | if (firmware.length){ 34 | setBlob(firmware[0]); 35 | } 36 | } 37 | fetchData(); 38 | }, [props.url]); 39 | 40 | const handleUpgrade = async () => { 41 | const { productIdentifier } = firmwareBlob; 42 | // find the device in the bus 43 | // TODO: async product id read?? 44 | let device = bus.devices().find(d => d.productIdentifier === productIdentifier); 45 | for (const dev of bus.devices()){ 46 | if (dev.productIdentifier === productIdentifier){ 47 | device = dev; 48 | } 49 | } 50 | if (!device) { 51 | message.error("Device not found"); 52 | return; 53 | } 54 | setIsDownloading(true); 55 | const firmwareInfo = device.firmwareInfo; 56 | let firmwareUpdater = device.firmwareUpdater; 57 | if (!firmwareUpdater) { 58 | firmwareUpdater = new FirmwareUpdater(bus, firmwareBlob); 59 | device.firmwareUpdater = firmwareUpdater; 60 | firmwareUpdater.subscribe(PROGRESS, (v: number) => { 61 | setProgress(v*100) 62 | }); 63 | } 64 | try { 65 | const updateCandidates = [firmwareInfo] 66 | await firmwareUpdater.flash(updateCandidates, false) 67 | } catch (error) { 68 | message.error(error.message); 69 | } finally { 70 | setIsDownloading(false); 71 | } 72 | 73 | } 74 | 75 | if (!isConnected) return( 76 | 77 | ); 78 | 79 | return ( 80 |
81 | {firmwareBlob ? 82 | 86 | {isDownloading ? ( 87 | 88 | ) : ( 89 | 92 | )} 93 | :
94 | Reading firmware... 95 |
} 96 |
97 | ); 98 | } 99 | 100 | export default JDFlashCard; -------------------------------------------------------------------------------- /src/components/Hardware/Elite60.css: -------------------------------------------------------------------------------- 1 | .keyboard { 2 | display: grid; 3 | gap: 5px; 4 | background-color: #f0f0f0; 5 | padding: 20px; 6 | border-radius: 10px; 7 | } 8 | 9 | .keyboard-row { 10 | display: flex; 11 | gap: 5px; 12 | } 13 | 14 | .key { 15 | display: flex; 16 | justify-content: center; 17 | align-items: center; 18 | background-color: #e0e0e0; 19 | border: 1px solid #ccc; 20 | border-radius: 5px; 21 | height: 40px; 22 | flex: 1; 23 | } 24 | 25 | .key.active { 26 | background-color: #a0d1ff; 27 | } 28 | 29 | .key.shortcut { 30 | /* bold */ 31 | font-weight: 700; 32 | color: #333; 33 | border-color: #333; 34 | border-width: 2px; 35 | cursor: pointer; 36 | } 37 | 38 | .key.drag-over { 39 | background-color: #ffcccb; 40 | } 41 | 42 | .key.esc { 43 | flex: 1; 44 | } 45 | 46 | .key.backspace { 47 | flex: 2; 48 | } 49 | 50 | .key.tab { 51 | flex: 1.5; 52 | } 53 | 54 | .key.capslock { 55 | flex: 1.75; 56 | } 57 | 58 | .key.enter { 59 | flex: 2.25; 60 | } 61 | 62 | .key.shift { 63 | flex: 2.25; 64 | } 65 | 66 | .key.ctrl { 67 | flex: 1.5; 68 | } 69 | 70 | .key.space { 71 | flex: 7.5; 72 | } -------------------------------------------------------------------------------- /src/components/Hardware/Elite60.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useState } from 'react'; 2 | import { Build, SkillEvtParam } from '@/lib/SkillBuild'; 3 | import './Elite60.css'; 4 | 5 | 6 | export interface KeyboardProps { 7 | shortcut?: Record; 8 | keys?: string[][]; 9 | build: Build 10 | onDrop: (id: string, key: string | SkillEvtParam, accept: string) => void; 11 | onClick: (key: string) => void; 12 | } 13 | 14 | const defaultLayout = [ 15 | ['ESC', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', '', '⭕'], 16 | ['Tab', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '[', ']', '\\'], 17 | ['CapsLock', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ';', '\'', 'Enter'], 18 | ['Shift', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', ',', '.', '/', '', '↑', 'Del'], 19 | ['Ctrl', '⌘', 'Alt', 'Space', 'Alt', '', '←', '↓', '→'] 20 | ]; 21 | 22 | const specialKeyCodeMap = { 23 | 'ArrowUp': '↑', 24 | 'ArrowLeft': '←', 25 | 'ArrowDown': '↓', 26 | 'ArrowRight': '→', 27 | 'Escape': 'ESC', 28 | 'Space': 'Space', 29 | 'ShiftLeft': 'Shift', 30 | 'ControlLeft': 'Ctrl', 31 | 'MetaLeft': '⌘', 32 | }; 33 | 34 | const Elite60 = (props: KeyboardProps) => { 35 | const [layout, setLayout] = useState(props.keys || defaultLayout); 36 | const [activeKey, setActiveKey] = useState(null); 37 | const [dragOverKey, setDragOverKey] = useState(null); 38 | 39 | const keyMap = useMemo(() => { 40 | return layout.flat().reduce((acc, key) => { 41 | acc[key.toLowerCase()] = key; 42 | return acc; 43 | }, {}); 44 | }, [layout]); 45 | 46 | useEffect(() => { 47 | const handleKeyDown = (event) => { 48 | console.log(event) 49 | const key = keyMap[event.key.toLowerCase()]; 50 | if (key){ 51 | setActiveKey(key); 52 | } else if (specialKeyCodeMap[event.code]) { 53 | setActiveKey(specialKeyCodeMap[event.code]); 54 | } 55 | }; 56 | 57 | const handleKeyUp = (event) => { 58 | const key = keyMap[event.key.toLowerCase()]; 59 | if (key) { 60 | setActiveKey(null); 61 | } else if (specialKeyCodeMap[event.code]) { 62 | setActiveKey(null); 63 | } 64 | }; 65 | 66 | window.addEventListener('keydown', handleKeyDown); 67 | window.addEventListener('keyup', handleKeyUp); 68 | 69 | return () => { 70 | window.removeEventListener('keydown', handleKeyDown); 71 | window.removeEventListener('keyup', handleKeyUp); 72 | }; 73 | }, []); 74 | 75 | const handleDragOver = (event, key) => { 76 | // TODO: only allow drop on certain keys 77 | event.preventDefault(); 78 | setDragOverKey(key); 79 | } 80 | 81 | const handleDragLeave = () => { 82 | setDragOverKey(null); 83 | } 84 | 85 | const handleDropKeybutton = (event, key) => { 86 | event.preventDefault(); 87 | setDragOverKey(null); 88 | props.onDrop(event.dataTransfer.getData('id'), key, 'keybutton'); 89 | } 90 | 91 | return ( 92 |
93 | {layout.map((row, rowIndex) => ( 94 |
95 | {row.map((key, keyIndex) => { 96 | let className =`key ${key.toLowerCase()}` 97 | if (activeKey === key) { 98 | className += ' active'; 99 | } 100 | if (dragOverKey === key) { 101 | className += ' drag-over'; 102 | } 103 | 104 | return ( 105 |
handleDragOver(event, key)} 109 | onDragLeave={handleDragLeave} 110 | onDrop={() => handleDropKeybutton(event, key)} 111 | onClick={() => console.log('Clicked on', key)} 112 | > 113 | {key} 114 |
) 115 | })} 116 |
117 | ))} 118 |
119 | ); 120 | }; 121 | 122 | export default Elite60; -------------------------------------------------------------------------------- /src/components/Hardware/NumPad.css: -------------------------------------------------------------------------------- 1 | .numpad { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 10px; 5 | background-color: #333; 6 | padding: 20px; 7 | border-radius: 10px; 8 | max-width: 210px; 9 | margin: auto; 10 | } 11 | 12 | .numpad-header { 13 | display: flex; 14 | justify-content: space-between; 15 | gap: 10px; 16 | } 17 | 18 | .oled { 19 | flex: 2; 20 | background-color: #051650; 21 | color: #eee; 22 | display: flex; 23 | justify-content: center; 24 | align-items: center; 25 | height: 40px; 26 | border: 1px solid #ccc; 27 | border-radius: 5px; 28 | } 29 | 30 | .oled.active { 31 | background-color: #0a2c9e; 32 | } 33 | 34 | .oled.filled { 35 | color: aqua; 36 | cursor: pointer; 37 | } 38 | 39 | .encoder { 40 | flex: 1; 41 | background-color: #444; 42 | color: #eee; 43 | display: flex; 44 | justify-content: center; 45 | align-items: center; 46 | height: 40px; 47 | border: 1px solid #ccc; 48 | border-radius: 5px; 49 | } 50 | 51 | .numpad-body { 52 | display: flex; 53 | } 54 | 55 | .main-keys { 56 | display: flex; 57 | flex-direction: column; 58 | gap: 5px; 59 | } 60 | 61 | .numpad-row { 62 | display: flex; 63 | gap: 5px; 64 | } 65 | 66 | .misc { 67 | display: flex; 68 | justify-content: center; 69 | flex-direction:row; 70 | padding: 5px; 71 | gap: 8px; 72 | } 73 | 74 | .misc .skill { 75 | display: flex; 76 | justify-content: center; 77 | align-items: center; 78 | background-color: #e0e0e0; 79 | border: 1px solid #ccc; 80 | border-radius: 5px; 81 | height: 40px; 82 | width: 80px; 83 | transition: background-color 0.3s; 84 | } 85 | 86 | .skill.filled { 87 | background-color: #a0d1ff; 88 | cursor: pointer; 89 | } 90 | 91 | .keynum { 92 | display: flex; 93 | justify-content: center; 94 | align-items: center; 95 | background-color: #e0e0e0; 96 | border: 1px solid #ccc; 97 | border-radius: 5px; 98 | height: 40px; 99 | width: 40px; 100 | transition: background-color 0.3s; 101 | } 102 | 103 | .keynum.active { 104 | background-color: #a0d1ff; 105 | } 106 | 107 | .keynum.shortcut { 108 | /* bold */ 109 | font-weight: 700; 110 | color: #333; 111 | border-color: #333; 112 | border-width: 2px; 113 | cursor: pointer; 114 | } 115 | 116 | .keynum.drag-over { 117 | background-color: #ffcccb; 118 | } 119 | 120 | .keynum.filled { 121 | background-color: coral; 122 | cursor: pointer; 123 | } 124 | 125 | .keynum img { 126 | max-width: 32px; 127 | } 128 | 129 | .keynum-tall { 130 | height: 85px; 131 | /* 40px * 2 + 5px (gap) = 85px */ 132 | } 133 | 134 | .keynum-wide { 135 | width: 85px; 136 | } 137 | 138 | .side-keys { 139 | display: flex; 140 | flex-direction: column; 141 | gap: 5px; 142 | margin-left: 5px; 143 | } -------------------------------------------------------------------------------- /src/components/HomepageFeatures/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import Heading from '@theme/Heading'; 3 | import styles from './styles.module.css'; 4 | 5 | type FeatureItem = { 6 | title: string; 7 | Svg: React.ComponentType>; 8 | description: JSX.Element; 9 | }; 10 | 11 | const FeatureList: FeatureItem[] = [ 12 | { 13 | title: 'Easy to Use', 14 | Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default, 15 | description: ( 16 | <> 17 | Docusaurus was designed from the ground up to be easily installed and 18 | used to get your website up and running quickly. 19 | 20 | ), 21 | }, 22 | { 23 | title: 'Focus on What Matters', 24 | Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default, 25 | description: ( 26 | <> 27 | Docusaurus lets you focus on your docs, and we'll do the chores. Go 28 | ahead and move your docs into the docs directory. 29 | 30 | ), 31 | }, 32 | { 33 | title: 'Powered by React', 34 | Svg: require('@site/static/img/undraw_docusaurus_react.svg').default, 35 | description: ( 36 | <> 37 | Extend or customize your website layout by reusing React. Docusaurus can 38 | be extended while reusing the same header and footer. 39 | 40 | ), 41 | }, 42 | ]; 43 | 44 | function Feature({title, Svg, description}: FeatureItem) { 45 | return ( 46 |
47 |
48 | 49 |
50 |
51 | {title} 52 |

{description}

53 |
54 |
55 | ); 56 | } 57 | 58 | export default function HomepageFeatures(): JSX.Element { 59 | return ( 60 |
61 |
62 |
63 | {FeatureList.map((props, idx) => ( 64 | 65 | ))} 66 |
67 |
68 |
69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /src/components/HomepageFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 200px; 10 | width: 200px; 11 | } 12 | .cardmeta :global(.ant-card-meta-detail) div{ 13 | color: var(--ifm-color-content); 14 | } 15 | .card :global(.ant-card-body) { 16 | display: flex; 17 | } 18 | .card :global(.ant-card-meta){ 19 | flex: 1; 20 | } 21 | .card :global(.ant-btn) { 22 | flex: 1; 23 | } 24 | -------------------------------------------------------------------------------- /src/components/Hostapp/llms.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useMemo } from 'react'; 2 | import { Form, Input, InputNumber, List, FloatButton, Modal, Card, Row } from 'antd'; 3 | import { 4 | AppstoreAddOutlined 5 | } from '@ant-design/icons'; 6 | 7 | export interface LLMMsg { 8 | role: string; 9 | content: string; 10 | } 11 | 12 | export interface LLMConfig { 13 | id: string; // file name 14 | title: string; 15 | description?: string; 16 | system?: string; // system prompt 17 | context: LLMMsg[]; 18 | historyLength?: number; 19 | } 20 | 21 | 22 | interface LLMConfigProps { 23 | llm: LLMConfig, 24 | isModalVisible: boolean; 25 | handleSave: (values: any) => void; 26 | } 27 | 28 | export const LLMConfigModal = (props: LLMConfigProps) => { 29 | const [form] = Form.useForm(); 30 | 31 | useEffect(() => { 32 | if (props.llm) { 33 | form.setFieldsValue(props.llm); 34 | } 35 | }, [props.llm]); 36 | 37 | const handleOk = () => { 38 | form 39 | .validateFields() 40 | .then((values) => { 41 | form.resetFields(); 42 | console.log(values) 43 | }) 44 | .catch((info) => { 45 | console.log('Validate Failed:', info); 46 | }); 47 | props.handleSave(form.getFieldsValue()); 48 | }; 49 | 50 | const handleCancel = () => { 51 | form.resetFields(); 52 | props.handleSave(null); 53 | } 54 | 55 | return ( 56 | 62 |
68 | 73 | 74 | 75 | 80 | 81 | 82 | 87 | 88 | 89 | 94 | 95 | 96 | 100 | 101 | 102 |
103 |
104 | ) 105 | 106 | } 107 | 108 | const LLMCard = ({config, onSelect}: { 109 | config: LLMConfig, 110 | onSelect: (cfg: LLMConfig) => void 111 | }) => { 112 | return ( 113 | onSelect(config)}>Edit} 118 | > 119 | 120 |

{config.system.length > 24 ? config.system.slice(0, 24) + '...' : config.system}

121 | 122 | 123 |
124 | 125 |
126 | ) 127 | } 128 | 129 | 130 | const LLMS: React.FC = () => { 131 | const [llms, setLlms] = useState([]); 132 | const [currentLLM, setCurrentLLM] = useState(null); 133 | 134 | const refreshLLM = useMemo(() => { 135 | const { list_llm } = window.electronAPI; 136 | return () => { 137 | list_llm().then((ret) => { 138 | console.log("llms", ret); 139 | const llms = ret.llms as LLMConfig[] 140 | const history = ret.history as Record 141 | setLlms(llms); 142 | }); 143 | } 144 | }, []); 145 | useEffect(() => { 146 | refreshLLM(); 147 | }, []); 148 | 149 | const handleAdd = () => { 150 | setCurrentLLM({ 151 | id: 'new_robot', 152 | title: '', 153 | context: [] 154 | }) 155 | } 156 | 157 | const handleSaveLLM = (values: any) => { 158 | if (values) { 159 | const { save_llm } = window.electronAPI; 160 | save_llm({id: values.id, llm: values}).then(() => { 161 | refreshLLM(); 162 | }); 163 | } 164 | setCurrentLLM(null); 165 | } 166 | 167 | const handleCardClick = (config: LLMConfig) => { 168 | setCurrentLLM(config) 169 | } 170 | 171 | return ( 172 |
173 |

LLMS

174 | ( 178 | 179 | 180 | 181 | )} 182 | /> 183 | 188 | } 191 | type='primary' 192 | description="ADD" 193 | shape="circle" 194 | style={{ right: 24 }} 195 | /> 196 |
197 | 198 | ) 199 | } 200 | 201 | export default LLMS; -------------------------------------------------------------------------------- /src/components/Hostapp/settings.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Form, Input, Button, message } from 'antd'; 3 | 4 | export interface SettingsProps { 5 | mqttbroker: string; 6 | mqtttopic: string; 7 | openaiUrl: string; 8 | openaiKey: string; 9 | openaiModel: string; 10 | } 11 | 12 | const Settings: React.FC = () => { 13 | const [form] = Form.useForm(); 14 | const [loading, setLoading] = useState(false); 15 | const { get_settings, save_settings } = window.electronAPI; 16 | 17 | useEffect(() => { 18 | get_settings().then((settings: any) => { 19 | form.setFieldsValue(settings); 20 | }); 21 | }, [form]); 22 | 23 | const onFinish = (values: any) => { 24 | setLoading(true); 25 | save_settings(values).then(() => { 26 | setLoading(false); 27 | message.success('Settings saved successfully'); 28 | }).catch((error) => { 29 | setLoading(false); 30 | message.error('Failed to save settings'); 31 | }); 32 | }; 33 | 34 | return ( 35 |
36 |

Settings

37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 57 | 58 |
59 |
60 | ); 61 | }; 62 | 63 | export default Settings; -------------------------------------------------------------------------------- /src/components/Hostapp/status.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useMemo } from 'react'; 2 | import { FloatButton, Row, Col, Card, Badge, Avatar, Divider } from 'antd'; 3 | import { 4 | RedoOutlined 5 | } from '@ant-design/icons'; 6 | 7 | import { 8 | deviceCatalogImage, 9 | deviceCatalog 10 | 11 | } from 'jacdac-ts' 12 | 13 | export interface HostAppStatusProps { 14 | transports: string[]; 15 | devices: { 16 | shortId: string; 17 | productIdentifier: number; 18 | }[] 19 | } 20 | 21 | const HostAppStatus = () => { 22 | const [specs, setSpecs] = useState([]) 23 | const [status, setStatus] = useState({ 24 | devices: [], 25 | transports: [] 26 | }) 27 | 28 | const handleRefresh = useMemo(() => () => { 29 | const { get_status } = window.electronAPI 30 | get_status().then((status: any) => { 31 | console.log("status", status) 32 | setStatus(status) 33 | }) 34 | }, []) 35 | 36 | useEffect(()=>{ 37 | handleRefresh() 38 | },[]) 39 | 40 | useEffect(() => { 41 | const _spec = [] 42 | for (const dev of status.devices) { 43 | if (!dev.productIdentifier) 44 | continue 45 | const spec = deviceCatalog.specificationFromProductIdentifier(dev.productIdentifier) 46 | if (!spec) 47 | continue 48 | _spec.push({ 49 | id: dev.shortId, 50 | name: spec.name, 51 | productIdentifier: dev.productIdentifier, 52 | img: deviceCatalogImage(spec, "list") 53 | }) 54 | } 55 | setSpecs(_spec) 56 | }, [status]) 57 | 58 | 59 | 60 | return (
61 |

Status

62 | {status.transports.map((transport, idx) => ( 63 | 64 | ))} 65 | 66 | 67 | {specs.map((_spec, idx) => ( 68 | 69 | 71 | 72 | {_spec.id} 73 |
} 74 | bordered={false} 75 | > 76 | 79 | 80 | 81 | ))} 82 | 83 | 84 | 85 | } 87 | shape="circle" 88 | style={{ right: 24 }} 89 | onClick={handleRefresh} 90 | /> 91 | ) 92 | } 93 | 94 | export default HostAppStatus; -------------------------------------------------------------------------------- /src/components/Keymap/keyboard.css: -------------------------------------------------------------------------------- 1 | 2 | .numPad { 3 | display: flex; 4 | align-items: flex-end; 5 | } 6 | 7 | .simple-keyboard-numpad.simple-keyboard { 8 | width: 160px; 9 | border-radius: 0; 10 | } 11 | 12 | .simple-keyboard-numpad.simple-keyboard .hg-button { 13 | width: 50px; 14 | justify-content: center; 15 | display: flex; 16 | align-items: center; 17 | } 18 | 19 | .simple-keyboard-numpadEnd.simple-keyboard { 20 | width: 50px; 21 | margin: 0; 22 | padding: 5px 5px 5px 0; 23 | border-radius: 0; 24 | } 25 | 26 | .simple-keyboard-numpadEnd.simple-keyboard .hg-button { 27 | align-items: center; 28 | justify-content: center; 29 | display: flex; 30 | } 31 | 32 | .simple-keyboard-numpadEnd .hg-button.hg-standardBtn.hg-button-plus { 33 | height: 85px; 34 | } 35 | 36 | .simple-keyboard-numpadEnd.simple-keyboard .hg-button.hg-button-enter { 37 | height: 85px; 38 | } 39 | -------------------------------------------------------------------------------- /src/components/Keymap/numpad.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useRef, useState } from "react"; 2 | import React from "react"; 3 | import Keyboard from "react-simple-keyboard"; 4 | 5 | import './keyboard.css'; 6 | import { KeyConfig } from "./elite60" 7 | 8 | const numberPad: KeyConfig[][] = [ 9 | [ 10 | { name: "{numlock}", hid: 0x59 }, 11 | { name: "{numpaddivide}", hid: 0x54 }, 12 | { name: "{numpadmultiply}", hid: 0x55 }, 13 | ], 14 | [ 15 | { name: "{numpad7}", hid: 0x5F }, 16 | { name: "{numpad8}", hid: 0x60 }, 17 | { name: "{numpad9}", hid: 0x61 }, 18 | ], 19 | [ 20 | { name: "{numpad4}", hid: 0x5C }, 21 | { name: "{numpad5}", hid: 0x5D }, 22 | { name: "{numpad6}", hid: 0x5E }, 23 | ], 24 | [ 25 | { name: "{numpad1}", hid: 0x59 }, 26 | { name: "{numpad2}", hid: 0x5A }, 27 | { name: "{numpad3}", hid: 0x5B }, 28 | ], 29 | [ 30 | { name: "{numpad0}", hid: 0x62 }, 31 | { name: "{numpaddecimal}", hid: 0x63 }, 32 | ] 33 | ] 34 | 35 | const numberPadEnd: KeyConfig[][] = [ 36 | [ 37 | { name: "{numpadsubtract}", hid: 0x56 }, 38 | ],[ 39 | { name: "{numpadadd}", hid: 0x57 }, 40 | ],[ 41 | { name: "{numpadenter}", hid: 0x58 }, 42 | ] 43 | 44 | ] 45 | 46 | export default function NumberPad() { 47 | const [keyboard, setKeyboard] = useState(null); 48 | const [layout, setLayout] = useState([]) 49 | const [layoutEnd, setLayoutEnd] = useState([]) 50 | 51 | useEffect(() => { 52 | const _layout = [] 53 | const _layoutEnd = [] 54 | for (const row of numberPad) { 55 | let rowAry = "" 56 | for (const key of row) { 57 | rowAry += key.name + " " 58 | } 59 | _layout.push(rowAry.trim()) 60 | } 61 | for (const row of numberPadEnd) { 62 | let rowAry = "" 63 | for (const key of row) { 64 | rowAry += key.name + " " 65 | } 66 | _layoutEnd.push(rowAry.trim()) 67 | } 68 | setLayout(_layout) 69 | setLayoutEnd(_layoutEnd) 70 | }, []) 71 | 72 | 73 | return ( 74 |
79 | { 82 | setKeyboard(r) 83 | }} 84 | // debug={true} 85 | layout={{ 86 | default: layout 87 | }} 88 | mergeDisplay={true} 89 | syncInstanceInputs={true} 90 | physicalKeyboardHighlight={true} 91 | /> 92 | 98 |
99 | ) 100 | } -------------------------------------------------------------------------------- /src/components/codeEditor/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import Editor from 'react-simple-code-editor'; 3 | import { highlight, languages } from 'prismjs/components/prism-core'; 4 | import 'prismjs/components/prism-clike'; 5 | import 'prismjs/components/prism-javascript'; 6 | import 'prismjs/components/prism-typescript'; 7 | import styles from './styles.module.css'; 8 | 9 | 10 | const CodeEditor = ({code, onChange}) => { 11 | return ( 12 |
13 | { 18 | onChange(code) 19 | }} 20 | highlight={code => highlight(code, languages.typescript)} 21 | padding={10} 22 | style={{ 23 | fontFamily: '"Fira code", "Fira Mono", monospace', 24 | fontSize: 14, 25 | }} 26 | /> 27 | 28 |
29 | ); 30 | }; 31 | 32 | export default CodeEditor; 33 | -------------------------------------------------------------------------------- /src/components/codeEditor/styles.module.css: -------------------------------------------------------------------------------- 1 | .editor{ 2 | border-radius: 4px; 3 | min-height: 200px; 4 | } 5 | :global(.operator){ 6 | background: none !important; 7 | } 8 | :global(.editorContainer){ 9 | overflow: auto; 10 | border: 1px solid var(--ifm-color-emphasis-300); 11 | border-radius: 8px; 12 | margin: 10px; 13 | } 14 | .editor :global(.operator){ 15 | background: none !important; 16 | } 17 | 18 | .editor 19 | :global(.token.comment), 20 | :global(.token.prolog), 21 | :global(.token.doctype), 22 | :global(.token.cdata) { 23 | color: slategray; 24 | } 25 | 26 | .editor 27 | :global(.token.punctuation) { 28 | color: #999; 29 | } 30 | 31 | .editor 32 | :global(.token.namespace) { 33 | opacity: .7; 34 | } 35 | 36 | .editor 37 | :global(.token.property), 38 | :global(.token.tag), 39 | :global(.token.boolean), 40 | :global(.token.number), 41 | :global(.token.constant), 42 | :global(.token.symbol), 43 | :global(.token.deleted) { 44 | color: #905; 45 | } 46 | 47 | .editor 48 | :global(.token.selector), 49 | :global(.token.attr-name), 50 | :global(.token.string), 51 | :global(.token.char), 52 | :global(.token.builtin), 53 | :global(.token.inserted) { 54 | color: #690; 55 | } 56 | 57 | .editor 58 | :global(.token.operator), 59 | :global(.token.entity), 60 | :global(.token.url), 61 | :global(.language-css .token.string), 62 | :global(.style .token.string) { 63 | color: #9a6e3a; 64 | background: hsla(0, 0%, 100%, .5); 65 | } 66 | 67 | .editor 68 | :global(.token.atrule), 69 | :global(.token.attr-value), 70 | :global(.token.keyword) { 71 | color: #07a; 72 | } 73 | 74 | .editor 75 | :global(.token.function), 76 | :global(.token.class-name) { 77 | color: #DD4A68; 78 | } 79 | 80 | .editor 81 | :global(.token.regex), 82 | :global(.token.important), 83 | :global(.token.variable) { 84 | color: #e90; 85 | } 86 | 87 | .editor 88 | :global(.token.important), 89 | :global(.token.bold) { 90 | font-weight: bold; 91 | } 92 | .editor :global(.token.italic) { 93 | font-style: italic; 94 | } 95 | 96 | .editor :global(.token.entity){ 97 | cursor: help; 98 | } 99 | 100 | -------------------------------------------------------------------------------- /src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --ifm-color-primary: #2e8555; 10 | --ifm-color-primary-dark: #29784c; 11 | --ifm-color-primary-darker: #277148; 12 | --ifm-color-primary-darkest: #205d3b; 13 | --ifm-color-primary-light: #33925d; 14 | --ifm-color-primary-lighter: #359962; 15 | --ifm-color-primary-lightest: #3cad6e; 16 | --ifm-code-font-size: 95%; 17 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); 18 | } 19 | 20 | /* For readability concerns, you should choose a lighter palette in dark mode. */ 21 | [data-theme='dark'] { 22 | --ifm-color-primary: #25c2a0; 23 | --ifm-color-primary-dark: #21af90; 24 | --ifm-color-primary-darker: #1fa588; 25 | --ifm-color-primary-darkest: #1a8870; 26 | --ifm-color-primary-light: #29d5b0; 27 | --ifm-color-primary-lighter: #32d8b4; 28 | --ifm-color-primary-lightest: #4fddbf; 29 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); 30 | } 31 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | 2 | interface ElectronAPI { 3 | start_service: (name: string) => Promise, 4 | stop_service: (name: string) => Promise, 5 | get_services: () => Promise, 6 | get_status: () => Promise, 7 | save_settings: (settings: any) => Promise, 8 | get_settings: () => Promise, 9 | onUserText: (callback: (text: string) => void) => void, 10 | list_llm: () => Promise, 11 | get_llm: (name: string) => Promise, 12 | save_llm: (props: any) => Promise, 13 | save_history: (props: any) => Promise, 14 | onLoadLLM: (callback: (history: any) => void) => void, 15 | } 16 | 17 | interface Window { 18 | electronAPI: ElectronAPI; 19 | } 20 | 21 | -------------------------------------------------------------------------------- /src/lib/DevsHost.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Host, 3 | resolveBuildConfig, 4 | } from "@kittenbot/devs_compiler"; 5 | 6 | import extrea_services from './services.json' 7 | 8 | 9 | const keyboarUtils = ` 10 | import * as ds from "@devicescript/core"; 11 | import { throttleTime } from "@devicescript/observables"; 12 | import { SSD1306Driver } from "@devicescript/drivers"; 13 | 14 | const kb = new ds.KeyboardClient() 15 | const settings = new ds.Settings() 16 | 17 | const _keyCallbacks: { [key: number]: () => Promise } = {}; 18 | const regKey = function(keyCode: number, callback: () => Promise): void { 19 | _keyCallbacks[keyCode] = callback; 20 | } 21 | 22 | const hidEnable = async (en: boolean) => { 23 | const b = Buffer.alloc(1) 24 | b[0] = en ? 1 : 0 25 | await settings.set("hidscan", b) 26 | } 27 | 28 | kb.down.pipe(throttleTime(500)).subscribe(async (key) => { 29 | if (_keyCallbacks[key]) { 30 | await _keyCallbacks[key](); 31 | } else { 32 | console.log(\`Unhandled\`, key); 33 | } 34 | }); 35 | 36 | async function startOLED(){ 37 | const oled = new SSD1306Driver({ 38 | width: 128, 39 | height: 64, 40 | devAddr: 0x3c as any, 41 | }) 42 | await oled.init() 43 | return oled 44 | } 45 | 46 | export { 47 | kb, 48 | regKey, 49 | hidEnable, 50 | startOLED, 51 | } 52 | 53 | ` 54 | 55 | // Device script implementation for kitten extension 56 | export class DevsHost implements Host { 57 | private files: {}; 58 | private cfg: any; 59 | bytecode: Uint8Array; 60 | 61 | constructor(opt: any = {}) { 62 | this.files = opt.files || {}; 63 | this.cfg = resolveBuildConfig(); 64 | this.cfg.hwInfo = opt.hwInfo || { 65 | progName: "hello", 66 | progVersion: "0.0.1", 67 | }; 68 | this.cfg.addServices = extrea_services 69 | this.cfg.services = [...this.cfg.services, ...extrea_services] 70 | this.files["src/keyboarUtils.ts"] = keyboarUtils 71 | } 72 | 73 | write(filename: string, contents: string | Uint8Array): void { 74 | // console.log("write", filename) 75 | this.files[filename] = contents; 76 | } 77 | read(filename: string): string { 78 | // console.log("read", filename) 79 | if (this.files.hasOwnProperty(filename)) { 80 | return this.files[filename] as string; 81 | } 82 | throw new Error("No such file: " + filename); 83 | } 84 | resolvePath(p: string) { 85 | // console.log("resolve", p) 86 | return p; 87 | } 88 | 89 | log(msg: string): void { 90 | console.log(msg); 91 | } 92 | 93 | verifyBytecode(buf: Uint8Array): void { 94 | // TODO: vm to verify bytecode?? 95 | this.bytecode = buf; 96 | return; 97 | } 98 | 99 | getConfig() { 100 | return this.cfg; 101 | } 102 | } -------------------------------------------------------------------------------- /src/lib/SkillBuild.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface SkillParam { 3 | type: 'string' | 'number' | 'boolean'| 'list' 4 | description: string 5 | default?: any 6 | constant?: boolean // user can't edit this parameter 7 | values?: string | string[] 8 | } 9 | 10 | export interface SkillConfig { 11 | id: string 12 | category: string // the category this skill may renderin 13 | index?: number // index render order in the category 14 | target?: string // the target may accept this skill 15 | name: string 16 | description: string 17 | thumbnail?: string 18 | params?: Record 19 | devs?: string // the device script to run 20 | jsSrc?: string // the js source code to skill, filled by the build plugin 21 | categoryThumbnail?: string // the thumbnail of the category, maybe should be in the category config 22 | libs?: Record // the libraries to import in the device script 23 | } 24 | 25 | export interface SkillCategory { 26 | name: string 27 | thumbnail?: string 28 | skills: Record 29 | } 30 | 31 | export interface SkillEvtParam { 32 | text: string 33 | value: string 34 | } 35 | 36 | export interface SkillEvent { 37 | id: string // the id of the skill 38 | params?: Record // the parameters of the skill, calculated by the builder?? 39 | key: string // the unique key to the component 40 | thumbnail?: string // set by skill loader 41 | } 42 | 43 | export interface Build { 44 | id: string 45 | name: string 46 | hardware: string 47 | modules?: string[] 48 | events: SkillEvent[] 49 | } 50 | 51 | export function getSkillById (id: string, skills: Record) { 52 | for (const category in skills) { 53 | if (skills[category].skills[id]) { 54 | return skills[category].skills[id] 55 | } 56 | } 57 | return null 58 | } 59 | 60 | export interface DeviceScriptGenResult { 61 | code: string 62 | libs: Record 63 | } 64 | 65 | export function generateDeviceScript(build: Build, skills: Record): DeviceScriptGenResult { 66 | let code = '' 67 | const libs = {} 68 | const _imports = [] 69 | const _instances = {} 70 | 71 | console.log("build: ", build, skills) 72 | 73 | for (const evt of build.events) { 74 | const skill = getSkillById(evt.id, skills) 75 | if (!skill) { 76 | console.error('Skill not found', evt) 77 | continue 78 | } 79 | let _code = skill.devs 80 | // params replace 81 | for (const key in evt.params) { 82 | let value = evt.params[key] 83 | if (typeof value === 'object') { 84 | value = value.value 85 | } 86 | _code = _code.replace(new RegExp(`#${key}#`, 'g'), value) 87 | } 88 | 89 | const _lines = _code.split('\n') 90 | for (const line of _lines) { 91 | if (line.startsWith('import')) { 92 | if (!_imports.includes(line)) _imports.push(line) 93 | } else if (line.startsWith('const')) { 94 | const _id = line.split(' ')[1] 95 | _instances[_id] = line 96 | } else if(line) { 97 | code += line + '\n' 98 | } 99 | } 100 | if (skill.libs) { 101 | for (const lib in skill.libs) { 102 | libs[lib] = skill.libs[lib] 103 | } 104 | } 105 | } 106 | 107 | // instances process 108 | const _instancesDs = [] 109 | const _instancesCode = [] 110 | for (const id in _instances) { 111 | const line = _instances[id] 112 | if (line.includes('new')) { 113 | _instancesDs.push(line) 114 | } else { 115 | _instancesCode.push(line) 116 | } 117 | } 118 | // join code in inverse order 119 | code = _instancesCode.join('\n') + '\n' + code 120 | code = _instancesDs.join('\n') + '\n' + code 121 | 122 | // imports process 123 | code = _imports.join('\n') + '\n' + code 124 | 125 | return { 126 | code, 127 | libs 128 | } 129 | } 130 | 131 | 132 | const SkillBuild = async (text: string) => { 133 | const config = {} 134 | const awagent = (content) => { 135 | const _keys = ['id', 'name', 'description'] 136 | for (const key of _keys) { 137 | if (content[key]) { 138 | config[key] = content[key] 139 | } 140 | } 141 | 142 | } 143 | 144 | const md = (content) => { 145 | console.log("md: ", content) 146 | } 147 | 148 | const evaluateInContext = (js, context) => { 149 | return function() { return eval(js); }.call(context); 150 | } 151 | 152 | try { 153 | evaluateInContext(text, { awagent, md }) 154 | } catch (e) { 155 | console.warn(e) 156 | } 157 | return config 158 | } 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | export default SkillBuild; 167 | -------------------------------------------------------------------------------- /src/lib/codeparse.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const parseColoredText = (text) => { 4 | const regex = /\x1b\[(\d+)m/g; 5 | const parts = []; 6 | let lastIndex = 0; 7 | let match; 8 | 9 | const colorMap = { 10 | 90: '#6D6D6D', // bright black 11 | 91: '#FF5555', // bright red 12 | 93: '#F1FA8C', // bright yellow 13 | 96: '#8BE9FD', // bright cyan 14 | }; 15 | 16 | while ((match = regex.exec(text)) !== null) { 17 | const colorCode = match[1]; 18 | const color = colorMap[colorCode] || '#000'; 19 | 20 | if (match.index > lastIndex) { 21 | parts.push(text.slice(lastIndex, match.index)); 22 | } 23 | 24 | lastIndex = regex.lastIndex; 25 | const nextMatch = regex.exec(text); 26 | const endIndex = nextMatch ? nextMatch.index : text.length; 27 | 28 | parts.push( 29 | 30 | {text.slice(lastIndex, endIndex)} 31 | 32 | ); 33 | 34 | lastIndex = endIndex; 35 | regex.lastIndex = endIndex; 36 | } 37 | 38 | if (lastIndex < text.length) { 39 | parts.push(text.slice(lastIndex)); 40 | } 41 | 42 | return parts; 43 | }; 44 | -------------------------------------------------------------------------------- /src/pages/device.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | 3 | import { useJacdacStore } from "../store/jacdacStore"; 4 | import { DisconnectState } from "../components/DevsDownload"; 5 | import { 6 | SRV_ROLE_MANAGER, 7 | RoleManagerClient 8 | } from 'jacdac-ts' 9 | import Layout from "@theme/Layout"; 10 | 11 | 12 | export default function DeivceManager (){ 13 | const {spec, connected, device} = useJacdacStore() 14 | 15 | useEffect(() => { 16 | if (!device) return 17 | const _service = device.services({serviceClass: SRV_ROLE_MANAGER}) 18 | if (_service.length){ 19 | const roleManagerClient = new RoleManagerClient(_service[0]) 20 | roleManagerClient.startRefreshRoles() 21 | roleManagerClient.subscribe("change", (role) => { 22 | console.log("change", role) 23 | }) 24 | console.log("roleManagerClient", roleManagerClient.allRolesBound()) 25 | for (const role of roleManagerClient.roles){ 26 | console.log("role", role) 27 | } 28 | } 29 | 30 | }, [device]) 31 | 32 | return ( 33 | 34 |
35 |

Device

36 | {connected ? null : } 37 | 38 | 39 |
40 |
41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /src/pages/editor.tsx: -------------------------------------------------------------------------------- 1 | // A code snippet editor for devicescript 2 | import React, { useEffect, useState } from 'react'; 3 | import Wrapper from "@theme/Layout"; 4 | import CodeEditor from "../components/codeEditor"; 5 | import { useDevsStore } from '../store/devsStore'; 6 | import {JDConnection} from '../components/DevsDownload'; 7 | import { Button } from 'antd'; 8 | 9 | const DevsEditor = () => { 10 | const [code, setCode] = useState('// Write your code here') 11 | const { 12 | compileWithHost 13 | } = useDevsStore() 14 | 15 | const handleCompile = async () => { 16 | const ret = await compileWithHost(code) 17 | console.log('Compiling code', ret) 18 | } 19 | 20 | return ( 21 | 22 | 23 | setCode(c)} 26 | /> 27 | 28 | 29 | ); 30 | 31 | 32 | 33 | } 34 | 35 | export default DevsEditor; 36 | -------------------------------------------------------------------------------- /src/pages/hostapp.module.css: -------------------------------------------------------------------------------- 1 | 2 | .servicelist{ 3 | padding: 30px; 4 | } 5 | 6 | .serviceicon { 7 | width: 48px; 8 | height: 48px; 9 | } 10 | .description{ 11 | white-space: nowrap; 12 | } -------------------------------------------------------------------------------- /src/pages/hostapp.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useEffect, useState } from 'react'; 3 | import { Card, Col, Row, List, Button, Menu, Layout, Switch } from 'antd' 4 | 5 | import styles from './hostapp.module.css'; 6 | import Settings from '../components/Hostapp/settings'; 7 | import LLMS from '../components/Hostapp/llms'; 8 | import Status from '../components/Hostapp/status'; 9 | 10 | const { Header, Sider, Content } = Layout; 11 | 12 | function ServiceCard(props: { name: string, status: boolean, icon: string, toggle: any,disabled?: boolean}) { 13 | 14 | return ( 20 | ]} 21 | > 22 | } 25 | title={{props.name}} 26 | description={{props.status ? "Running" : "Stopped"}} 27 | /> 28 | ) 29 | } 30 | 31 | export default function HostApp() { 32 | 33 | const [services, setServices] = useState([]) 34 | const [tabs,setTabs] = useState ('1') 35 | 36 | const handleToggleService = async (status: boolean, name: string) => { 37 | const { start_service, stop_service } = window.electronAPI 38 | if (status) { 39 | const _nextServices = await stop_service(name) 40 | console.log("services", _nextServices) 41 | setServices(_nextServices) 42 | } else { 43 | const _nextServices = await start_service(name) 44 | console.log("services", _nextServices) 45 | setServices(_nextServices) 46 | } 47 | } 48 | 49 | useEffect(() => { 50 | const { get_services } = window.electronAPI 51 | // get services from backend 52 | get_services().then((services: any) => { 53 | console.log("services", services) 54 | setServices(services) 55 | }) 56 | }, []) 57 | 58 | return ( 59 | 60 | 61 | setTabs(e.key)} 66 | > 67 | Status 68 | Services 69 | Settings 70 | LLM 71 | 72 | 73 | 74 | {tabs === '1' && } 75 | {tabs === '2' && ( 79 | 80 | handleToggleService(item.status, item.name)}/> 81 | 82 | )} 83 | />} 84 | {tabs === '3' && } 85 | {tabs === '4' && } 86 | 87 | 88 | 89 | ); 90 | } 91 | 92 | -------------------------------------------------------------------------------- /src/pages/hostchat.css: -------------------------------------------------------------------------------- 1 | .llm-config-btn { 2 | right: unset !important; 3 | left: unset !important; 4 | top: 16px !important; 5 | right: 16px !important; 6 | } -------------------------------------------------------------------------------- /src/pages/hostchat.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useRef, useState } from "react"; 2 | import { debounce } from 'lodash'; 3 | import {FloatButton} from 'antd' 4 | import { ChatMessage, ProChat, ProChatInstance } from '@ant-design/pro-chat'; 5 | import './hostchat.css'; 6 | import { LLMConfig, LLMConfigModal, LLMMsg } from "../components/Hostapp/llms"; 7 | 8 | 9 | const HostChat = () => { 10 | const chatRef = useRef(); 11 | const [llm, setLLM] = useState(); 12 | const [llmEdit, setLLMEdit] = useState(); 13 | 14 | const [hash, setHash] = useState('0/0'); 15 | const [promt, setPromt] = useState(''); 16 | const [context, setContext] = useState([]); 17 | const [settings, setSettings] = useState({}); 18 | 19 | 20 | useEffect(() => { 21 | const { get_settings, onUserText, onLoadLLM } = window.electronAPI; 22 | get_settings().then((settings: any) => { 23 | setSettings(settings); 24 | }); 25 | onUserText((text: string) => { 26 | text = text.trim(); 27 | console.log("input", text) 28 | text && chatRef.current?.sendMessage(text); 29 | }); 30 | 31 | onLoadLLM((llm: LLMConfig) => { 32 | console.log("on llm loaded", llm) 33 | setLLM(llm); 34 | setContext(llm.context); 35 | setPromt(llm.system || 'You are a helpful assistant.'); 36 | setHash(llm.id); 37 | }); 38 | 39 | const param = new URLSearchParams(window.location.search); 40 | const id = param.get('id') // llmid/historyid 41 | setHash(id); 42 | }, []); 43 | 44 | const saveHistory = useMemo(() => { 45 | return (messages: LLMMsg[]) => { 46 | const { save_history } = window.electronAPI; 47 | save_history({ id: hash, history: messages }); 48 | } 49 | }, [hash]); 50 | 51 | const handleSaveLLM = (values: any) => { 52 | if (values) { 53 | const { save_llm } = window.electronAPI; 54 | save_llm({id: values.id, llm: values}); 55 | setLLM(values); 56 | } 57 | setLLMEdit(null); 58 | } 59 | 60 | 61 | const debouncedSaveHistory = debounce(saveHistory, 1000); 62 | 63 | return ( 64 |
65 | setLLMEdit(llm)} /> 66 | 71 | {promt ? { 78 | console.log('onChatsChange', messages); 79 | debouncedSaveHistory(messages); 80 | }} 81 | request={async (inputValue) => { 82 | console.log('request', inputValue); 83 | const messages = inputValue.map((item) => { 84 | return { 85 | role: item.role, 86 | content: item.content 87 | } 88 | }) 89 | if (llm.historyLength){ 90 | // only keep the last Nx2 messages 91 | messages.splice(0, messages.length - llm.historyLength * 2); 92 | 93 | } 94 | const url = settings.openaiUrl || 'http://127.0.0.1:11434/v1/chat/completions' 95 | const res = await fetch(url, { 96 | method: 'POST', 97 | headers: { 98 | 'Content-Type': 'application/json', 99 | 'Authorization': 'Bearer ' + settings.openaiKey 100 | }, 101 | body: JSON.stringify({ 102 | model: settings.openaiModel || 'llama2', 103 | messages: [ 104 | { 105 | role: 'system', 106 | content: promt 107 | }, 108 | ...messages 109 | ], 110 | stream: true 111 | }) 112 | }); 113 | if (!res.ok) { 114 | throw new Error('Failed to send message'); 115 | } 116 | 117 | const reader = res.body?.getReader(); 118 | const decoder = new TextDecoder(); 119 | const encoder = new TextEncoder(); 120 | let done = false; 121 | 122 | const readableStream = new ReadableStream({ 123 | async start(controller) { 124 | while (!done) { 125 | const { value, done: _done } = await reader.read(); 126 | done = _done; 127 | const _chunk = decoder.decode(value, { stream: true }); 128 | let botMsg = ''; 129 | const lines = _chunk.split('\n').filter((line) => line.trim()); 130 | for (const line of lines) { 131 | if (line.includes('[DONE]')) { 132 | break; 133 | } else if (line.startsWith('data: ')) { 134 | const message = JSON.parse(line.substring(6)); 135 | if (message.choices && message.choices[0].delta?.content) { 136 | const _txt: string = message.choices[0].delta.content; 137 | botMsg += _txt; 138 | controller.enqueue(encoder.encode(_txt)); 139 | } 140 | // if (message.choices && message.choices[0].finish_reason === 'stop') { 141 | // done = true; 142 | // controller.close(); 143 | // } 144 | } 145 | } 146 | 147 | } // end while 148 | controller.close(); 149 | } 150 | }); 151 | return new Response(readableStream); 152 | }} 153 | /> : null} 154 | 155 |
156 | ); 157 | } 158 | 159 | export default HostChat; 160 | 161 | -------------------------------------------------------------------------------- /src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | @media screen and (max-width: 996px) { 14 | .heroBanner { 15 | padding: 2rem; 16 | } 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import Link from '@docusaurus/Link'; 3 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 4 | import Layout from '@theme/Layout'; 5 | import Heading from '@theme/Heading'; 6 | import Showcase from './showcase'; 7 | 8 | import React from 'react'; 9 | 10 | export default function Home(): JSX.Element { 11 | const {siteConfig} = useDocusaurusContext(); 12 | return ( 13 | 16 |
17 | 18 |
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/pages/keymap.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .react-simple-keyboard { 4 | margin: auto; 5 | width: 75vw; 6 | } 7 | 8 | .hg-button-space { 9 | width: 300px; 10 | } 11 | .keymap .react-simple-keyboard{ 12 | width: 100%; 13 | } -------------------------------------------------------------------------------- /src/pages/keymap.tsx: -------------------------------------------------------------------------------- 1 | import Layout from "@theme/Layout"; 2 | 3 | import Keyboard from "react-simple-keyboard"; 4 | import "react-simple-keyboard/build/css/index.css"; 5 | import { DisconnectState, JDConnection } from "../components/DevsDownload"; 6 | import { 7 | jdpack, Packet, jdunpack, 8 | JDDevice, 9 | SRV_SETTINGS, 10 | } from 'jacdac-ts' 11 | 12 | import "./keymap.css"; 13 | import { useEffect, useMemo, useRef, useState } from "react"; 14 | import React from "react"; 15 | import { useJacdacStore } from "../store/jacdacStore"; 16 | import Elite60 from "../components/Keymap/elite60"; 17 | import NumberPad from "../components/Keymap/numpad"; 18 | 19 | // for testing 20 | const testKeymap = [ 21 | // esc, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, -, =, backspace 22 | 0x29, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23,0x24, 0x25, 0x26, 0x27, 0x2d, 0x2e, 0x2a, 23 | // tab, q, w, e, r, t, y, u, i, o, p, [, ], slash 24 | 0x2b, 0x14, 0x1a, 0x08, 0x15, 0x17, 0x1c, 0x18, 0x0c, 0x12, 0x13, 0x2f, 0x30, 0x31, 25 | // cap, a, s, d, f, g, h, j, k, l, ;, ', enter 26 | 0x39, 0x04, 0x16, 0x07, 0x09, 0x0a, 0x0b, 0x0d, 0x0e, 0x0f, 0x33, 0x34, 0x28, 0x00, 27 | // lshift, z, x, c, v, b, n, m, ,, ., /, rshift, up, del 28 | 0xe1, 0x1d, 0x1b, 0x06, 0x19, 0x05, 0x11, 0x10, 0x36, 0x37, 0x38, 0xe5, 0x52, 0x4c, 29 | // lctrl, cmd, alt, null, null, space, null, null, null, fn, rctrl, left, down, right 30 | 0xe0, 0xe3, 0xe2, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0xe6, 0xFF, 0x50, 0x51, 0x4f, 31 | ] 32 | 33 | export default function Keymap() { 34 | const [keyboard, setKeyboard] = useState('elite60'); 35 | const [keymap, setKeymap] = useState(testKeymap) 36 | 37 | const {spec, device} = useJacdacStore() 38 | 39 | async function readKeymap(device: JDDevice) { 40 | const settingService = device.services({serviceClass: SRV_SETTINGS})[0] 41 | const pkt = Packet.from(0x80, jdpack('s', ['keymap'])) 42 | const ret = await settingService.sendCmdAwaitResponseAsync(pkt) 43 | const [key, value] = jdunpack<[string, Uint8Array]>(ret.data, 'z b') 44 | // TODO: refactor to jacdac config format 45 | } 46 | 47 | useEffect(() => { 48 | if (spec?.productIdentifiers.includes(0x3ae6b1e2)){ 49 | setKeyboard('numpad') 50 | } else if (spec?.productIdentifiers.includes(0x3458bc2a)){ 51 | setKeyboard('elite60') 52 | } 53 | }, [spec]) 54 | 55 | useEffect(() => { 56 | if (device) { 57 | readKeymap(device) 58 | } 59 | 60 | }, [device]) 61 | 62 | const handleUpdate = (index: number, value: number) => { 63 | console.log("update", index, value) 64 | const _keymap = [...keymap] 65 | _keymap[index] = value 66 | setKeymap(_keymap) 67 | } 68 | 69 | return ( 70 | <> 71 | 72 |
73 |

Keymap

74 | 75 | {keyboard === 'elite60' ? : null} 76 | {keyboard === 'numpad' ? : null} 77 | 78 |
79 |
80 | 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /src/pages/markdown-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown page example 3 | --- 4 | 5 | # Markdown page example 6 | 7 | You don't need React to write simple standalone pages. 8 | -------------------------------------------------------------------------------- /src/pages/test.tsx: -------------------------------------------------------------------------------- 1 | import DevsDownloadCard from "@site/src/components/DevsDownload"; 2 | 3 | 4 | export default function Page() { 5 | 6 | return ( 7 |
8 | 9 |
10 | ) 11 | 12 | } 13 | 14 | -------------------------------------------------------------------------------- /src/remark/render-skill.ts: -------------------------------------------------------------------------------- 1 | import type { Transformer } from "unified"; 2 | import type { Code, Literal, Root } from "mdast"; 3 | import type { Node, Parent } from "unist"; 4 | 5 | import fs from 'fs-extra'; 6 | import path from 'path'; 7 | 8 | export interface Skill { 9 | shortid?: string; 10 | name: string; 11 | code?: string; 12 | main?: string; 13 | description?: string; 14 | tags?: string[]; 15 | dependencies?: string[]; 16 | params?: Record 17 | 18 | } 19 | 20 | function createImportNode() { 21 | return { 22 | type: "mdxjsEsm", 23 | value: "import DevsDownload from '@site/src/components/DevsDownload'", 24 | data: { 25 | estree: { 26 | type: "Program", 27 | body: [ 28 | { 29 | type: "ImportDeclaration", 30 | specifiers: [ 31 | { 32 | type: "ImportDefaultSpecifier", 33 | local: { type: "Identifier", name: "DevsDownload" }, 34 | }, 35 | ], 36 | source: { 37 | type: "Literal", 38 | value: "@site/src/components/DevsDownload", 39 | raw: "'@site/src/components/DevsDownload'", 40 | }, 41 | }, 42 | ], 43 | sourceType: "module", 44 | }, 45 | }, 46 | }; 47 | } 48 | 49 | function transformNode( 50 | node: Node, 51 | newNode: NewNode, 52 | ): NewNode { 53 | Object.keys(node).forEach((key) => { 54 | // @ts-expect-error: unsafe but ok 55 | delete node[key]; 56 | }); 57 | Object.keys(newNode).forEach((key) => { 58 | // @ts-expect-error: unsafe but ok 59 | node[key] = newNode[key]; 60 | }); 61 | return node as NewNode; 62 | } 63 | 64 | export default function plugin(): Transformer { 65 | const visited = new Set(); // visit called twice on async 66 | 67 | return async (root, vfile) => { 68 | const { visit } = await import("unist-util-visit"); 69 | 70 | // visit(root, "mdxJsxFlowElement", (node: Node, nodeIndex: number, parent: Parent) => { 71 | // console.log(node); 72 | // }) 73 | 74 | visit(root, "code", (node: Code, nodeIndex: number, parent: Parent) => { 75 | // if (!parent || visited.has(node)) return; 76 | // visited.add(node); 77 | 78 | if (node.lang === "devs") { 79 | 80 | const { lang, meta, value } = node; 81 | let skill = {code: value} as Skill; 82 | 83 | const directory = path.dirname(vfile.history[0]); 84 | const jsonFilePath = path.join(directory, "skill.json"); 85 | if (fs.pathExistsSync(jsonFilePath)) { 86 | let mainFile = 'main.ts' 87 | skill = fs.readJSONSync(jsonFilePath) as Skill; 88 | if (skill.code) 89 | skill.code = skill.code; 90 | if (skill.main){ 91 | mainFile = skill.main; 92 | } 93 | const mainFilePath = path.join(directory, mainFile); 94 | if (fs.pathExistsSync(mainFilePath)) { 95 | skill.code = fs.readFileSync(mainFilePath, 'utf-8'); 96 | } 97 | } 98 | 99 | 100 | // // insert mdx to current node 101 | // parent.children.splice(startIndex, 1, ...mdx); 102 | transformNode(node, { 103 | type: "mdxJsxFlowElement", 104 | name: "DevsDownload", 105 | attributes: [ 106 | { 107 | type: "mdxJsxAttribute", 108 | name: "name", 109 | value: "test", 110 | }, 111 | { 112 | type: "mdxJsxAttribute", 113 | name: "config", 114 | value: JSON.stringify(skill), 115 | }, 116 | ], 117 | data: { _mdxExplicitJsx: true }, 118 | position: node.position, 119 | children: [], 120 | }); 121 | } 122 | }); 123 | 124 | (root as Parent).children.unshift(createImportNode()); 125 | }; 126 | } 127 | -------------------------------------------------------------------------------- /src/store/devsStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | const loadCompiler = async () => { 4 | if (typeof window !== "undefined") { 5 | const module = await import("@kittenbot/devs_compiler"); 6 | return module.compileWithHost; 7 | } 8 | }; 9 | 10 | export const useDevsStore = create<{ 11 | code: string; 12 | params: Record; 13 | extraLibs: Record 14 | setCode: (code: string) => void; 15 | setParams: (params: Record) => void; 16 | compileWithHost: (code?: string) => Promise; 17 | setExtraLibs: (libs: Record) => void; 18 | compiler: any; 19 | }>((set, get) => { 20 | const devsState = { 21 | code: "// Write your code here", 22 | params: {}, 23 | extraLibs: {}, 24 | setCode: (code: string) => set({ code }), 25 | setParams: (params) => set({ params }), 26 | compileWithHost: null, 27 | compiler: null, 28 | setExtraLibs: (libs) => set({ extraLibs: libs }), 29 | }; 30 | 31 | loadCompiler().then((compiler) => { 32 | const compileWithHost = async (userCode) => { 33 | const { code, params, extraLibs } = get(); 34 | console.log("compileWithHost", code, params, extraLibs); 35 | if (!compiler) { 36 | console.error("Compiler not loaded"); 37 | return; 38 | } 39 | const { DevsHost } = await import("../lib/DevsHost"); 40 | let _code = userCode || code; 41 | for (const key in params) { 42 | const value = params[key]; 43 | _code = code.replace(new RegExp(`\\$${key}`, "g"), value); 44 | } 45 | const host = new DevsHost({ 46 | hwInfo: { 47 | // progName: "DeviceScript-workspace devs/hello", 48 | // progVersion: "6.0.0", 49 | }, 50 | files: { 51 | "src/main.ts": _code, 52 | ...extraLibs 53 | }, 54 | }); 55 | const result = compiler("src/main.ts", host); 56 | return result; 57 | }; 58 | set({ compiler, compileWithHost }); 59 | }); 60 | 61 | return devsState; 62 | }); 63 | -------------------------------------------------------------------------------- /src/store/jacdacStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | import { 3 | createWebSerialTransport, 4 | createUSBTransport, 5 | createWebSocketTransport, 6 | deviceCatalogImage, 7 | JDBus, 8 | JDDevice, 9 | JDService, 10 | DeviceSpec, 11 | Transport, 12 | DeviceScriptManagerCmd,OutPipe, 13 | DEVICE_ANNOUNCE, DEVICE_CHANGE, CONNECTION_STATE, SRV_DEVICE_SCRIPT_MANAGER,SRV_ROLE_MANAGER, 14 | SRV_SETTINGS, Packet 15 | } from 'jacdac-ts' 16 | 17 | const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) 18 | 19 | export const useJacdacStore = create<{ 20 | bus: JDBus; 21 | webSerialConnected: boolean; 22 | webSocketConnected: boolean; 23 | device: JDDevice; 24 | spec: DeviceSpec; 25 | brainAvatar: string 26 | deviceAvatar: string[]; 27 | devsService: JDService; 28 | downloadProgress: number; 29 | downloadErr: string; 30 | 31 | connectJDBus: (usbConnect?:boolean) => Promise; 32 | refresh: () => Promise; 33 | downloadDevs: (bytecode: any) => Promise; 34 | 35 | }>((set, get) => ({ 36 | bus: null, 37 | webSerialConnected: false, 38 | webSocketConnected: false, 39 | spec: null, 40 | device: null, 41 | devsService: null, 42 | brainAvatar: null, 43 | deviceAvatar: [], 44 | downloadProgress: -1, 45 | downloadErr: '', 46 | 47 | refresh: async () => { 48 | const bus = get().bus 49 | if (!bus) return 50 | const devices: JDDevice[] = bus.devices({ 51 | ignoreInfrastructure: true, 52 | announced: true, 53 | }) 54 | const _nextDeviceAvatar = [] 55 | for (const device of devices){ 56 | if (device.hasService(SRV_DEVICE_SCRIPT_MANAGER)){ 57 | const devsService = device.services({serviceClass: SRV_DEVICE_SCRIPT_MANAGER})[0] 58 | const spec = bus.deviceCatalog.specificationFromProductIdentifier( 59 | device.productIdentifier 60 | ); 61 | const img = deviceCatalogImage(spec, "list") 62 | set(state => ({ spec, devsService, brainAvatar: img, device })); 63 | } else { 64 | let productIdentifier = device.productIdentifier 65 | while(!productIdentifier){ 66 | await sleep(100) 67 | productIdentifier = device.productIdentifier 68 | } 69 | const spec = bus.deviceCatalog.specificationFromProductIdentifier(device.productIdentifier) 70 | console.log("spec", device.productIdentifier, spec) 71 | if (!spec) continue 72 | const img = deviceCatalogImage(spec, "list") 73 | _nextDeviceAvatar.push({img,name:spec.name}) 74 | } 75 | } 76 | set(state => ({ deviceAvatar: _nextDeviceAvatar })) 77 | }, 78 | 79 | connectJDBus: async (usbConnect?:boolean) => { 80 | let bus: JDBus = get().bus 81 | if (!bus || (usbConnect && bus._transports[0].type === "serial") || (!usbConnect && bus._transports[0].type === 'usb')) { 82 | const transports = [ 83 | usbConnect ? createUSBTransport() :createWebSerialTransport(), 84 | createWebSocketTransport('ws://localhost:8081') 85 | ]; 86 | bus = new JDBus(transports, { 87 | client: false, 88 | disableRoleManager: true, 89 | }); 90 | bus.on(DEVICE_CHANGE, async () => { 91 | await sleep(100) 92 | get().refresh() 93 | }) 94 | 95 | bus.on(CONNECTION_STATE, (transport: Transport) => { 96 | if (transport.type === "web") { 97 | set(state => ({ webSocketConnected: transport.connectionState === 'connected' })); 98 | } else if (transport.type === "serial" || transport.type === 'usb') { 99 | set(state => ({ webSerialConnected: transport.connectionState === 'connected' })); 100 | } 101 | }); 102 | // for debug only 103 | (window as any).bus = bus 104 | } 105 | 106 | await bus.connect(); 107 | 108 | set(state => ({ bus })); 109 | 110 | }, 111 | 112 | downloadDevs: async (bytecode: any) => { 113 | const devsService = get().devsService 114 | if (!devsService) return 'No device script manager service found, please connect to hardware' 115 | try { 116 | await OutPipe.sendBytes(devsService, DeviceScriptManagerCmd.DeployBytecode, bytecode, p => { 117 | set(state => ({ downloadProgress: p*100 })) 118 | }) 119 | } catch (error) { 120 | set(state => ({ downloadErr: error.toString() })) 121 | return error.toString() 122 | } finally { 123 | set(state => ({ downloadProgress: -1 })) 124 | } 125 | return '' 126 | } 127 | 128 | 129 | })) -------------------------------------------------------------------------------- /src/store/skillsStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | import { useDevsStore } from './devsStore' 3 | import { 4 | SkillConfig, 5 | SkillParam, 6 | SkillCategory, 7 | SkillEvent, 8 | Build, 9 | generateDeviceScript, 10 | getSkillById 11 | } from '../lib/SkillBuild' 12 | 13 | const loadOrCreateBuild = (key: string): Build => { 14 | let _build: Build = null 15 | const _buildStr = localStorage.getItem(key) 16 | if (!_buildStr) { 17 | // init a initial build 18 | _build = { 19 | id: key, 20 | name: key, 21 | hardware: '', 22 | events: [] 23 | } 24 | } else { 25 | _build = JSON.parse(_buildStr) 26 | // compatibility check 27 | if (!_build.events) { 28 | _build.events = [] 29 | } 30 | } 31 | return _build 32 | } 33 | 34 | const initialBuilds = (): string[] => { 35 | let builds: string[] = [] 36 | // ssr check 37 | if (typeof window == 'undefined') { 38 | return builds 39 | } 40 | let _buildsStr = localStorage.getItem('skillbuilds') 41 | if (!_buildsStr) { 42 | builds = ["My Build"] 43 | // init a initial build 44 | const _temp_build = loadOrCreateBuild('My Build') 45 | localStorage.setItem('My Build', JSON.stringify(_temp_build)) 46 | localStorage.setItem('skillbuilds', JSON.stringify(builds)) 47 | } else { 48 | builds = JSON.parse(_buildsStr) 49 | } 50 | return builds 51 | } 52 | 53 | export const useSkillsStore = create<{ 54 | build: Build 55 | builds: string[] 56 | skills: Record 57 | loadSkills: (skills: Record) => void 58 | generate: () => string // generate code for current build 59 | load: (key: string) => void 60 | addEvent: (skill: SkillEvent) => void 61 | saveEvent: (skill: SkillEvent) => void 62 | deleteEvent: (skill: SkillEvent) => void 63 | getSkill: (id: string) => SkillConfig 64 | // delete: (key: string) => void 65 | }>((set, get) => ({ 66 | build: {id: '', name: '', hardware: '', events: []}, 67 | builds: [], 68 | skills: {}, 69 | extraLibs: {}, 70 | getSkill: (id: string) => { 71 | return getSkillById(id, get().skills) 72 | }, 73 | loadSkills: (skills: Record) => { 74 | const _builds = initialBuilds() 75 | // load first build 76 | const _build = loadOrCreateBuild(_builds[0]) 77 | 78 | set({ skills, builds: _builds, build: _build }) 79 | // TODO: load user indexdb saved skills ?? 80 | }, 81 | load: (key: string) => { 82 | if (key === get().build?.id) return 83 | const _buildStr = localStorage.getItem(key) 84 | if (!_buildStr) { 85 | console.error('No build found with key', key) 86 | return 87 | } 88 | const _build: Build = JSON.parse(_buildStr) 89 | set({ build: _build }) 90 | }, 91 | generate: () => { 92 | const { build, skills } = get() 93 | const {code, libs} = generateDeviceScript(build, skills) 94 | const { setExtraLibs } = useDevsStore.getState() 95 | setExtraLibs(libs) 96 | 97 | return code 98 | }, 99 | addEvent: (skill: SkillEvent) => { 100 | const _build = get().build 101 | if (!_build) { 102 | console.error('No build found') 103 | return 104 | } 105 | // find if the event exists, both id and key are same 106 | if (_build.events.find((e) => e.id === skill.id && e.key === skill.key)) { 107 | _build.events = _build.events.filter((e) => e.id !== skill.id && e.key !== skill.key) 108 | } 109 | _build.events.push(skill) 110 | localStorage.setItem(_build.id, JSON.stringify(_build)) 111 | set({ build: _build }) 112 | }, 113 | saveEvent: (skill: SkillEvent) => { 114 | const _build = get().build 115 | if (!_build) { 116 | console.error('No build found') 117 | return 118 | } 119 | // update the build event 120 | for (let i = 0; i < _build.events.length; i++) { 121 | if (_build.events[i].id === skill.id && _build.events[i].key === skill.key) { 122 | _build.events[i] = skill 123 | break 124 | } 125 | } 126 | localStorage.setItem(_build.id, JSON.stringify(_build)) 127 | set({ build: _build }) 128 | }, 129 | deleteEvent: (skill: SkillEvent) => { 130 | const _build = get().build 131 | if (!_build) { 132 | console.error('No build found') 133 | return 134 | } 135 | _build.events = _build.events.filter((e) => e.id !== skill.id && e.key !== skill.key) 136 | localStorage.setItem(_build.id, JSON.stringify(_build)) 137 | set({ build: _build }) 138 | } 139 | })) 140 | -------------------------------------------------------------------------------- /src/test/skillbuild.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import SkillBuild from '../lib/SkillBuild' 3 | 4 | test('SkillButton', async () => { 5 | const _txt = fs.readFileSync('src/test/testskill.js', 'utf-8') 6 | const _skill = await SkillBuild(_txt) 7 | console.log(_skill) 8 | }) -------------------------------------------------------------------------------- /src/test/testskill.js: -------------------------------------------------------------------------------- 1 | 2 | const template = ` 3 | import * as ds from "@devicescript/core" 4 | const kb = new ds.KeyboardClient() 5 | const _key#KEY# = kb.button(HidKeyboardSelector.#KEY#) 6 | _key#KEY#.subscribe(() => { 7 | #CALLBACK# 8 | }) 9 | 10 | ` 11 | 12 | awagent({ 13 | id: 'testskill', 14 | name: 'Test Skill', 15 | description: 'This is a test skill', 16 | 17 | }) 18 | 19 | 20 | 21 | md` 22 | # Test Skill 23 | 24 | This is a test skill 25 | 26 | ## Code 27 | \`\`\`js 28 | 29 | \`\`\` 30 | 31 | ## Usage 32 | 33 | Prints a message to the console 34 | 35 | ` -------------------------------------------------------------------------------- /static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/.nojekyll -------------------------------------------------------------------------------- /static/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/assets/.keep -------------------------------------------------------------------------------- /static/firmwares/app-acc-da213.uf2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/firmwares/app-acc-da213.uf2 -------------------------------------------------------------------------------- /static/firmwares/app-env-aht20.uf2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/firmwares/app-env-aht20.uf2 -------------------------------------------------------------------------------- /static/firmwares/app-neopixel.uf2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/firmwares/app-neopixel.uf2 -------------------------------------------------------------------------------- /static/firmwares/app-servo.uf2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/firmwares/app-servo.uf2 -------------------------------------------------------------------------------- /static/firmwares/devicescript-esp32c3-kitten_grapebit_c3-0x0.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/firmwares/devicescript-esp32c3-kitten_grapebit_c3-0x0.bin -------------------------------------------------------------------------------- /static/firmwares/devicescript-rp2040-kitten_mkc.uf2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/firmwares/devicescript-rp2040-kitten_mkc.uf2 -------------------------------------------------------------------------------- /static/firmwares/devicescript-rp2040-kitten_numpad.uf2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/firmwares/devicescript-rp2040-kitten_numpad.uf2 -------------------------------------------------------------------------------- /static/img/Maskgroup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/img/Maskgroup.png -------------------------------------------------------------------------------- /static/img/Rectangle92.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/img/Rectangle92.png -------------------------------------------------------------------------------- /static/img/Rectangle93.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/img/Rectangle93.png -------------------------------------------------------------------------------- /static/img/Rectangle94.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/img/Rectangle94.png -------------------------------------------------------------------------------- /static/img/Rectangle95.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/img/Rectangle95.png -------------------------------------------------------------------------------- /static/img/agilewhisker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/img/agilewhisker.png -------------------------------------------------------------------------------- /static/img/aircraftWar.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/img/aircraftWar.mp4 -------------------------------------------------------------------------------- /static/img/ambient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/img/ambient.png -------------------------------------------------------------------------------- /static/img/blog/modular.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/img/blog/modular.gif -------------------------------------------------------------------------------- /static/img/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/img/cloud.png -------------------------------------------------------------------------------- /static/img/coomu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/img/coomu.png -------------------------------------------------------------------------------- /static/img/docusaurus-social-card.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/img/docusaurus-social-card.jpg -------------------------------------------------------------------------------- /static/img/docusaurus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/img/docusaurus.png -------------------------------------------------------------------------------- /static/img/event.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/img/event.png -------------------------------------------------------------------------------- /static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/img/favicon.ico -------------------------------------------------------------------------------- /static/img/gif_1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/img/gif_1.gif -------------------------------------------------------------------------------- /static/img/gif_2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/img/gif_2.gif -------------------------------------------------------------------------------- /static/img/gif_3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/img/gif_3.gif -------------------------------------------------------------------------------- /static/img/gif_4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/img/gif_4.gif -------------------------------------------------------------------------------- /static/img/iot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/img/iot.png -------------------------------------------------------------------------------- /static/img/jacdac1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/img/jacdac1.png -------------------------------------------------------------------------------- /static/img/monitor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/img/monitor.png -------------------------------------------------------------------------------- /static/img/quickstart/elite60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/img/quickstart/elite60.png -------------------------------------------------------------------------------- /static/img/quickstart/keyboard-sample-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/img/quickstart/keyboard-sample-1.gif -------------------------------------------------------------------------------- /static/img/quickstart/keyboard-sample-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/img/quickstart/keyboard-sample-2.png -------------------------------------------------------------------------------- /static/img/quickstart/keypad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/img/quickstart/keypad.png -------------------------------------------------------------------------------- /static/img/quickstart/vscode-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/img/quickstart/vscode-1.png -------------------------------------------------------------------------------- /static/img/quickstart/vscode-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/img/quickstart/vscode-2.png -------------------------------------------------------------------------------- /static/img/quickstart/vscode-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/img/quickstart/vscode-3.png -------------------------------------------------------------------------------- /static/img/quickstart/vscode-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/img/quickstart/vscode-4.png -------------------------------------------------------------------------------- /static/img/screenPic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/img/screenPic.jpg -------------------------------------------------------------------------------- /static/img/sliderVolume.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/img/sliderVolume.mp4 -------------------------------------------------------------------------------- /static/img/ultrasonic.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/img/ultrasonic.mp4 -------------------------------------------------------------------------------- /static/img/video.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/img/video.mp4 -------------------------------------------------------------------------------- /static/js/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KittenBot/agilewhisker-web/e01104a464608eac6ce8a6a4e387d8ed589046e7/static/js/.keep -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@docusaurus/tsconfig", 4 | "compilerOptions": { 5 | "jsx": "react", 6 | "baseUrl": ".", 7 | "paths": { 8 | "@/*": ["./src/*"] 9 | }, 10 | "types": ["node", "jest"] 11 | } 12 | } 13 | --------------------------------------------------------------------------------