├── .gitattributes ├── .github └── workflows │ ├── stale.yml │ └── sync_issues.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── _locales ├── de │ └── Grove-strings.json └── ja │ └── Grove-strings.json ├── blocks ├── GroveAHT20.ts ├── GroveDHT11.ts ├── GroveLCD1602v1.ts └── GroveVisionAIV2.ts ├── icon.png ├── main.ts ├── plugins └── SSCMA.ts ├── pxt.json ├── sensors ├── AHT20.ts ├── DHT11.cpp └── DHT11Helper.ts ├── test.ts └── tsconfig.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '* 4 * * *' 7 | 8 | jobs: 9 | stale: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v4 15 | 16 | - name: Checkout script repository 17 | uses: actions/checkout@v4 18 | with: 19 | repository: Seeed-Studio/sync-github-all-issues 20 | path: ci 21 | 22 | - name: Run script 23 | run: ./ci/tools/stale.sh 24 | env: 25 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /.github/workflows/sync_issues.yml: -------------------------------------------------------------------------------- 1 | name: Automate Issue Management 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | - edited 8 | - assigned 9 | - unassigned 10 | - labeled 11 | - unlabeled 12 | - reopened 13 | 14 | jobs: 15 | add_issue_to_project: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Add issue to GitHub Project 19 | uses: actions/add-to-project@v1.0.2 20 | with: 21 | project-url: https://github.com/orgs/Seeed-Studio/projects/17 22 | github-token: ${{ secrets.ISSUE_ASSEMBLE }} 23 | labeled: bug 24 | label-operator: NOT -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear in the root of a volume 35 | .DocumentRevisions-V100 36 | .fseventsd 37 | .Spotlight-V100 38 | .TemporaryItems 39 | .Trashes 40 | .VolumeIcon.icns 41 | 42 | # Directories potentially created on remote AFP share 43 | .AppleDB 44 | .AppleDesktop 45 | Network Trash Folder 46 | Temporary Items 47 | .apdisk 48 | 49 | node_modules/ 50 | pxt_modules/ 51 | built/ 52 | .python-version 53 | package-lock.json 54 | projects/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Seeed Studio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: deploy 2 | 3 | build: 4 | pxt build 5 | 6 | deploy: 7 | pxt deploy 8 | 9 | test: 10 | pxt test 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Grove 2 | 3 | A Microsoft MakeCode package for for Seeed Studio Grove module. 4 | 5 | ## Basic usage 6 | 7 | ### Grove - Gesture 8 | 9 | get gesture model. 10 | 11 | ```blocks 12 | 13 | 14 | grove.onGesture(GroveGesture.Up, () => { 15 | basic.showString("Up"); 16 | }) 17 | grove.onGesture(GroveGesture.Down, () => { 18 | basic.showString("Down"); 19 | }) 20 | 21 | 22 | grove.initGesture() 23 | basic.forever(function () { 24 | if (grove.getGestureModel() == 1) { 25 | basic.showLeds(` 26 | . . # . . 27 | . . . # . 28 | # # # # # 29 | . . . # . 30 | . . # . . 31 | `) 32 | } 33 | if (grove.getGestureModel() == 2) { 34 | basic.showLeds(` 35 | . . # . . 36 | . # . . . 37 | # # # # # 38 | . # . . . 39 | . . # . . 40 | `) 41 | } 42 | if (grove.getGestureModel() == 3) { 43 | basic.showLeds(` 44 | . . # . . 45 | . # # # . 46 | # . # . # 47 | . . # . . 48 | . . # . . 49 | `) 50 | } 51 | if (grove.getGestureModel() == 4) { 52 | basic.showLeds(` 53 | . . # . . 54 | . . # . . 55 | # . # . # 56 | . # # # . 57 | . . # . . 58 | `) 59 | } 60 | basic.pause(100) 61 | }) 62 | ``` 63 | all the model 64 | ``` 65 | 66 | /** 67 | * Grove Gestures 68 | */ 69 | enum GroveGesture { 70 | //% block=None 71 | None = 0, 72 | //% block=Right 73 | Right = 1, 74 | //% block=Left 75 | Left = 2, 76 | //% block=Up 77 | Up = 3, 78 | //% block=Down 79 | Down = 4, 80 | //% block=Forward 81 | Forward = 5, 82 | //% block=Backward 83 | Backward = 6, 84 | //% block=Clockwise 85 | Clockwise = 7, 86 | //% block=Anticlockwise 87 | Anticlockwise = 8, 88 | //% block=Wave 89 | Wave = 9 90 | } 91 | ``` 92 | 93 | ### Grove - Ultrasonic Ranger 94 | 95 | Measure distance in centimeters, specify the signal pin. 96 | 97 | ```blocks 98 | let distance = grove.measureInCentimeters(DigitalPin.P0); 99 | ``` 100 | 101 | Measure distance in inches, specify the signal pin. 102 | 103 | ```blocks 104 | let distance = grove.measureInInches(DigitalPin.P0); 105 | ``` 106 | 107 | ### Grove - 4 digital display 108 | 109 | Create a 4 Digital Display driver, specify the clk and data pin, and set the brightness level, then start display value. 110 | 111 | ```blocks 112 | let display = grove.createDisplay(DigitalPin.P0, DigitalPin.P1); 113 | display.set(7); 114 | display.show(1234); 115 | ``` 116 | 117 | Use ``||bit||`` to display one bit number. 118 | 119 | Use ``||point||`` to open or close point dispay. 120 | 121 | Use ``||clear||`` to clean display. 122 | 123 | ### Grove - UART WiFi V2 124 | 125 | Connect to a WiFi and send data to ThinkSpeak or IFTTT, specify the UART tx and rx pin. 126 | 127 | ```blocks 128 | grove.setupWifi( 129 | SerialPin.P15, 130 | SerialPin.P1, 131 | BaudRate.BaudRate115200, 132 | "test-ssid", 133 | "test-passwd" 134 | ) 135 | 136 | basic.forever(() => { 137 | if (grove.wifiOK()) { 138 | basic.showIcon(IconNames.Yes) 139 | } else { 140 | basic.showIcon(IconNames.No) 141 | } 142 | grove.sendToThinkSpeak("write_api_key", 1, 2, 3, 4, 5, 6, 7, 8) 143 | grove.sendToIFTTT("ifttt_event", "ifttt_key", "hello", 'micro', 'bit') 144 | basic.pause(60000) 145 | }) 146 | ``` 147 | 148 | ### Grove - LCD 16x2 149 | 150 | Show string and number after initialize LCD. 151 | 152 | ```blocks 153 | grove.lcd_init() 154 | grove.lcd_show_string("Hello", 0, 0) 155 | grove.lcd_show_number(12345, 0, 1) 156 | ``` 157 | 158 | 159 | ### Grove Temperature & Humidity Sensor (DHT11) 160 | 161 | Read the temperature and humidity from the Grove Temperature & Humidity Sensor. 162 | 163 | ```blocks 164 | serial.redirectToUSB(); 165 | 166 | let dht11 = grove.connectToDHT11(DigitalPin.P1, false); 167 | 168 | basic.forever(function () { 169 | if (grove.readTemperatureHumidity(dht11)) { 170 | serial.writeLine("New data received:"); 171 | serial.writeValue("humidity", grove.getHumidity(dht11)); 172 | serial.writeValue("temperature", grove.getTemperatureCelsius(dht11)); 173 | } else { 174 | serial.writeLine("Fail to read, try again later"); 175 | } 176 | basic.pause(2000); 177 | }) 178 | ``` 179 | 180 | 181 | ### Grove - Vision AI Module V2 182 | 183 | Connect the Grove Vision AI Module V2 through I2C and get AI inference results. 184 | 185 | Before uploading the code, make sure you have deployed the model on the AI Module V2. [Click here to deploy.](https://sensecraft.seeed.cc/ai/model) 186 | 187 | We use People Detection model and Gesture Detection model as examples. 188 | 189 | This demo shows how to get the number of detected gesture of Rock, Paper, Scissors, and dispay the number on the LED matrix of Microbit. 190 | 191 | ```blocks 192 | let person_num = 0 193 | serial.redirectToUSB() 194 | grove.connectAndSetupGroveVisionAIV2( 195 | true 196 | ) 197 | while (!(grove.startAIInference())) { 198 | serial.writeLine("Fail to initialize") 199 | basic.pause(5000) 200 | } 201 | basic.forever(function () { 202 | if (grove.fetchAIInferenceResults()) { 203 | if (grove.containsObjectName(["Rock", "Paper", "Scissors"])) { 204 | person_num = grove.countObjectByName(["Rock", "Paper", "Scissors"]) 205 | basic.showNumber(person_num) 206 | basic.pause(2000) 207 | } else { 208 | basic.showIcon(IconNames.No) 209 | } 210 | } 211 | basic.pause(1000) 212 | }) 213 | ``` 214 | 215 | This demo will teach you how to use callback functions to print the recognized information in the serial port. 216 | 217 | ```blocks 218 | grove.onReceiveDetectionResult(function (detectionResults) { 219 | for (let detectionResult of detectionResults) { 220 | serial.writeLine(detectionResult.toString()) 221 | } 222 | }) 223 | 224 | let persons = 0 225 | 226 | serial.redirectToUSB() 227 | grove.connectAndSetupGroveVisionAIV2(true) 228 | 229 | while (!(grove.startAIInference())) { 230 | serial.writeLine("Failed to start inference") 231 | basic.pause(1000) 232 | } 233 | 234 | basic.forever(function () { 235 | if (grove.fetchAIInferenceResults()) { 236 | serial.writeLine("Fetch inference result success") 237 | if (grove.containsObjectName(["person"])) { 238 | persons = grove.countObjectByName(["person"]) 239 | serial.writeString("Detected persons: ") 240 | serial.writeNumber(persons) 241 | serial.writeLine("" + ("\n")) 242 | } 243 | } 244 | basic.pause(100) 245 | }) 246 | ``` 247 | 248 | 249 | ## License 250 | 251 | MIT 252 | 253 | ## Supported targets 254 | 255 | * for PXT/calliopemini 256 | -------------------------------------------------------------------------------- /_locales/de/Grove-strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "{id:category}Grove": "Grove", 3 | "{id:group}4-Digit": "4 Ziffern Display", 4 | "{id:group}Ultrasonic": "Ultraschall", 5 | "{id:group}Gesture": "Gestenerkennung", 6 | "{id:group}Thumbjoystick": "Grove Joystick", 7 | "GroveGesture.None|block": "keine", 8 | "GroveGesture.Right|block": "rechts", 9 | "GroveGesture.Left|block": "links", 10 | "GroveGesture.Up|block": "auf", 11 | "GroveGesture.Down|block": "nieder", 12 | "GroveGesture.Forward|block": "vorwärts", 13 | "GroveGesture.Backward|block": "rückwärts", 14 | "GroveGesture.Clockwise|block": "Uhrzeigersinn", 15 | "GroveGesture.Anticlockwise|block": "Gegenuhrzeigersinn", 16 | "GroveGesture.Wave|block": "Welle", 17 | "grove.initGesture|block": "initialisiere Grove Gestenerkennung", 18 | "grove.getGestureModel|block": "lies Geste", 19 | "grove.onGesture|block": "wenn Grovegeste |%gesture|", 20 | "grove.measureInCentimeters|block": "Ultraschallsensor (in cm) an |%pin", 21 | "grove.measureInInches|block": "Ultraschallsensor (in inch) an |%pin", 22 | "grove.createDisplay|block": "4-Ziffern Display an|%clkPin|und|%dataPin", 23 | "grove.PAJ7620.init|block": "%GestureModul|initialisiere den Gestensensor", 24 | "grove.PAJ7620.read|block": "%GestureModul|erkenne Geste", 25 | "grove.TM1637.show|block": "%4Digit|zeige Zahl|%dispData|", 26 | "grove.TM1637.set|block": "%4Digit|setze Helligkeit auf|%level|", 27 | "grove.TM1637.bit|block": "%4Digit|zeige Ziffer|%dispData|an Stelle|%bitAddr|", 28 | "grove.TM1637.point|block": "%4Digit|schalte Kommapunkt|%point|", 29 | "grove.TM1637.clear|block": "%4Digit|lösche 4-Ziffern Display", 30 | "GroveJoystickKey.None|block": "Keine", 31 | "GroveJoystickKey.Right|block": "Rechts", 32 | "GroveJoystickKey.Left|block": "Links", 33 | "GroveJoystickKey.Up|block": "Hoch", 34 | "GroveJoystickKey.Down|block": "Runter", 35 | "GroveJoystickKey.UL|block": "Oben links", 36 | "GroveJoystickKey.UR|block": "Oben rechts", 37 | "GroveJoystickKey.LL|block": "Unten links", 38 | "GroveJoystickKey.LR|block": "Unten rechts", 39 | "GroveJoystickKey.Press|block": "Gedrückt", 40 | "grove.onJoystick|block": "wenn Grove - Joystickrichtung|%key an|%xpin|und|%ypin", 41 | "grove.getJoystick|block": "lies Joystickrichtung an|%xpin|und|%ypin", 42 | "grove.aht20ReadTemperatureC|block": "Temperatur (°C)", 43 | "grove.aht20ReadTemperatureF|block": "Temperatur (°F)", 44 | "grove.aht20ReadHumidity|block": "Feuchtigkeit", 45 | "grove.sendToThinkSpeak|block": "Sende Daten an deinen ThinkSpeak Kanal|Write API Key %apiKey|Feld 1 %field1|Feld 2 %field2|Feld 3 %field3|Feld 4 %field4|Feld 5 %field5|Feld 6 %field6|Feld 7 %field7|Feld 8 %field8", 46 | "grove.sendToIFTTT|block": "Sende Daten an dein IFTTT Event|Event %event|Key %key|Wert 1 %value1|Wert 2 %value2|Wert 3 %value3" 47 | } 48 | -------------------------------------------------------------------------------- /_locales/ja/Grove-strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "{id:category}Grove": "Grove", 3 | "GroveGesture.None|block": "なし", 4 | "GroveGesture.Right|block": "右に", 5 | "GroveGesture.Left|block": "左に", 6 | "GroveGesture.Up|block": "上に", 7 | "GroveGesture.Down|block": "下に", 8 | "GroveGesture.Forward|block": "前進", 9 | "GroveGesture.Backward|block": "後退", 10 | "GroveGesture.Clockwise|block": "時計回りに", 11 | "GroveGesture.Anticlockwise|block": "反時計回りに", 12 | "GroveGesture.Wave|block": "手振り", 13 | "grove.onGesture|block": "[Grove - ジェスチャーセンサー]|%gesture|ジェスチャーしたとき", 14 | "grove.measureInCentimeters|block": "[Grove - 超音波距離センサー]|距離(cm)を読み取る 端子|%pin", 15 | "grove.measureInInches|block": "[Grove - 超音波距離センサー]|距離(inch)を読み取る 端子|%pin", 16 | "grove.createDisplay|block": "[Grove - 4桁ディスプレイ]|端子|%clkPin|と端子|%dataPin", 17 | "grove.PAJ7620.init|block": "%strip|[Grove - ジェスチャーセンサー]|初期化する", 18 | "grove.PAJ7620.read|block": "%strip|[Grove - ジェスチャーセンサー]|ジェスチャーを読み取る", 19 | "grove.TM1637.show|block": "%strip|[Grove - 4桁ディスプレイ]|%dispData|を表示する", 20 | "grove.TM1637.set|block": "%strip|[Grove - 4桁ディスプレイ]|明るさを|%level|に変更する", 21 | "grove.TM1637.bit|block": "%strip|[Grove - 4桁ディスプレイ]|%dispData|を|%bitAddr|桁目に表示する", 22 | "grove.TM1637.point|block": "%strip|[Grove - 4桁ディスプレイ]|コロンの表示を|%point|に変更する", 23 | "grove.TM1637.clear|block": "%strip|[Grove - 4桁ディスプレイ]|表示を消す", 24 | "grove.aht20ReadTemperatureC|block": "[Grove - 温湿度センサー]|温度(℃)を読み取る", 25 | "grove.aht20ReadTemperatureF|block": "[Grove - 温湿度センサー]|温度(℉)を読み取る", 26 | "grove.aht20ReadHumidity|block": "[Grove - 温湿度センサー]|湿度を読み取る" 27 | } 28 | -------------------------------------------------------------------------------- /blocks/GroveAHT20.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Grove - AHT20 Custom Block 3 | */ 4 | //% groups=['AHT20'] 5 | namespace grove 6 | { 7 | function Read(aht20: grove.sensors.AHT20): { Humidity: number, Temperature: number } 8 | { 9 | if (!aht20.GetState().Calibrated) 10 | { 11 | aht20.Initialization(); 12 | if (!aht20.GetState().Calibrated) return null; 13 | } 14 | 15 | aht20.TriggerMeasurement(); 16 | for (let i = 0; ; ++i) 17 | { 18 | if (!aht20.GetState().Busy) break; 19 | if (i >= 500) return null; 20 | basic.pause(10); 21 | } 22 | 23 | return aht20.Read(); 24 | } 25 | 26 | /** 27 | * Read the temperature(°C) from Grove-AHT20(SKU#101990644) 28 | */ 29 | //% group="AHT20" 30 | //% block="[Grove - Temp&Humi Sensor]|Read the temperature(°C))" 31 | //% weight=3 32 | export function aht20ReadTemperatureC(): number 33 | { 34 | const aht20 = new grove.sensors.AHT20(); 35 | const val = Read(aht20); 36 | if (val == null) return null; 37 | 38 | return val.Temperature; 39 | } 40 | 41 | /** 42 | * Read the temperature(°F) from Grove-AHT20(SKU#101990644) 43 | */ 44 | //% group="AHT20" 45 | //% block="[Grove - Temp&Humi Sensor]|Read the temperature(°F))" 46 | //% weight=2 47 | export function aht20ReadTemperatureF(): number 48 | { 49 | const aht20 = new grove.sensors.AHT20(); 50 | const val = Read(aht20); 51 | if (val == null) return null; 52 | 53 | return val.Temperature * 9 / 5 + 32; 54 | } 55 | 56 | /** 57 | * Read the humidity from Grove-AHT20(SKU#101990644) 58 | */ 59 | //% group="AHT20" 60 | //% block="[Grove - Temp&Humi Sensor]|Read the humidity" 61 | //% weight=1 62 | export function aht20ReadHumidity(): number 63 | { 64 | const aht20 = new grove.sensors.AHT20(); 65 | const val = Read(aht20); 66 | if (val == null) return null; 67 | 68 | return val.Humidity; 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /blocks/GroveDHT11.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Grove Temperature & Humidity Sensor (DHT11) support 3 | */ 4 | //% groups='["DHT11"]' 5 | namespace grove { 6 | 7 | /** 8 | * Connect and setup the Grove Temperature & Humidity Sensor (DHT11) 9 | */ 10 | //% block="connect to sensor on %signalPin, serial logging %serialLogging" 11 | //% signalPin.defl=DigitalPin.P1 12 | //% blockSetVariable=dht11 13 | //% group="DHT11" 14 | //% weight=99 15 | //% color="#AA278D" 16 | export function connectToDHT11(signalPin: DigitalPin = DigitalPin.P1, serialLogging: boolean = false): grove.sensors.DHT11Helper { 17 | return new grove.sensors.DHT11Helper(signalPin, serialLogging); 18 | } 19 | 20 | /** 21 | * Read the temperature and humidity from the sensor 22 | */ 23 | //% block="read temperature and humidity from $sensor, force read %forceRead" 24 | //% sensor.defl=dht11 25 | //% sensor.shadow=variables_get 26 | //% group="DHT11" 27 | //% weight=98 28 | //% color="#AA278D" 29 | export function readTemperatureHumidity(sensor: grove.sensors.DHT11Helper, forceRead: boolean = false): boolean { 30 | if (sensor) { 31 | return sensor.readSensorData(forceRead); 32 | } 33 | return false; 34 | } 35 | 36 | 37 | /** 38 | * Get the humidity in percentage 39 | */ 40 | //% block="get humidity from $sensor" 41 | //% sensor.defl=dht11 42 | //% sensor.shadow=variables_get 43 | //% group="DHT11" 44 | //% weight=89 45 | export function getHumidity(sensor: grove.sensors.DHT11Helper, autoRead: boolean = true): number { 46 | if (sensor) { 47 | if (autoRead) { 48 | sensor.readSensorData(); 49 | } 50 | return sensor.humidity; 51 | } 52 | return NaN; 53 | } 54 | 55 | /** 56 | * Get the temperature in Celsius 57 | */ 58 | //% block="get temperature in celsius from $sensor" 59 | //% sensor.defl=dht11 60 | //% sensor.shadow=variables_get 61 | //% group="DHT11" 62 | //% weight=88 63 | export function getTemperatureCelsius(sensor: grove.sensors.DHT11Helper, autoRead: boolean = true): number { 64 | if (sensor) { 65 | if (autoRead) { 66 | sensor.readSensorData(); 67 | } 68 | return sensor.temperature; 69 | } 70 | return NaN; 71 | } 72 | 73 | /** 74 | * Get the temperature in Fahrenheit 75 | */ 76 | //% block="get temperature in fahrenheit from $sensor" 77 | //% sensor.defl=dht11 78 | //% sensor.shadow=variables_get 79 | //% group="DHT11" 80 | //% weight=87 81 | export function getTemperatureFahrenheit(sensor: grove.sensors.DHT11Helper, autoRead: boolean = true): number { 82 | if (sensor) { 83 | if (autoRead) { 84 | sensor.readSensorData(); 85 | } 86 | const celsius = sensor.temperature; 87 | return (celsius * 1.8) + 32; 88 | } 89 | return NaN; 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /blocks/GroveLCD1602v1.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom blocks 3 | */ 4 | //% groups=['LCD1602v1'] 5 | namespace grove 6 | { 7 | let _lcdI2cAddr = 0x3e; 8 | let _displayfunction = 0; 9 | let _displaycontrol = 0; 10 | let _displaymode = 0; 11 | 12 | function i2c_send_byte(buf: Buffer) { 13 | pins.i2cWriteBuffer(_lcdI2cAddr, buf, false); 14 | } 15 | 16 | function lcd_send_cmd(cmd: number) { 17 | let buf: Buffer = pins.createBuffer(2); 18 | buf[0] = 0x80; 19 | buf[1] = cmd; 20 | i2c_send_byte(buf); 21 | } 22 | 23 | function lcd_send_data(data: number) { 24 | let buf: Buffer = pins.createBuffer(2); 25 | buf[0] = 0x40; 26 | buf[1] = data; 27 | i2c_send_byte(buf); 28 | } 29 | 30 | function lcd_set_cursor(col: number, row: number) { 31 | let buf: Buffer = pins.createBuffer(2); 32 | col = (row == 0 ? col | 0x80 : col | 0xc0); 33 | buf[0] = 0x80; 34 | buf[1] = col; 35 | i2c_send_byte(buf); 36 | } 37 | 38 | /** 39 | * initial LCD 40 | */ 41 | //% group="LCD1602v1" 42 | //% block="[Grove - LCD 16x2]|LCD initialize" 43 | //% weight=3 44 | export function lcd_init() { 45 | _displayfunction |= 0x08; 46 | lcd_send_cmd(0x20 | _displayfunction); // set command sequence 47 | 48 | _displaycontrol = 0x04 | 0x00 | 0x00; 49 | lcd_send_cmd(0x08 | _displaycontrol); // set display control 50 | 51 | _displaymode = 0x02 | 0x00; 52 | lcd_send_cmd(0x04|_displaymode); // set display mode 53 | 54 | lcd_clear(); // 55 | } 56 | 57 | /** 58 | * show a number in LCD at given position 59 | * @param n is number will be show, eg: 10, 100, 200 60 | * @param x is LCD column position, eg: 0 61 | * @param y is LCD row position, eg: 0 62 | */ 63 | //% group="LCD1602v1" 64 | //% block="[Grove - LCD 16x2]|show number %n|at x %x|y %y" 65 | //% weight=3 66 | //% x.min=0 x.max=15 67 | //% y.min=0 y.max=1 68 | export function lcd_show_number(n: number, x: number, y: number): void { 69 | let s = n.toString() 70 | lcd_show_string(s, x, y) 71 | } 72 | 73 | /** 74 | * show a string in LCD at given position 75 | * @param s is string will be show, eg: "Hello" 76 | * @param x is LCD column position, [0 - 15], eg: 0 77 | * @param y is LCD row position, [0 - 1], eg: 0 78 | */ 79 | //% group="LCD1602v1" 80 | //% block="[Grove - LCD 16x2]|show string %n|at x %x|y %y" 81 | //% weight=3 82 | //% x.min=0 x.max=15 83 | //% y.min=0 y.max=1 84 | export function lcd_show_string(s: string, x: number, y: number): void { 85 | lcd_set_cursor(x,y); 86 | for(let i = 0; i < s.length; i++) { 87 | lcd_send_data(s.charCodeAt(i)) 88 | } 89 | } 90 | 91 | /** 92 | * turn on LCD 93 | */ 94 | //% group="LCD1602v1" 95 | //% block="[Grove - LCD 16x2]|display turn on" 96 | //% weight=3 97 | export function lcd_dispaly_on(): void { 98 | _displaycontrol |= 0x04; 99 | lcd_send_cmd(0x08 | _displaycontrol); 100 | } 101 | 102 | /** 103 | * turn off LCD 104 | */ 105 | //% group="LCD1602v1" 106 | //% block="[Grove - LCD 16x2]|dispaly turn off" 107 | //% weight=3 108 | export function lcd_dispaly_off(): void { 109 | _displaycontrol &= ~0x04; 110 | lcd_send_cmd(0x08 | _displaycontrol); 111 | } 112 | 113 | /** 114 | * clear all display content 115 | */ 116 | //% group="LCD1602v1" 117 | //% block="[Grove - LCD 16x2]|display clear" 118 | //% weight=3 119 | export function lcd_clear(): void { 120 | lcd_send_cmd(0x01); 121 | basic.pause(2); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /blocks/GroveVisionAIV2.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * SenseCraft AI support for Grove Vision AI Module V2 4 | */ 5 | //% groups='["Grove Vision AI V2"]' 6 | namespace grove { 7 | 8 | namespace vision_ai_v2 { 9 | 10 | export let transport: grove.plugins.sscma.Transport; 11 | export let atClient: grove.plugins.sscma.ATClient; 12 | 13 | export let detectionResults: DetectionResult[] = []; 14 | export let classificationResults: ClassificationResult[] = []; 15 | export let errorCode: OperationCode = OperationCode.Unknown; 16 | 17 | } 18 | 19 | 20 | /** 21 | * Connect and setup the Grove Vision AI Module V2 through I2C transport 22 | */ 23 | //% block="connect and setup device, serial logging %serialLogging" 24 | //% group="Grove Vision AI V2" 25 | //% weight=100 26 | //% color="#AA278D" 27 | export function connectAndSetupGroveVisionAIV2( 28 | serialLogging: boolean = false, 29 | i2cAddress: number = 0x62, 30 | i2cClock: number = 400000, 31 | force: boolean = false 32 | ) { 33 | if (force || !vision_ai_v2.transport) { 34 | vision_ai_v2.transport = grove.plugins.sscma.Transport.connect( 35 | i2cAddress, 36 | i2cClock 37 | ); 38 | } 39 | 40 | if (force || !vision_ai_v2.atClient) { 41 | vision_ai_v2.atClient = new grove.plugins.sscma.ATClient( 42 | vision_ai_v2.transport, 43 | serialLogging 44 | ); 45 | } else if (vision_ai_v2.atClient && vision_ai_v2.atClient.getDeviceStatus() === DeviceStatus.Disconnected) { 46 | vision_ai_v2.atClient.getDeviceStatus(); 47 | } 48 | } 49 | 50 | /** 51 | * Get the device status of the Grove Vision AI Module V2 52 | */ 53 | //% block="get device status" 54 | //% group="Grove Vision AI V2" 55 | //% weight=99 56 | export function getGroveVisionAIV2DeviceStatus(fromCache: boolean = true): DeviceStatus { 57 | if (vision_ai_v2.atClient) { 58 | return vision_ai_v2.atClient.getDeviceStatus(fromCache); 59 | } 60 | return DeviceStatus.Disconnected; 61 | } 62 | 63 | /** 64 | * Get the device name of the Grove Vision AI Module V2 65 | */ 66 | //% block="get device name" 67 | //% blockSetVariable=deviceName 68 | //% group="Grove Vision AI V2" 69 | //% weight=98 70 | export function getGroveVisionAIV2DeviceName(fromCache: boolean = true): string { 71 | if (vision_ai_v2.atClient) { 72 | return vision_ai_v2.atClient.getDeviceName(fromCache); 73 | } 74 | return ""; 75 | } 76 | 77 | /** 78 | * Get the device ID of the Grove Vision AI Module V2 79 | */ 80 | //% block="get device id" 81 | //% blockSetVariable=deviceId 82 | //% group="Grove Vision AI V2" 83 | //% weight=97 84 | export function getGroveVisionAIV2DeviceId(fromCache: boolean = true): string { 85 | if (vision_ai_v2.atClient) { 86 | return vision_ai_v2.atClient.getDeviceId(fromCache); 87 | } 88 | return ""; 89 | } 90 | 91 | /** 92 | * Get the AI model info from the Grove Vision AI Module V2 93 | */ 94 | //% block="get ai model info" 95 | //% blockSetVariable=modelInfo 96 | //% group="Grove Vision AI V2" 97 | //% weight=89 98 | export function getAIModelInfo(fromCache: boolean = true): ModelInfo { 99 | if (vision_ai_v2.atClient) { 100 | return vision_ai_v2.atClient.getModelInfo(fromCache); 101 | } 102 | return new ModelInfo("", "", []); 103 | } 104 | 105 | /** 106 | * Start AI inference on the Grove Vision AI Module V2 107 | */ 108 | //% block="start ai inference" 109 | //% group="Grove Vision AI V2" 110 | //% weight=79 111 | //% color="#AA278D" 112 | export function startAIInference(timeout: number = 1000, force: boolean = true): boolean { 113 | if (vision_ai_v2.atClient) { 114 | if (force && vision_ai_v2.atClient.isInference(timeout)) { 115 | vision_ai_v2.atClient.stopInference(timeout); 116 | } 117 | return vision_ai_v2.atClient.startInference(timeout); 118 | } 119 | return false; 120 | } 121 | 122 | let _onDetectionResultsHandler: (detectionResults: DetectionResult[]) => void = null; 123 | let _onClassificationResultsHandler: (classificationResults: ClassificationResult[]) => void = null; 124 | let _onErrorHandler: (errorCode: OperationCode) => void = null; 125 | 126 | function _onReceiveDetectionResults(detectionResults: DetectionResult[]) { 127 | vision_ai_v2.detectionResults = detectionResults; 128 | if (_onDetectionResultsHandler) { 129 | _onDetectionResultsHandler(vision_ai_v2.detectionResults); 130 | } 131 | } 132 | 133 | function _onReceiveClassificationResults(classificationResults: ClassificationResult[]) { 134 | vision_ai_v2.classificationResults = classificationResults; 135 | if (_onClassificationResultsHandler) { 136 | _onClassificationResultsHandler(vision_ai_v2.classificationResults); 137 | } 138 | } 139 | 140 | function _onReceiveError(errorCode: OperationCode) { 141 | vision_ai_v2.errorCode = errorCode; 142 | if (_onErrorHandler) { 143 | _onErrorHandler(vision_ai_v2.errorCode); 144 | } 145 | } 146 | 147 | /** 148 | * Fetch AI inference results from the Grove Vision AI Module V2 149 | */ 150 | //% block="fetch ai inference results" 151 | //% group="Grove Vision AI V2" 152 | //% weight=78 153 | //% color="#AA278D" 154 | export function fetchAIInferenceResults( 155 | maxResults: number = 15, 156 | timeout: number = 1000, 157 | ): boolean { 158 | vision_ai_v2.detectionResults = [] 159 | vision_ai_v2.classificationResults = [] 160 | 161 | if (vision_ai_v2.atClient) { 162 | return vision_ai_v2.atClient.fetchInferenceResult( 163 | _onReceiveDetectionResults, 164 | _onReceiveClassificationResults, 165 | _onReceiveError, 166 | maxResults, 167 | timeout 168 | ); 169 | } 170 | return false; 171 | } 172 | 173 | /** 174 | * Stop AI inference on the Grove Vision AI Module V2 175 | */ 176 | //% block="stop ai inference" 177 | //% group="Grove Vision AI V2" 178 | //% weight=77 179 | export function stopAIInference(timeout: number = 1000) { 180 | if (vision_ai_v2.atClient) { 181 | return vision_ai_v2.atClient.stopInference(); 182 | } 183 | } 184 | 185 | /** 186 | * Get total number of specific object id(s) 187 | */ 188 | //% block 189 | //% group="Grove Vision AI V2" 190 | //% weight=69 191 | export function countObjectById(ids: number[]): number { 192 | let count = 0; 193 | 194 | for (let detectionResult of vision_ai_v2.detectionResults) { 195 | for (let id of ids) { 196 | if (detectionResult.id == id) { 197 | ++count; 198 | } 199 | } 200 | } 201 | 202 | for (let classificationResult of vision_ai_v2.classificationResults) { 203 | for (let id of ids) { 204 | if (classificationResult.id == id) { 205 | ++count; 206 | } 207 | } 208 | } 209 | 210 | return count; 211 | } 212 | 213 | /** 214 | * Get total number of specific object name(s) 215 | */ 216 | //% block 217 | //% group="Grove Vision AI V2" 218 | //% weight=68 219 | export function countObjectByName(labels: string[]): number { 220 | let count = 0; 221 | 222 | for (let detectionResult of vision_ai_v2.detectionResults) { 223 | for (let label of labels) { 224 | if (detectionResult.label == label) { 225 | ++count; 226 | } 227 | } 228 | } 229 | 230 | for (let classificationResult of vision_ai_v2.classificationResults) { 231 | for (let label of labels) { 232 | if (classificationResult.label == label) { 233 | ++count; 234 | } 235 | } 236 | } 237 | 238 | return count; 239 | } 240 | 241 | /** 242 | * Check if contains specific object id(s) 243 | */ 244 | //% block 245 | //% group="Grove Vision AI V2" 246 | //% weight=67 247 | export function containsObjectId(ids: number[]): boolean { 248 | return countObjectById(ids) > 0; 249 | } 250 | 251 | /** 252 | * Check if contains specific object name(s) 253 | */ 254 | //% block 255 | //% group="Grove Vision AI V2" 256 | //% weight=66 257 | export function containsObjectName(labels: string[]): boolean { 258 | return countObjectByName(labels) > 0; 259 | } 260 | 261 | /** 262 | * Event on receive detection results from the Grove Vision AI Module V2 263 | */ 264 | //% block="on receive detection results" 265 | //% group="Grove Vision AI V2" 266 | //% weight=59 267 | //% color="#AA278D" 268 | export function onReceiveDetectionResult(handler: (detectionResults: DetectionResult[]) => void) { 269 | _onDetectionResultsHandler = handler; 270 | } 271 | 272 | /** 273 | * Event on receive classification results from the Grove Vision AI Module V2 274 | */ 275 | //% block="on receive classification results" 276 | //% group="Grove Vision AI V2" 277 | //% weight=58 278 | //% color="#AA278D" 279 | export function onReceiveClassificationResult(handler: (classificationResults: ClassificationResult[]) => void) { 280 | _onClassificationResultsHandler = handler; 281 | } 282 | 283 | /** 284 | * Event on receive error from the Grove Vision AI Module V2 285 | */ 286 | //% block="on receive error" 287 | //% group="Grove Vision AI V2" 288 | //% weight=57 289 | //% color="#AA278D" 290 | export function onReceiveError(handler: (errorCode: OperationCode) => void) { 291 | _onErrorHandler = handler; 292 | } 293 | 294 | /** 295 | * Get fetched detection results from the Grove Vision AI Module V2 296 | */ 297 | //% block="get fetched detection results" 298 | //% blockSetVariable=detectionResults 299 | //% group="Grove Vision AI V2" 300 | //% weight=56 301 | export function getFetchedDetectionResults(): DetectionResult[] { 302 | return vision_ai_v2.detectionResults; 303 | } 304 | 305 | /** 306 | * Get fetched classification results from the Grove Vision AI Module V2 307 | */ 308 | //% block="get fetched classification results" 309 | //% blockSetVariable=classificationResults 310 | //% group="Grove Vision AI V2" 311 | //% weight=55 312 | export function getFetchedClassificationResults(): ClassificationResult[] { 313 | return vision_ai_v2.classificationResults; 314 | } 315 | 316 | /** 317 | * Get the AI inference error code from the Grove Vision AI Module V2 318 | */ 319 | //% block="get inference error code" 320 | //% blockSetVariable=errorCode 321 | //% group="Grove Vision AI V2" 322 | //% weight=54 323 | export function getAIInferenceErrorCode(fromCache: boolean = true): OperationCode { 324 | return vision_ai_v2.errorCode; 325 | } 326 | 327 | }; 328 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Seeed-Studio/pxt-grove/c7358005e7ff02ff542b133daf002a27c7f8487d/icon.png -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | const initRegisterArray: number[] = [ 2 | 0xEF, 0x00, 0x32, 0x29, 0x33, 0x01, 0x34, 0x00, 0x35, 0x01, 0x36, 0x00, 0x37, 0x07, 0x38, 0x17, 3 | 0x39, 0x06, 0x3A, 0x12, 0x3F, 0x00, 0x40, 0x02, 0x41, 0xFF, 0x42, 0x01, 0x46, 0x2D, 0x47, 0x0F, 4 | 0x48, 0x3C, 0x49, 0x00, 0x4A, 0x1E, 0x4B, 0x00, 0x4C, 0x20, 0x4D, 0x00, 0x4E, 0x1A, 0x4F, 0x14, 5 | 0x50, 0x00, 0x51, 0x10, 0x52, 0x00, 0x5C, 0x02, 0x5D, 0x00, 0x5E, 0x10, 0x5F, 0x3F, 0x60, 0x27, 6 | 0x61, 0x28, 0x62, 0x00, 0x63, 0x03, 0x64, 0xF7, 0x65, 0x03, 0x66, 0xD9, 0x67, 0x03, 0x68, 0x01, 7 | 0x69, 0xC8, 0x6A, 0x40, 0x6D, 0x04, 0x6E, 0x00, 0x6F, 0x00, 0x70, 0x80, 0x71, 0x00, 0x72, 0x00, 8 | 0x73, 0x00, 0x74, 0xF0, 0x75, 0x00, 0x80, 0x42, 0x81, 0x44, 0x82, 0x04, 0x83, 0x20, 0x84, 0x20, 9 | 0x85, 0x00, 0x86, 0x10, 0x87, 0x00, 0x88, 0x05, 0x89, 0x18, 0x8A, 0x10, 0x8B, 0x01, 0x8C, 0x37, 10 | 0x8D, 0x00, 0x8E, 0xF0, 0x8F, 0x81, 0x90, 0x06, 0x91, 0x06, 0x92, 0x1E, 0x93, 0x0D, 0x94, 0x0A, 11 | 0x95, 0x0A, 0x96, 0x0C, 0x97, 0x05, 0x98, 0x0A, 0x99, 0x41, 0x9A, 0x14, 0x9B, 0x0A, 0x9C, 0x3F, 12 | 0x9D, 0x33, 0x9E, 0xAE, 0x9F, 0xF9, 0xA0, 0x48, 0xA1, 0x13, 0xA2, 0x10, 0xA3, 0x08, 0xA4, 0x30, 13 | 0xA5, 0x19, 0xA6, 0x10, 0xA7, 0x08, 0xA8, 0x24, 0xA9, 0x04, 0xAA, 0x1E, 0xAB, 0x1E, 0xCC, 0x19, 14 | 0xCD, 0x0B, 0xCE, 0x13, 0xCF, 0x64, 0xD0, 0x21, 0xD1, 0x0F, 0xD2, 0x88, 0xE0, 0x01, 0xE1, 0x04, 15 | 0xE2, 0x41, 0xE3, 0xD6, 0xE4, 0x00, 0xE5, 0x0C, 0xE6, 0x0A, 0xE7, 0x00, 0xE8, 0x00, 0xE9, 0x00, 16 | 0xEE, 0x07, 0xEF, 0x01, 0x00, 0x1E, 0x01, 0x1E, 0x02, 0x0F, 0x03, 0x10, 0x04, 0x02, 0x05, 0x00, 17 | 0x06, 0xB0, 0x07, 0x04, 0x08, 0x0D, 0x09, 0x0E, 0x0A, 0x9C, 0x0B, 0x04, 0x0C, 0x05, 0x0D, 0x0F, 18 | 0x0E, 0x02, 0x0F, 0x12, 0x10, 0x02, 0x11, 0x02, 0x12, 0x00, 0x13, 0x01, 0x14, 0x05, 0x15, 0x07, 19 | 0x16, 0x05, 0x17, 0x07, 0x18, 0x01, 0x19, 0x04, 0x1A, 0x05, 0x1B, 0x0C, 0x1C, 0x2A, 0x1D, 0x01, 20 | 0x1E, 0x00, 0x21, 0x00, 0x22, 0x00, 0x23, 0x00, 0x25, 0x01, 0x26, 0x00, 0x27, 0x39, 0x28, 0x7F, 21 | 0x29, 0x08, 0x30, 0x03, 0x31, 0x00, 0x32, 0x1A, 0x33, 0x1A, 0x34, 0x07, 0x35, 0x07, 0x36, 0x01, 22 | 0x37, 0xFF, 0x38, 0x36, 0x39, 0x07, 0x3A, 0x00, 0x3E, 0xFF, 0x3F, 0x00, 0x40, 0x77, 0x41, 0x40, 23 | 0x42, 0x00, 0x43, 0x30, 0x44, 0xA0, 0x45, 0x5C, 0x46, 0x00, 0x47, 0x00, 0x48, 0x58, 0x4A, 0x1E, 24 | 0x4B, 0x1E, 0x4C, 0x00, 0x4D, 0x00, 0x4E, 0xA0, 0x4F, 0x80, 0x50, 0x00, 0x51, 0x00, 0x52, 0x00, 25 | 0x53, 0x00, 0x54, 0x00, 0x57, 0x80, 0x59, 0x10, 0x5A, 0x08, 0x5B, 0x94, 0x5C, 0xE8, 0x5D, 0x08, 26 | 0x5E, 0x3D, 0x5F, 0x99, 0x60, 0x45, 0x61, 0x40, 0x63, 0x2D, 0x64, 0x02, 0x65, 0x96, 0x66, 0x00, 27 | 0x67, 0x97, 0x68, 0x01, 0x69, 0xCD, 0x6A, 0x01, 0x6B, 0xB0, 0x6C, 0x04, 0x6D, 0x2C, 0x6E, 0x01, 28 | 0x6F, 0x32, 0x71, 0x00, 0x72, 0x01, 0x73, 0x35, 0x74, 0x00, 0x75, 0x33, 0x76, 0x31, 0x77, 0x01, 29 | 0x7C, 0x84, 0x7D, 0x03, 0x7E, 0x01 30 | ]; 31 | 32 | let TubeTab: number [] = [ 33 | 0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 34 | 0x7f, 0x6f, 0x77, 0x7c, 0x39, 0x5e, 0x79, 0x71 35 | ]; 36 | 37 | /** 38 | * Grove Gestures 39 | */ 40 | enum GroveGesture { 41 | //% block=None 42 | None = 0, 43 | //% block=Right 44 | Right = 1, 45 | //% block=Left 46 | Left = 2, 47 | //% block=Up 48 | Up = 3, 49 | //% block=Down 50 | Down = 4, 51 | //% block=Forward 52 | Forward = 5, 53 | //% block=Backward 54 | Backward = 6, 55 | //% block=Clockwise 56 | Clockwise = 7, 57 | //% block=Anticlockwise 58 | Anticlockwise = 8, 59 | //% block=Wave 60 | Wave = 9 61 | } 62 | 63 | enum GroveJoystickKey { 64 | //% block="None" 65 | None = 0, 66 | //% block="Right" 67 | Right = 1, 68 | //% block="Left" 69 | Left = 2, 70 | //% block="Up" 71 | Up = 3, 72 | //% block="Down" 73 | Down = 4, 74 | //% block="Upper left" 75 | UL = 5, 76 | //% block="Upper right" 77 | UR = 6, 78 | //% block="Lower left" 79 | LL = 7, 80 | //% block="Lower right" 81 | LR = 8, 82 | //% block="press" 83 | Press = 9 84 | } 85 | 86 | 87 | /** 88 | * Functions to operate Grove module. 89 | */ 90 | //% weight=10 color=#9F79EE icon="\uf1b3" block="Grove" 91 | //% groups='["4-Digit","Ultrasonic","Gesture","Thumbjoystick","UartWiFi"]' 92 | namespace grove { 93 | /** 94 | * 95 | */ 96 | export class PAJ7620 { 97 | private paj7620WriteReg(addr: number, cmd: number) { 98 | let buf: Buffer = pins.createBuffer(2); 99 | 100 | buf[0] = addr; 101 | buf[1] = cmd; 102 | 103 | pins.i2cWriteBuffer(0x73, buf, false); 104 | } 105 | 106 | private paj7620ReadReg(addr: number): number { 107 | let buf: Buffer = pins.createBuffer(1); 108 | 109 | buf[0] = addr; 110 | 111 | pins.i2cWriteBuffer(0x73, buf, false); 112 | 113 | buf = pins.i2cReadBuffer(0x73, 1, false); 114 | 115 | return buf[0]; 116 | } 117 | 118 | private paj7620SelectBank(bank: number) { 119 | if (bank == 0) this.paj7620WriteReg(0xEF, 0); 120 | else if (bank == 1) this.paj7620WriteReg(0xEF, 1); 121 | } 122 | 123 | private paj7620Init() { 124 | let temp = 0; 125 | 126 | this.paj7620SelectBank(0); 127 | 128 | temp = this.paj7620ReadReg(0); 129 | if (temp == 0x20) { 130 | for (let i = 0; i < 438; i += 2) { 131 | this.paj7620WriteReg(initRegisterArray[i], initRegisterArray[i + 1]); 132 | } 133 | } 134 | 135 | this.paj7620SelectBank(0); 136 | } 137 | 138 | /** 139 | * Create a new driver of Grove - Gesture 140 | */ 141 | init() { 142 | this.paj7620Init(); 143 | basic.pause(200); 144 | } 145 | 146 | /** 147 | * Detect and recognize the gestures from Grove - Gesture 148 | */ 149 | 150 | read(): number { 151 | let data = 0, result = 0; 152 | 153 | data = this.paj7620ReadReg(0x43); 154 | switch (data) { 155 | case 0x01: 156 | result = GroveGesture.Right; 157 | break; 158 | 159 | case 0x02: 160 | result = GroveGesture.Left; 161 | break; 162 | 163 | case 0x04: 164 | result = GroveGesture.Up; 165 | break; 166 | 167 | case 0x08: 168 | result = GroveGesture.Down; 169 | break; 170 | 171 | case 0x10: 172 | result = GroveGesture.Forward; 173 | break; 174 | 175 | case 0x20: 176 | result = GroveGesture.Backward; 177 | break; 178 | 179 | case 0x40: 180 | result = GroveGesture.Clockwise; 181 | break; 182 | 183 | case 0x80: 184 | result = GroveGesture.Anticlockwise; 185 | break; 186 | 187 | default: 188 | data = this.paj7620ReadReg(0x44); 189 | if (data == 0x01) 190 | result = GroveGesture.Wave; 191 | break; 192 | } 193 | 194 | return result; 195 | } 196 | } 197 | 198 | /** 199 | * 200 | */ 201 | export class TM1637 202 | { 203 | clkPin: DigitalPin; 204 | dataPin: DigitalPin; 205 | brightnessLevel: number; 206 | pointFlag: boolean; 207 | buf: Buffer; 208 | 209 | private writeByte(wrData: number) 210 | { 211 | for(let i = 0; i < 8; i ++) 212 | { 213 | pins.digitalWritePin(this.clkPin, 0); 214 | if(wrData & 0x01)pins.digitalWritePin(this.dataPin, 1); 215 | else pins.digitalWritePin(this.dataPin, 0); 216 | wrData >>= 1; 217 | pins.digitalWritePin(this.clkPin, 1); 218 | } 219 | 220 | pins.digitalWritePin(this.clkPin, 0); // Wait for ACK 221 | pins.digitalWritePin(this.dataPin, 1); 222 | pins.digitalWritePin(this.clkPin, 1); 223 | } 224 | 225 | private start() 226 | { 227 | pins.digitalWritePin(this.clkPin, 1); 228 | pins.digitalWritePin(this.dataPin, 1); 229 | pins.digitalWritePin(this.dataPin, 0); 230 | pins.digitalWritePin(this.clkPin, 0); 231 | } 232 | 233 | private stop() 234 | { 235 | pins.digitalWritePin(this.clkPin, 0); 236 | pins.digitalWritePin(this.dataPin, 0); 237 | pins.digitalWritePin(this.clkPin, 1); 238 | pins.digitalWritePin(this.dataPin, 1); 239 | } 240 | 241 | private coding(dispData: number): number 242 | { 243 | let pointData = 0; 244 | 245 | if(this.pointFlag == true)pointData = 0x80; 246 | else if(this.pointFlag == false)pointData = 0; 247 | 248 | if(dispData == 0x7f)dispData = 0x00 + pointData; 249 | else dispData = TubeTab[dispData] + pointData; 250 | 251 | return dispData; 252 | } 253 | 254 | /** 255 | * Show a 4 digits number on display 256 | * @param dispData value of number 257 | */ 258 | 259 | //% blockId=grove_tm1637_display_number block="%4Digit|show number|%dispData" 260 | //% group="4-Digit" 261 | show(dispData: number) 262 | { 263 | let compare_01:number = dispData % 100; 264 | let compare_001:number = dispData % 1000; 265 | 266 | if(dispData < 10) 267 | { 268 | this.bit(dispData, 3); 269 | this.bit(0x7f, 2); 270 | this.bit(0x7f, 1); 271 | this.bit(0x7f, 0); 272 | } 273 | else if(dispData < 100) 274 | { 275 | this.bit(dispData % 10, 3); 276 | if(dispData > 90){ 277 | this.bit(9, 2); 278 | } else{ 279 | this.bit(Math.floor(dispData / 10) % 10, 2); 280 | } 281 | 282 | this.bit(0x7f, 1); 283 | this.bit(0x7f, 0); 284 | } 285 | else if(dispData < 1000) 286 | { 287 | this.bit(dispData % 10, 3); 288 | if(compare_01 > 90){ 289 | this.bit(9, 2); 290 | } else{ 291 | this.bit(Math.floor(dispData / 10) % 10, 2); 292 | } 293 | if(compare_001 > 900){ 294 | this.bit(9, 1); 295 | } else{ 296 | this.bit(Math.floor(dispData / 100) % 10, 1); 297 | } 298 | this.bit(0x7f, 0); 299 | } 300 | else if(dispData < 10000) 301 | { 302 | this.bit(dispData % 10, 3); 303 | if(compare_01 > 90){ 304 | this.bit(9, 2); 305 | } else{ 306 | this.bit(Math.floor(dispData / 10) % 10, 2); 307 | } 308 | if(compare_001 > 900){ 309 | this.bit(9, 1); 310 | } else{ 311 | this.bit(Math.floor(dispData / 100) % 10, 1); 312 | } 313 | if(dispData > 9000){ 314 | this.bit(9, 0); 315 | } else{ 316 | this.bit(Math.floor(dispData / 1000) % 10, 0); 317 | } 318 | } 319 | else 320 | { 321 | this.bit(9, 3); 322 | this.bit(9, 2); 323 | this.bit(9, 1); 324 | this.bit(9, 0); 325 | } 326 | } 327 | 328 | /** 329 | * Set the brightness level of display at from 0 to 7 330 | * @param level value of brightness light level 331 | */ 332 | //% blockId=grove_tm1637_set_display_level block="%4Digit|brightness level to|%level" 333 | //% level.min=0 level.max=7 334 | //% group="4-Digit" 335 | set(level: number) 336 | { 337 | this.brightnessLevel = level; 338 | 339 | this.bit(this.buf[0], 0x00); 340 | this.bit(this.buf[1], 0x01); 341 | this.bit(this.buf[2], 0x02); 342 | this.bit(this.buf[3], 0x03); 343 | } 344 | 345 | /** 346 | * Show a single number from 0 to 9 at a specified digit of Grove - 4-Digit Display 347 | * @param dispData value of number 348 | * @param bitAddr value of bit number 349 | */ 350 | //% blockId=grove_tm1637_display_bit block="%4Digit|show single number|%dispData|at digit|%bitAddr" 351 | //% dispData.min=0 dispData.max=9 352 | //% bitAddr.min=0 bitAddr.max=3 353 | //% group="4-Digit" 354 | bit(dispData: number, bitAddr: number) 355 | { 356 | if((dispData == 0x7f) || ((dispData <= 9) && (bitAddr <= 3))) 357 | { 358 | let segData = 0; 359 | 360 | segData = this.coding(dispData); 361 | this.start(); 362 | this.writeByte(0x44); 363 | this.stop(); 364 | this.start(); 365 | this.writeByte(bitAddr | 0xc0); 366 | this.writeByte(segData); 367 | this.stop(); 368 | this.start(); 369 | this.writeByte(0x88 + this.brightnessLevel); 370 | this.stop(); 371 | 372 | this.buf[bitAddr] = dispData; 373 | } 374 | } 375 | 376 | /** 377 | * Turn on or off the colon point on Grove - 4-Digit Display 378 | * @param pointEn value of point switch 379 | */ 380 | //% blockId=grove_tm1637_display_point block="%4Digit|turn|%point|colon point" 381 | //% group="4-Digit" 382 | point(point: boolean) 383 | { 384 | this.pointFlag = point; 385 | 386 | this.bit(this.buf[0], 0x00); 387 | this.bit(this.buf[1], 0x01); 388 | this.bit(this.buf[2], 0x02); 389 | this.bit(this.buf[3], 0x03); 390 | } 391 | 392 | /** 393 | * Clear the display 394 | */ 395 | //% blockId=grove_tm1637_display_clear block="%4Digit|clear" 396 | //% group="4-Digit" 397 | clear() 398 | { 399 | this.bit(0x7f, 0x00); 400 | this.bit(0x7f, 0x01); 401 | this.bit(0x7f, 0x02); 402 | this.bit(0x7f, 0x03); 403 | } 404 | } 405 | 406 | 407 | export class GroveJoystick 408 | { 409 | /** 410 | * Detect position from Grove - Thumb Joystick 411 | * @param xPin 412 | * @param yPin 413 | */ 414 | 415 | joyread(xPin: AnalogPin, yPin: AnalogPin): number { 416 | 417 | let xdata = 0, ydata = 0, result = 0; 418 | if (xPin && yPin) { 419 | xdata = pins.analogReadPin(xPin); 420 | ydata = pins.analogReadPin(yPin); 421 | if (xdata > 1000) { 422 | result = GroveJoystickKey.Press; 423 | } 424 | else if (xdata > 600) { 425 | if (ydata > 600) result = GroveJoystickKey.UR; 426 | else if (ydata < 400) result = GroveJoystickKey.LR; 427 | else result = GroveJoystickKey.Right; 428 | } 429 | else if (xdata < 400) { 430 | if (ydata > 600) result = GroveJoystickKey.UL; 431 | else if (ydata < 400) result = GroveJoystickKey.LL; 432 | else result = GroveJoystickKey.Left; 433 | } 434 | else { 435 | if (ydata > 600) result = GroveJoystickKey.Up; 436 | else if (ydata < 400) result = GroveJoystickKey.Down; 437 | else result = GroveJoystickKey.None; 438 | } 439 | } 440 | else { 441 | result = GroveJoystickKey.None; 442 | } 443 | return result; 444 | } 445 | } 446 | 447 | const gestureEventId = 3100; 448 | const joystickEventID = 3101; 449 | let lastGesture = GroveGesture.None; 450 | let lastJoystick = GroveJoystickKey.None; 451 | let distanceBackup: number = 0; 452 | let joystick = new GroveJoystick(); 453 | let paj7620 = new PAJ7620(); 454 | // adapted to Calliope mini V2 Core by M.Klein 17.09.2020 455 | 456 | /** 457 | * Create a new driver of Grove - Ultrasonic Sensor to measure distances in cm 458 | * @param pin signal pin of ultrasonic ranger module 459 | */ 460 | //% blockId=grove_ultrasonic_centimeters_v2 block="(V2)Ultrasonic Sensor (in cm) at|%pin" 461 | //% pin.fieldEditor="gridpicker" pin.fieldOptions.columns=4 462 | //% pin.fieldOptions.tooltips="false" pin.fieldOptions.width="250" 463 | //% group="Ultrasonic" pin.defl=DigitalPin.P0 464 | 465 | export function measureInCentimetersV2(pin: DigitalPin): number 466 | { 467 | let duration = 0; 468 | let RangeInCentimeters = 0; 469 | 470 | pins.digitalWritePin(pin, 0); 471 | control.waitMicros(2); 472 | pins.digitalWritePin(pin, 1); 473 | control.waitMicros(20); 474 | pins.digitalWritePin(pin, 0); 475 | duration = pins.pulseIn(pin, PulseValue.High, 50000); // Max duration 50 ms 476 | 477 | RangeInCentimeters = duration * 153 / 44 / 2 / 100 ; 478 | 479 | if(RangeInCentimeters > 0) distanceBackup = RangeInCentimeters; 480 | else RangeInCentimeters = distanceBackup; 481 | 482 | basic.pause(50); 483 | 484 | return RangeInCentimeters; 485 | } 486 | 487 | /** 488 | * Create a new driver Grove - Ultrasonic Sensor to measure distances in inch 489 | * @param pin signal pin of ultrasonic ranger module 490 | */ 491 | //% blockId=grove_ultrasonic_inches_v2 block="(V2)Ultrasonic Sensor (in inch) at|%pin" 492 | //% pin.fieldEditor="gridpicker" pin.fieldOptions.columns=4 493 | //% pin.fieldOptions.tooltips="false" pin.fieldOptions.width="250" 494 | //% group="Ultrasonic" pin.defl=DigitalPin.P0 495 | export function measureInInchesV2(pin: DigitalPin): number 496 | { 497 | let duration = 0; 498 | let RangeInInches = 0; 499 | 500 | pins.digitalWritePin(pin, 0); 501 | control.waitMicros(2); 502 | pins.digitalWritePin(pin, 1); 503 | control.waitMicros(20); 504 | pins.digitalWritePin(pin, 0); 505 | duration = pins.pulseIn(pin, PulseValue.High, 100000); // Max duration 100 ms 506 | 507 | RangeInInches = duration * 153 / 113 / 2 / 100; 508 | 509 | if(RangeInInches > 0) distanceBackup = RangeInInches; 510 | else RangeInInches = distanceBackup; 511 | 512 | basic.pause(50); 513 | 514 | return RangeInInches; 515 | } 516 | 517 | /** 518 | * Create a new driver of Grove - Ultrasonic Sensor to measure distances in cm 519 | * @param pin signal pin of ultrasonic ranger module 520 | */ 521 | //% blockId=grove_ultrasonic_centimeters block="Ultrasonic Sensor (in cm) at|%pin" 522 | //% pin.fieldEditor="gridpicker" pin.fieldOptions.columns=4 523 | //% pin.fieldOptions.tooltips="false" pin.fieldOptions.width="250" 524 | //% group="Ultrasonic" pin.defl=DigitalPin.P0 525 | 526 | export function measureInCentimeters(pin: DigitalPin): number 527 | { 528 | let duration = 0; 529 | let RangeInCentimeters = 0; 530 | 531 | pins.digitalWritePin(pin, 0); 532 | control.waitMicros(2); 533 | pins.digitalWritePin(pin, 1); 534 | control.waitMicros(20); 535 | pins.digitalWritePin(pin, 0); 536 | duration = pins.pulseIn(pin, PulseValue.High, 50000); // Max duration 50 ms 537 | 538 | RangeInCentimeters = duration * 153 / 29 / 2 / 100; 539 | 540 | if(RangeInCentimeters > 0) distanceBackup = RangeInCentimeters; 541 | else RangeInCentimeters = distanceBackup; 542 | 543 | basic.pause(50); 544 | 545 | return RangeInCentimeters; 546 | } 547 | 548 | /** 549 | * Create a new driver Grove - Ultrasonic Sensor to measure distances in inch 550 | * @param pin signal pin of ultrasonic ranger module 551 | */ 552 | //% blockId=grove_ultrasonic_inches block="Ultrasonic Sensor (in inch) at|%pin" 553 | //% pin.fieldEditor="gridpicker" pin.fieldOptions.columns=4 554 | //% pin.fieldOptions.tooltips="false" pin.fieldOptions.width="250" 555 | //% group="Ultrasonic" pin.defl=DigitalPin.P0 556 | export function measureInInches(pin: DigitalPin): number 557 | { 558 | let duration = 0; 559 | let RangeInInches = 0; 560 | 561 | pins.digitalWritePin(pin, 0); 562 | control.waitMicros(2); 563 | pins.digitalWritePin(pin, 1); 564 | control.waitMicros(20); 565 | pins.digitalWritePin(pin, 0); 566 | duration = pins.pulseIn(pin, PulseValue.High, 100000); // Max duration 100 ms 567 | 568 | RangeInInches = duration * 153 / 74 / 2 / 100; 569 | 570 | if(RangeInInches > 0) distanceBackup = RangeInInches; 571 | else RangeInInches = distanceBackup; 572 | 573 | basic.pause(50); 574 | 575 | return RangeInInches; 576 | } 577 | 578 | /** 579 | * Create a new driver Grove - 4-Digit Display 580 | * @param clkPin value of clk pin number 581 | * @param dataPin value of data pin number 582 | */ 583 | //% blockId=grove_tm1637_create block="4-Digit Display at|%clkPin|and|%dataPin" 584 | //% clkPin.fieldEditor="gridpicker" clkPin.fieldOptions.columns=4 585 | //% group="4-Digit" 586 | //% clkPin.fieldOptions.tooltips="false" clkPin.fieldOptions.width="250" 587 | //% dataPin.fieldEditor="gridpicker" dataPin.fieldOptions.columns=4 588 | //% clkPin.defl=DigitalPin.P0 dataPin.defl=DigitalPin.P1 589 | //% dataPin.fieldOptions.tooltips="false" dataPin.fieldOptions.width="250" 590 | //% blockSetVariable=4digit 591 | export function createDisplay(clkPin: DigitalPin, dataPin: DigitalPin): TM1637 592 | { 593 | let display = new TM1637(); 594 | 595 | display.buf = pins.createBuffer(4); 596 | display.clkPin = clkPin; 597 | display.dataPin = dataPin; 598 | display.brightnessLevel = 0; 599 | display.pointFlag = false; 600 | display.clear(); 601 | 602 | return display; 603 | } 604 | 605 | /** 606 | * init Grove Gesture modules 607 | * 608 | */ 609 | //% blockId=grove_initgesture block="init gesture" 610 | //% group="Gesture" 611 | export function initGesture() { 612 | if (!paj7620) { 613 | paj7620.init(); 614 | } 615 | } 616 | 617 | /** 618 | * get Grove Gesture model 619 | * 620 | */ 621 | //% blockId=grove_getgesture block="get gesture model" 622 | //% group="Gesture" 623 | export function getGestureModel(): number { 624 | return paj7620.read(); 625 | } 626 | /** 627 | * get Joystick key 628 | * 629 | */ 630 | //% blockId=grove_getjoystick block="get joystick key at|%xpin|and|%ypin" 631 | //% group="Thumbjoystick" xpin.defl=AnalogPin.P0 ypin.defl=AnalogPin.P1 632 | export function getJoystick(xpin: AnalogPin, ypin: AnalogPin): number { 633 | return joystick.joyread(xpin, ypin); 634 | } 635 | 636 | /** 637 | * Converts the gesture name to a number 638 | * Useful for comparisons 639 | */ 640 | //% blockId=ggesture block="%key" 641 | //% group="Gesture" 642 | export function ggesture(g: GroveGesture): number { 643 | return g; 644 | } 645 | 646 | /** 647 | * Do something when a gesture is detected by Grove - Gesture 648 | * @param gesture type of gesture to detect 649 | * @param handler code to run 650 | */ 651 | //% blockId=grove_gesture_create_event block="on Gesture|%gesture" 652 | //% group="Gesture" 653 | export function onGesture(gesture: GroveGesture, handler: () => void) { 654 | control.onEvent(gestureEventId, gesture, handler); 655 | paj7620.init(); 656 | control.inBackground(() => { 657 | while(true) { 658 | const gesture = paj7620.read(); 659 | if (gesture != lastGesture) { 660 | lastGesture = gesture; 661 | control.raiseEvent(gestureEventId, lastGesture); 662 | } 663 | basic.pause(50); 664 | } 665 | }) 666 | } 667 | 668 | /** 669 | * Converts the key name to a number 670 | * Useful for comparisons 671 | */ 672 | //% blockId=joystickkey block="%key" 673 | //% group="Thumbjoystick" 674 | export function joystickkey(key: GroveJoystickKey): number { 675 | return key; 676 | } 677 | 678 | /** 679 | * Do something when a key is detected by Grove - Thumb Joystick 680 | * @param key type of joystick to detect 681 | * @param xpin 682 | * @param ypin 683 | * @param handler code to run 684 | */ 685 | //% blockId=grove_joystick_create_event block="on Key|%key at |%xpin|and|%ypin" 686 | //% group="Thumbjoystick" xpin.defl=AnalogPin.P0 ypin.defl=AnalogPin.P1 687 | 688 | export function onJoystick(key: GroveJoystickKey, xpin: AnalogPin, ypin: AnalogPin, handler: () => void) { 689 | control.onEvent(joystickEventID, key, handler); 690 | control.inBackground(() => { 691 | while(true) { 692 | const key = joystick.joyread(xpin, ypin); 693 | if (key != lastJoystick) { 694 | lastJoystick = key; 695 | control.raiseEvent(joystickEventID, lastJoystick); 696 | } 697 | basic.pause(50); 698 | } 699 | }) 700 | 701 | } 702 | 703 | let isWifiConnected = false; 704 | /** 705 | * Setup Grove - Uart WiFi V2 to connect to Wi-Fi 706 | */ 707 | //% block="Setup Wifi|TX %txPin|RX %rxPin|Baud rate %baudrate|SSID = %ssid|Password = %passwd" 708 | //% group="UartWiFi" 709 | //% txPin.defl=SerialPin.P15 710 | //% rxPin.defl=SerialPin.P1 711 | //% baudRate.defl=BaudRate.BaudRate115200 712 | export function setupWifi(txPin: SerialPin, rxPin: SerialPin, baudRate: BaudRate, ssid: string, passwd: string) { 713 | let result = 0 714 | 715 | isWifiConnected = false 716 | 717 | serial.redirect( 718 | txPin, 719 | rxPin, 720 | baudRate 721 | ) 722 | 723 | sendAtCmd("AT") 724 | result = waitAtResponse("OK", "ERROR", "None", 1000) 725 | 726 | sendAtCmd("AT+CWMODE=1") 727 | result = waitAtResponse("OK", "ERROR", "None", 1000) 728 | 729 | sendAtCmd(`AT+CWJAP="${ssid}","${passwd}"`) 730 | result = waitAtResponse("WIFI GOT IP", "ERROR", "None", 20000) 731 | 732 | if (result == 1) { 733 | isWifiConnected = true 734 | } 735 | } 736 | 737 | /** 738 | * Check if Grove - Uart WiFi V2 is connected to Wifi 739 | */ 740 | //% block="Wifi OK?" 741 | //% group="UartWiFi" 742 | export function wifiOK() { 743 | return isWifiConnected 744 | } 745 | 746 | /** 747 | * Send data to ThinkSpeak 748 | */ 749 | //% block="Send Data to your ThinkSpeak Channel|Write API Key %apiKey|Field1 %field1|Field2 %field2|Field3 %field3|Field4 %field4|Field5 %field5|Field6 %field6|Field7 %field7|Field8 %field8" 750 | //% group="UartWiFi" 751 | //% apiKey.defl="your Write API Key" 752 | export function sendToThinkSpeak(apiKey: string, field1: number, field2: number, field3: number, field4: number, field5: number, field6: number, field7: number, field8: number) { 753 | let result = 0 754 | let retry = 2 755 | 756 | // close the previous TCP connection 757 | if (isWifiConnected) { 758 | sendAtCmd("AT+CIPCLOSE") 759 | waitAtResponse("OK", "ERROR", "None", 2000) 760 | } 761 | 762 | while (isWifiConnected && retry > 0) { 763 | retry = retry - 1; 764 | // establish TCP connection 765 | sendAtCmd("AT+CIPSTART=\"TCP\",\"api.thingspeak.com\",80") 766 | result = waitAtResponse("OK", "ALREADY CONNECTED", "ERROR", 2000) 767 | if (result == 3) continue 768 | 769 | let data = "GET /update?api_key=" + apiKey 770 | if (!isNaN(field1)) data = data + "&field1=" + field1 771 | if (!isNaN(field2)) data = data + "&field2=" + field2 772 | if (!isNaN(field3)) data = data + "&field3=" + field3 773 | if (!isNaN(field4)) data = data + "&field4=" + field4 774 | if (!isNaN(field5)) data = data + "&field5=" + field5 775 | if (!isNaN(field6)) data = data + "&field6=" + field6 776 | if (!isNaN(field7)) data = data + "&field7=" + field7 777 | if (!isNaN(field8)) data = data + "&field8=" + field8 778 | 779 | sendAtCmd("AT+CIPSEND=" + (data.length + 2)) 780 | result = waitAtResponse(">", "OK", "ERROR", 2000) 781 | if (result == 3) continue 782 | sendAtCmd(data) 783 | result = waitAtResponse("SEND OK", "SEND FAIL", "ERROR", 5000) 784 | 785 | // // close the TCP connection 786 | // sendAtCmd("AT+CIPCLOSE") 787 | // waitAtResponse("OK", "ERROR", "None", 2000) 788 | 789 | if (result == 1) break 790 | } 791 | } 792 | 793 | /** 794 | * Send data to IFTTT 795 | */ 796 | //% block="Send Data to your IFTTT Event|Event %event|Key %key|value1 %value1|value2 %value2|value3 %value3" 797 | //% group="UartWiFi" 798 | //% event.defl="your Event" 799 | //% key.defl="your Key" 800 | //% value1.defl="hello" 801 | //% value2.defl="micro" 802 | //% value3.defl="bit" 803 | export function sendToIFTTT(event: string, key: string, value1: string, value2: string, value3: string) { 804 | let result = 0 805 | let retry = 2 806 | 807 | // close the previous TCP connection 808 | if (isWifiConnected) { 809 | sendAtCmd("AT+CIPCLOSE") 810 | waitAtResponse("OK", "ERROR", "None", 2000) 811 | } 812 | 813 | while (isWifiConnected && retry > 0) { 814 | retry = retry - 1; 815 | // establish TCP connection 816 | sendAtCmd("AT+CIPSTART=\"TCP\",\"maker.ifttt.com\",80") 817 | result = waitAtResponse("OK", "ALREADY CONNECTED", "ERROR", 2000) 818 | if (result == 3) continue 819 | 820 | let data = "GET /trigger/" + event + "/with/key/" + key 821 | data = data + "?value1=" + value1 822 | data = data + "&value2=" + value2 823 | data = data + "&value3=" + value3 824 | data = data + " HTTP/1.1" 825 | data = data + "\u000D\u000A" 826 | data = data + "User-Agent: curl/7.58.0" 827 | data = data + "\u000D\u000A" 828 | data = data + "Host: maker.ifttt.com" 829 | data = data + "\u000D\u000A" 830 | data = data + "Accept: */*" 831 | data = data + "\u000D\u000A" 832 | 833 | sendAtCmd("AT+CIPSEND=" + (data.length + 2)) 834 | result = waitAtResponse(">", "OK", "ERROR", 2000) 835 | if (result == 3) continue 836 | sendAtCmd(data) 837 | result = waitAtResponse("SEND OK", "SEND FAIL", "ERROR", 5000) 838 | 839 | // // close the TCP connection 840 | // sendAtCmd("AT+CIPCLOSE") 841 | // waitAtResponse("OK", "ERROR", "None", 2000) 842 | 843 | if (result == 1) break 844 | } 845 | } 846 | 847 | 848 | function waitAtResponse(target1: string, target2: string, target3: string, timeout: number) { 849 | let buffer = "" 850 | let start = input.runningTime() 851 | 852 | while ((input.runningTime() - start) < timeout) { 853 | buffer += serial.readString() 854 | 855 | if (buffer.includes(target1)) return 1 856 | if (buffer.includes(target2)) return 2 857 | if (buffer.includes(target3)) return 3 858 | 859 | basic.pause(100) 860 | } 861 | 862 | return 0 863 | } 864 | 865 | function sendAtCmd(cmd: string) { 866 | serial.writeString(cmd + "\u000D\u000A") 867 | } 868 | } 869 | 870 | -------------------------------------------------------------------------------- /plugins/SSCMA.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * SenseCraft AI AT backend support by @nullptr, 2025.5.16 3 | */ 4 | 5 | 6 | //% blockNamespace=grove 7 | //% group="Grove Vision AI V2" 8 | class DetectionResult { 9 | private _x: number; 10 | private _y: number; 11 | private _w: number; 12 | private _h: number; 13 | private _score: number; 14 | private _id: number; 15 | private _label: string; 16 | 17 | constructor(x: number, y: number, w: number, h: number, score: number, id: number, label: string) { 18 | this._x = x; 19 | this._y = y; 20 | this._w = w; 21 | this._h = h; 22 | this._score = score; 23 | this._id = id; 24 | this._label = label; 25 | } 26 | 27 | //% blockCombine 28 | get x(): number { return this._x; } 29 | 30 | //% blockCombine 31 | get y(): number { return this._y; } 32 | 33 | //% blockCombine 34 | get w(): number { return this._w; } 35 | 36 | //% blockCombine 37 | get h(): number { return this._h; } 38 | 39 | //% blockCombine 40 | get score(): number { return this._score; } 41 | 42 | //% blockCombine 43 | get id(): number { return this._id; } 44 | 45 | //% blockCombine 46 | get label(): string { return this._label; } 47 | 48 | /** 49 | * Convert a detection result object to string representation 50 | */ 51 | //% block="convert $this to string" 52 | //% this.defl=detectionResult 53 | //% this.shadow=variables_get 54 | public toString(): string { 55 | return "xywh=[" + this._x + "," + this._y + "," + this._w + "," + this._h + "] " + 56 | "score=" + this._score + " " + 57 | "id=" + this._id + " " + 58 | "label=" + this._label; 59 | } 60 | }; 61 | 62 | //% blockNamespace=grove 63 | //% group="Grove Vision AI V2" 64 | class ClassificationResult { 65 | private _score: number; 66 | private _id: number; 67 | private _label: string; 68 | 69 | constructor(score: number, id: number, label: string) { 70 | this._score = score; 71 | this._id = id; 72 | this._label = label; 73 | } 74 | 75 | //% blockCombine 76 | public get score(): number { return this._score; } 77 | 78 | //% blockCombine 79 | public get id(): number { return this._id; } 80 | 81 | //% blockCombine 82 | public get label(): string { return this._label; } 83 | 84 | /** 85 | * Convert a classification result object to string representation 86 | */ 87 | //% block="convert $this to string" 88 | //% this.defl=classificationResult 89 | //% this.shadow=variables_get 90 | public toString(): string { 91 | return "score=" + this._score + " " + 92 | "id=" + this._id + " " + 93 | "label=" + this._label; 94 | } 95 | }; 96 | 97 | //% blockNamespace=grove 98 | //% group="Grove Vision AI V2" 99 | class ModelInfo { 100 | private _name: string; 101 | private _version: string; 102 | private _labels: string[]; 103 | 104 | constructor(name: any, version: any, labels: any) { 105 | if (typeof name != "string") { 106 | name = ""; 107 | } 108 | this._name = name; 109 | 110 | if (typeof version != "string") { 111 | version = ""; 112 | } 113 | this._version = version; 114 | 115 | let safeLabels: any[] = []; 116 | if (labels && typeof labels == "object") { 117 | safeLabels = labels; 118 | for (let i = 0; i < safeLabels.length; ++i) { 119 | if (typeof safeLabels[i] != "string") { 120 | safeLabels[i] = ""; 121 | } 122 | } 123 | } 124 | this._labels = safeLabels as string[]; 125 | } 126 | 127 | //% blockCombine 128 | get name(): string { 129 | return this._name; 130 | } 131 | 132 | //% blockCombine 133 | get version(): string { 134 | return this._version; 135 | } 136 | 137 | /** 138 | * Get the label string by ID, returns empty string if ID is not exist 139 | */ 140 | //% block="get label by $id" 141 | //% id.defl=id 142 | //% id.min=0 143 | //% id.shadow=variables_get 144 | public getLabel(id: number): string { 145 | if (id >= 0 && id < this._labels.length) { 146 | return this._labels[id]; 147 | } 148 | return ""; 149 | } 150 | }; 151 | 152 | 153 | namespace grove { 154 | 155 | //% group="Grove Vision AI V2" 156 | export enum DeviceStatus { 157 | //% block="Disconnected" 158 | Disconnected = 0, 159 | //% block="Busy" 160 | Busy = 1, 161 | //% block="Idle" 162 | Idle = 2, 163 | //% block="Inferencing" 164 | Inferencing = 3, 165 | }; 166 | 167 | /** 168 | * Choose a device status 169 | */ 170 | //% block="device status %status" 171 | //% group="Grove Vision AI V2" 172 | export function deviceStatus(status: DeviceStatus): DeviceStatus { 173 | return status; 174 | } 175 | 176 | //% group="Grove Vision AI V2" 177 | export enum OperationCode { 178 | //% block="Success" 179 | Success = 0, 180 | //% block="Again" 181 | Again = 1, 182 | //% block="Logic Error" 183 | LogicError = 2, 184 | //% block="Timeout" 185 | Timeout = 3, 186 | //% block="IO Error" 187 | IOError = 4, 188 | //% block="Invalid Argument" 189 | InvalidArgument = 5, 190 | //% block="Busy" 191 | Busy = 6, 192 | //% block="Not Supported" 193 | NotSupported = 7, 194 | //% block="Not Permitted" 195 | NotPermitted = 8, 196 | //% block="Unknown" 197 | Unknown = 10 198 | }; 199 | 200 | /** 201 | * Choose an error code 202 | */ 203 | //% block="error code %code" 204 | //% group="Grove Vision AI V2" 205 | export function operationCode(code: OperationCode): OperationCode { 206 | return code; 207 | } 208 | 209 | 210 | export namespace plugins { 211 | 212 | export namespace sscma { 213 | 214 | 215 | export class Transport { 216 | private static startOfFrame = 0x10; 217 | 218 | private static cmdRead = 0x01; 219 | private static cmdWrite = 0x02; 220 | private static cmdAvail = 0x03; 221 | private static cmdStart = 0x04; 222 | private static cmdStop = 0x05; 223 | private static cmdReset = 0x06; 224 | 225 | private static i2cAddr: number; 226 | private static i2cClk: number; 227 | 228 | private static readPktSize: number = 250; 229 | private static writePktSize: number = 250; 230 | 231 | private static probBuffer: Buffer; 232 | private static readSofBuffer: Buffer; 233 | private static writeBuffer: Buffer; 234 | private static availSofBuffer: Buffer; 235 | 236 | private static instance: Transport; 237 | 238 | private constructor() { 239 | if (!Transport.probBuffer) { 240 | Transport.probBuffer = pins.createBuffer(0); 241 | } 242 | 243 | if (!Transport.readSofBuffer) { 244 | Transport.readSofBuffer = pins.createBuffer(6); 245 | } 246 | Transport.readSofBuffer.fill(0); 247 | Transport.readSofBuffer.setNumber(NumberFormat.UInt8LE, 0, Transport.startOfFrame); 248 | Transport.readSofBuffer.setNumber(NumberFormat.UInt8LE, 1, Transport.cmdRead); 249 | 250 | if (!Transport.writeBuffer) { 251 | Transport.writeBuffer = pins.createBuffer(Transport.writePktSize + 6); 252 | } 253 | Transport.writeBuffer.fill(0); 254 | Transport.writeBuffer.setNumber(NumberFormat.UInt8LE, 0, Transport.startOfFrame); 255 | Transport.writeBuffer.setNumber(NumberFormat.UInt8LE, 1, Transport.cmdWrite); 256 | 257 | if (!Transport.availSofBuffer) { 258 | Transport.availSofBuffer = pins.createBuffer(6); 259 | } 260 | Transport.availSofBuffer.fill(0); 261 | Transport.availSofBuffer.setNumber(NumberFormat.UInt8LE, 0, Transport.startOfFrame); 262 | Transport.availSofBuffer.setNumber(NumberFormat.UInt8LE, 1, Transport.cmdAvail); 263 | } 264 | 265 | public static connect( 266 | i2cAddr: number = 0x62, 267 | i2cClk: number = 400000 268 | ): Transport { 269 | if (i2cAddr != Transport.i2cAddr || i2cClk != Transport.i2cClk) { 270 | Transport.instance = null; 271 | } 272 | 273 | if (!Transport.instance) { 274 | Transport.instance = new Transport(); 275 | 276 | Transport.i2cAddr = i2cAddr; 277 | Transport.i2cClk = i2cClk; 278 | } 279 | 280 | if (!this.instance.isConnected()) { 281 | Transport.instance = null; 282 | } else { 283 | this.instance.cleanup(); 284 | } 285 | 286 | return Transport.instance; 287 | } 288 | 289 | public static disconnect(): void { 290 | if (Transport.instance) { 291 | Transport.instance = null; 292 | } 293 | } 294 | 295 | public read(buf: Buffer, size: number, syncDelay: number = 2): number { 296 | const packets = Math.floor(size / Transport.readPktSize); 297 | const remain = size % Transport.readPktSize; 298 | 299 | let sizeHigh = (Transport.readPktSize >> 8) & 0xFF; 300 | let sizeLow = Transport.readPktSize & 0xFF; 301 | 302 | Transport.readSofBuffer.setNumber(NumberFormat.UInt8LE, 2, sizeHigh); 303 | Transport.readSofBuffer.setNumber(NumberFormat.UInt8LE, 3, sizeLow); 304 | 305 | let offset = 0; 306 | let ret = 0; 307 | 308 | for (let i = 0; i < packets; ++i) { 309 | ret = pins.i2cWriteBuffer( 310 | Transport.i2cAddr, 311 | Transport.readSofBuffer, 312 | false 313 | ); 314 | if (ret == 0) { 315 | basic.pause(syncDelay); 316 | 317 | let buffer = pins.i2cReadBuffer( 318 | Transport.i2cAddr, 319 | Transport.readPktSize, 320 | false 321 | ); 322 | 323 | let end = offset + buffer.length; 324 | buf.write(offset, buffer); 325 | offset = end; 326 | } 327 | } 328 | 329 | if (remain == 0) { 330 | return offset; 331 | } 332 | 333 | sizeHigh = (remain >> 8) & 0xFF; 334 | sizeLow = remain & 0xFF; 335 | 336 | Transport.readSofBuffer.setNumber(NumberFormat.UInt8LE, 2, sizeHigh); 337 | Transport.readSofBuffer.setNumber(NumberFormat.UInt8LE, 3, sizeLow); 338 | 339 | ret = pins.i2cWriteBuffer( 340 | Transport.i2cAddr, 341 | Transport.readSofBuffer, 342 | false 343 | ); 344 | if (ret == 0) { 345 | basic.pause(syncDelay); 346 | 347 | let buffer = pins.i2cReadBuffer( 348 | Transport.i2cAddr, 349 | remain, 350 | false 351 | ); 352 | 353 | let end = offset + buffer.length; 354 | buf.write(offset, buffer); 355 | offset = end; 356 | } 357 | 358 | return offset; 359 | } 360 | 361 | public write(data: Buffer, size: number, syncDelay: number = 2): number { 362 | const packets = Math.floor(size / Transport.writePktSize); 363 | const remain = size % Transport.writePktSize; 364 | 365 | let sizeHigh = (Transport.writePktSize >> 8) & 0xFF; 366 | let sizeLow = Transport.writePktSize & 0xFF; 367 | 368 | Transport.writeBuffer.setNumber(NumberFormat.UInt8LE, 2, sizeHigh); 369 | Transport.writeBuffer.setNumber(NumberFormat.UInt8LE, 3, sizeLow); 370 | 371 | let offset = 0; 372 | let ret = 0; 373 | 374 | for (let i = 0; i < packets; ++i) { 375 | const end = offset + Transport.writePktSize; 376 | const chunk = data.slice(offset, end); 377 | 378 | Transport.writeBuffer.write(4, chunk); 379 | ret = pins.i2cWriteBuffer( 380 | Transport.i2cAddr, 381 | Transport.writeBuffer, 382 | false 383 | ); 384 | if (ret != 0) { 385 | return offset; 386 | } 387 | 388 | offset = end; 389 | 390 | basic.pause(syncDelay); 391 | } 392 | 393 | if (remain == 0) { 394 | return offset; 395 | } 396 | 397 | sizeHigh = (remain >> 8) & 0xFF; 398 | sizeLow = remain & 0xFF; 399 | 400 | Transport.writeBuffer.setNumber(NumberFormat.UInt8LE, 2, sizeHigh); 401 | Transport.writeBuffer.setNumber(NumberFormat.UInt8LE, 3, sizeLow); 402 | 403 | const end = offset + remain; 404 | const chunk = data.slice(offset, end); 405 | Transport.writeBuffer.write(4, chunk); 406 | Transport.writeBuffer.setNumber(NumberFormat.UInt8LE, 4 + end, 0x00); 407 | Transport.writeBuffer.setNumber(NumberFormat.UInt8LE, 5 + end, 0x00); 408 | 409 | const buffer = Transport.writeBuffer.slice(0, 6 + end); 410 | ret = pins.i2cWriteBuffer( 411 | Transport.i2cAddr, 412 | buffer, 413 | false 414 | ); 415 | if (ret != 0) { 416 | return offset; 417 | } 418 | 419 | offset = end; 420 | 421 | basic.pause(syncDelay); 422 | 423 | return offset; 424 | } 425 | 426 | public avail(syncDelay: number = 2): number { 427 | let ret = pins.i2cWriteBuffer( 428 | Transport.i2cAddr, 429 | Transport.availSofBuffer, 430 | false 431 | ); 432 | if (ret != 0) { 433 | return 0; 434 | } 435 | 436 | basic.pause(syncDelay); 437 | 438 | let buffer = pins.i2cReadBuffer( 439 | Transport.i2cAddr, 440 | 2, 441 | false 442 | ); 443 | if (buffer.length != 2) { 444 | return 0; 445 | } 446 | 447 | let sizeHigh = buffer.getNumber(NumberFormat.UInt8LE, 0); 448 | let sizeLow = buffer.getNumber(NumberFormat.UInt8LE, 1); 449 | 450 | return (sizeHigh << 8) | sizeLow; 451 | } 452 | 453 | public cleanup( 454 | skipIfAvail: number = 0, 455 | repeatIfAvail: number = 65535, 456 | stepSize: number = 4096, 457 | ): void { 458 | if (skipIfAvail < 0) { 459 | skipIfAvail = 0; 460 | } 461 | if (stepSize <= 0) { 462 | return; 463 | } 464 | 465 | let buffer = pins.createBuffer(stepSize); 466 | let avail = this.avail(); 467 | while (avail > skipIfAvail) { 468 | while (avail > 0) { 469 | const len = this.read(buffer, Math.min(avail, stepSize)); 470 | if (len <= 0) { 471 | break; 472 | } 473 | avail -= len; 474 | } 475 | avail = this.avail(); 476 | } 477 | } 478 | 479 | public isConnected(): boolean { 480 | let status = pins.i2cWriteBuffer( 481 | Transport.i2cAddr, 482 | Transport.probBuffer, 483 | false 484 | ); 485 | 486 | return status == 0; 487 | } 488 | }; 489 | 490 | 491 | enum ResponseType { 492 | Direct = 0, 493 | Event = 1, 494 | System = 2, 495 | }; 496 | 497 | 498 | function decodeBase64(encoded: string): string { 499 | if (encoded && encoded.length <= 0 && encoded.length % 4 != 0) { 500 | return ""; 501 | } 502 | 503 | const base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; 504 | const base64Map: { [key: string]: number } = {}; 505 | for (let i = 0; i < base64Chars.length; ++i) { 506 | base64Map[base64Chars[i]] = i; 507 | } 508 | 509 | let decoded = ""; 510 | for (let i = 0; i < encoded.length;) { 511 | const b1 = base64Map[encoded[i++]]; 512 | const b2 = base64Map[encoded[i++]]; 513 | const b3 = base64Map[encoded[i++]]; 514 | const b4 = base64Map[encoded[i++]]; 515 | 516 | if (b1 == undefined || b2 == undefined || b3 == undefined || b4 == undefined) { 517 | return ""; 518 | } 519 | 520 | const d1 = (b1 << 2) | (b2 >> 4); 521 | const d2 = ((b2 & 0x0F) << 4) | (b3 >> 2); 522 | const d3 = ((b3 & 0x03) << 6) | b4; 523 | decoded += String.fromCharCode(d1); 524 | if (b3 ^ 64) { 525 | decoded += String.fromCharCode(d2); 526 | } 527 | if (b4 ^ 64) { 528 | decoded += String.fromCharCode(d3); 529 | } 530 | } 531 | 532 | return decoded; 533 | }; 534 | 535 | 536 | export class ATClient { 537 | 538 | private transport: Transport; 539 | private deviceStatus: DeviceStatus = DeviceStatus.Disconnected; 540 | 541 | private commandPrefix: string = "AT+"; 542 | private commandSuffix: string = "\r\n"; 543 | 544 | private responseSof: string = "\r"; 545 | private responseEof: string = "\n"; 546 | 547 | private cachedDeviceId: string = ""; 548 | private cachedDeviceName: string = ""; 549 | private cachedModelInfo: ModelInfo = new ModelInfo("", "", []); 550 | 551 | private responseQueueLimit: number; 552 | private responseQueue: Array = []; 553 | 554 | private loggingToSerial: boolean; 555 | 556 | private maxRetry: number; 557 | private retryTimeout: number; 558 | private retryDelay: number; 559 | 560 | constructor( 561 | transport: Transport, 562 | loggingToSerial: boolean = false, 563 | responseQueueLimit: number = 10, 564 | maxRetry: number = 3, 565 | retryTimeout: number = 1000, 566 | retryDelay: number = 100, 567 | 568 | ) { 569 | this.transport = transport; 570 | if (!this.transport) { 571 | return this; 572 | } 573 | 574 | this.loggingToSerial = loggingToSerial; 575 | 576 | this.responseQueueLimit = responseQueueLimit; 577 | if (this.responseQueueLimit <= 0) { 578 | this.LOG("Invalid response queue limit: " + responseQueueLimit + ", using default value of 10."); 579 | this.responseQueueLimit = 10; 580 | } 581 | 582 | this.deviceStatus = DeviceStatus.Busy; 583 | this.transport.cleanup(); 584 | 585 | if (maxRetry <= 0) { 586 | this.LOG("Invalid max retry value: " + maxRetry + ", using default value of 3."); 587 | maxRetry = 3; 588 | } 589 | this.maxRetry = maxRetry; 590 | if (retryTimeout <= 0) { 591 | this.LOG("Invalid retry timeout value: " + retryTimeout + ", using default value of 1000 ms."); 592 | retryTimeout = 1000; 593 | } 594 | this.retryTimeout = retryTimeout; 595 | if (retryDelay <= 0) { 596 | this.LOG("Invalid retry delay value: " + retryDelay + ", using default value of 100 ms."); 597 | retryDelay = 100; 598 | } 599 | this.retryDelay = retryDelay; 600 | 601 | this.updateDeviceStatus(); 602 | if (this.deviceStatus < DeviceStatus.Idle) { 603 | this.LOG("Failed to initialize device, please check the connection."); 604 | } 605 | } 606 | 607 | private LOG(msg: string): void { 608 | if (this.loggingToSerial) { 609 | serial.writeLine(msg); 610 | } 611 | } 612 | 613 | private waitResponse( 614 | responseType: ResponseType, 615 | responseName: string, 616 | discardStaled: boolean, 617 | fetchDelay: number, 618 | timeout: number = 1000 619 | ): any { 620 | if (timeout <= 0) { 621 | this.LOG("Invalid timeout value: " + timeout + ", using default value of 1000 ms."); 622 | timeout = 1000; 623 | } 624 | 625 | let startTime = input.runningTime(); 626 | let endTime = startTime + timeout; 627 | 628 | while (input.runningTime() < endTime) { 629 | let responseIdxs: number[] = []; 630 | for (let i = 0; i < this.responseQueue.length; ++i) { 631 | let response = this.responseQueue[i]; 632 | if (response["type"] == responseType && response["name"] == responseName) { 633 | if (!discardStaled) { 634 | this.responseQueue.splice(i, 1); 635 | return response; 636 | } 637 | responseIdxs.push(i - responseIdxs.length); 638 | } 639 | } 640 | if (responseIdxs.length > 0) { 641 | while (responseIdxs.length > 1) { 642 | let idx = responseIdxs.shift(); 643 | this.responseQueue.splice(idx, 1); 644 | } 645 | const idx = responseIdxs[0]; 646 | const response = this.responseQueue[idx]; 647 | this.responseQueue.splice(idx, 1); 648 | return response; 649 | } 650 | 651 | let responseCount = this.fetchResponse(); 652 | if (responseCount == 0) { 653 | basic.pause(fetchDelay); 654 | } 655 | } 656 | 657 | return null; 658 | } 659 | 660 | private fetchResponse(): number { 661 | const avail = this.transport.avail(); 662 | if (avail <= 0) { 663 | return 0; 664 | } 665 | 666 | let buffer = pins.createBuffer(avail); 667 | const size = this.transport.read(buffer, avail); 668 | 669 | if (size <= 0) { 670 | this.LOG("Failed to read data from transport."); 671 | return 0; 672 | } else if (size != avail) { 673 | this.LOG("Potential data loss, expected " + avail + " bytes but received " + size + " bytes."); 674 | } 675 | 676 | let start = 0; 677 | let end = 0; 678 | let responseCount = 0; 679 | 680 | for (let i = 0; i < size; ++i) { 681 | const ch = buffer.getNumber(NumberFormat.UInt8LE, i); 682 | 683 | if (ch == this.responseSof.charCodeAt(0)) { 684 | start = i + 1; 685 | } else if (ch == this.responseEof.charCodeAt(0)) { 686 | end = i; 687 | 688 | const response = buffer.slice(start, end); 689 | const responseStr = response.toString(); 690 | 691 | const responseObj = JSON.parse(responseStr); 692 | if (responseObj && typeof responseObj == "object") { 693 | while (this.responseQueue.length > this.responseQueueLimit) { 694 | this.responseQueue.shift(); 695 | } 696 | this.responseQueue.push(responseObj); 697 | ++responseCount; 698 | } else { 699 | this.LOG("Failed to parse response: " + responseStr); 700 | } 701 | start = end + 1; 702 | } 703 | } 704 | 705 | return responseCount; 706 | } 707 | 708 | private updateDeviceStatus(): void { 709 | if (!this.transport.isConnected()) { 710 | this.deviceStatus = DeviceStatus.Disconnected; 711 | return; 712 | } 713 | 714 | let maxRetry = this.maxRetry; 715 | while (maxRetry >= 0) { 716 | this.getDeviceId(false, this.retryTimeout); 717 | this.getDeviceName(false, this.retryTimeout); 718 | if (this.cachedDeviceId.length > 0 && this.cachedDeviceName.length > 0) { 719 | this.deviceStatus = DeviceStatus.Idle; 720 | return; 721 | } 722 | --maxRetry; 723 | basic.pause(this.retryDelay); 724 | } 725 | 726 | this.deviceStatus = DeviceStatus.Busy; 727 | } 728 | 729 | public getDeviceStatus(fromCache: boolean = false): DeviceStatus { 730 | if (!fromCache) { 731 | this.updateDeviceStatus(); 732 | this.isInference(); 733 | } 734 | return this.deviceStatus; 735 | } 736 | 737 | public getDeviceId(fromCache: boolean = true, timeout: number = 1000, cleanupIf: number = 0): string { 738 | if (this.deviceStatus == DeviceStatus.Disconnected) { 739 | return ""; 740 | } 741 | 742 | if (fromCache && this.cachedDeviceId.length > 0) { 743 | return this.cachedDeviceId; 744 | } 745 | 746 | this.transport.cleanup(cleanupIf); 747 | 748 | const cmdName = "ID?"; 749 | const cmd = this.commandPrefix + cmdName + this.commandSuffix; 750 | const data = pins.createBuffer(cmd.length); 751 | for (let i = 0; i < cmd.length; ++i) { 752 | data.setNumber(NumberFormat.UInt8LE, i, cmd.charCodeAt(i)); 753 | } 754 | const size = this.transport.write(data, data.length); 755 | if (size != data.length) { 756 | this.LOG("Failed to request device ID."); 757 | return ""; 758 | } 759 | 760 | const response = this.waitResponse(ResponseType.Direct, cmdName, false, 30, timeout); 761 | if (response && response["code"] == OperationCode.Success) { 762 | const deviceId = response["data"]; 763 | if (typeof deviceId == "string") { 764 | this.cachedDeviceId = deviceId; 765 | return this.cachedDeviceId; 766 | } 767 | this.LOG("Invalid device ID format: " + typeof deviceId); 768 | } 769 | this.LOG("Failed to parse response."); 770 | 771 | return ""; 772 | } 773 | 774 | public getDeviceName(fromCache: boolean = true, timeout: number = 1000, cleanupIf: number = 0): string { 775 | if (this.deviceStatus == DeviceStatus.Disconnected) { 776 | return ""; 777 | } 778 | 779 | if (fromCache && this.cachedDeviceName.length > 0) { 780 | return this.cachedDeviceName; 781 | } 782 | 783 | this.transport.cleanup(cleanupIf); 784 | 785 | const cmdName = "NAME?"; 786 | const cmd = this.commandPrefix + cmdName + this.commandSuffix; 787 | const data = pins.createBuffer(cmd.length); 788 | for (let i = 0; i < cmd.length; ++i) { 789 | data.setNumber(NumberFormat.UInt8LE, i, cmd.charCodeAt(i)); 790 | } 791 | const size = this.transport.write(data, data.length); 792 | if (size != data.length) { 793 | this.LOG("Failed to request device name."); 794 | return ""; 795 | } 796 | 797 | const response = this.waitResponse(ResponseType.Direct, cmdName, false, 30, timeout); 798 | if (response && response["code"] == OperationCode.Success) { 799 | const deviceName = response["data"]; 800 | if (typeof deviceName == "string") { 801 | this.cachedDeviceName = deviceName; 802 | return this.cachedDeviceName; 803 | } 804 | this.LOG("Invalid device name format: " + typeof deviceName); 805 | } 806 | this.LOG("Failed to parse response."); 807 | 808 | return ""; 809 | } 810 | 811 | public getModelInfo(fromCache: boolean = true, timeout: number = 1000, cleanupIf: number = 0): ModelInfo { 812 | if (this.deviceStatus == DeviceStatus.Disconnected) { 813 | return new ModelInfo("", "", []); 814 | } 815 | 816 | if (fromCache && this.cachedModelInfo.name.length > 0) { 817 | return this.cachedModelInfo; 818 | } 819 | 820 | this.transport.cleanup(cleanupIf); 821 | 822 | const cmdName = "INFO?"; 823 | const cmd = this.commandPrefix + cmdName + this.commandSuffix; 824 | const data = pins.createBuffer(cmd.length); 825 | for (let i = 0; i < cmd.length; ++i) { 826 | data.setNumber(NumberFormat.UInt8LE, i, cmd.charCodeAt(i)); 827 | } 828 | const size = this.transport.write(data, data.length); 829 | if (size != data.length) { 830 | this.LOG("Failed to request model info."); 831 | return this.cachedModelInfo; 832 | } 833 | const response = this.waitResponse(ResponseType.Direct, cmdName, false, 30, timeout); 834 | if (!response || response["code"] != OperationCode.Success) { 835 | this.LOG("Failed to get model info: " + response["code"]); 836 | return this.cachedModelInfo; 837 | } 838 | const modelObj = response["data"]; 839 | if (!modelObj || typeof modelObj != "object") { 840 | this.LOG("Invalid model info format: " + typeof modelObj); 841 | return new ModelInfo("", "", []); 842 | } 843 | const encodedModelInfo = modelObj["info"]; 844 | if (typeof encodedModelInfo != "string") { 845 | this.LOG("Invalid model info format: " + typeof encodedModelInfo); 846 | return new ModelInfo("", "", []); 847 | } 848 | const decodedModelInfo = decodeBase64(encodedModelInfo); 849 | if (!decodedModelInfo || decodedModelInfo.length <= 0) { 850 | this.LOG("Failed to decode model info: " + encodedModelInfo); 851 | return new ModelInfo("", "", []); 852 | } 853 | const fullModelInfo = JSON.parse(decodedModelInfo); 854 | if (fullModelInfo && typeof fullModelInfo == "object") { 855 | this.cachedModelInfo = new ModelInfo( 856 | fullModelInfo["model_name"], 857 | fullModelInfo["version"], 858 | fullModelInfo["classes"] 859 | ); 860 | return this.cachedModelInfo; 861 | } 862 | this.LOG("Failed to parse model info."); 863 | 864 | return new ModelInfo("", "", []); 865 | } 866 | 867 | public isInference(timeout: number = 1000, cleanupIf: number = 0): boolean { 868 | if (this.deviceStatus == DeviceStatus.Disconnected) { 869 | return false; 870 | } 871 | 872 | this.transport.cleanup(cleanupIf); 873 | 874 | const cmdName = "INVOKE?"; 875 | const cmd = this.commandPrefix + cmdName + this.commandSuffix; 876 | const data = pins.createBuffer(cmd.length); 877 | for (let i = 0; i < cmd.length; ++i) { 878 | data.setNumber(NumberFormat.UInt8LE, i, cmd.charCodeAt(i)); 879 | } 880 | const size = this.transport.write(data, data.length); 881 | if (size != data.length) { 882 | this.LOG("Failed to request inference status."); 883 | return false; 884 | } 885 | const response = this.waitResponse(ResponseType.Direct, cmdName, false, 30, timeout); 886 | if (response && response["code"] == OperationCode.Success) { 887 | const inferenceStatus = response["data"]; 888 | if (typeof inferenceStatus == "number") { 889 | this.deviceStatus = inferenceStatus == 1 ? DeviceStatus.Inferencing : DeviceStatus.Idle; 890 | return this.deviceStatus == DeviceStatus.Inferencing; 891 | } 892 | this.LOG("Invalid inference status format: " + typeof inferenceStatus); 893 | } 894 | 895 | return false; 896 | } 897 | 898 | public startInference(timeout: number = 1000, cleanupIf: number = 0): boolean { 899 | if (this.deviceStatus == DeviceStatus.Disconnected) { 900 | return false; 901 | } 902 | 903 | this.getModelInfo(true, timeout, cleanupIf); 904 | 905 | this.transport.cleanup(cleanupIf); 906 | 907 | const cmdName = "INVOKE"; 908 | const cmdArgs = "=-1,0,1"; 909 | const cmd = this.commandPrefix + cmdName + cmdArgs + this.commandSuffix; 910 | const data = pins.createBuffer(cmd.length); 911 | for (let i = 0; i < cmd.length; ++i) { 912 | data.setNumber(NumberFormat.UInt8LE, i, cmd.charCodeAt(i)); 913 | } 914 | const size = this.transport.write(data, data.length); 915 | if (size != data.length) { 916 | this.LOG("Failed to request inference start."); 917 | return false; 918 | } 919 | const response = this.waitResponse(ResponseType.Direct, cmdName, false, 30, timeout); 920 | if (!response) { 921 | this.LOG("Failed to get inference start response."); 922 | return false; 923 | } 924 | if (response["code"] == OperationCode.Success) { 925 | this.deviceStatus = DeviceStatus.Inferencing; 926 | return true; 927 | } else { 928 | this.LOG("Failed to start inference: " + response["code"]); 929 | } 930 | 931 | return false; 932 | } 933 | 934 | public stopInference(timeout: number = 1000): void { 935 | if (this.deviceStatus == DeviceStatus.Disconnected) { 936 | return; 937 | } 938 | 939 | this.transport.cleanup(); 940 | 941 | const cmdName = "BREAK"; 942 | const cmd = this.commandPrefix + cmdName + this.commandSuffix; 943 | const data = pins.createBuffer(cmd.length); 944 | for (let i = 0; i < cmd.length; ++i) { 945 | data.setNumber(NumberFormat.UInt8LE, i, cmd.charCodeAt(i)); 946 | } 947 | const size = this.transport.write(data, data.length); 948 | if (size != data.length) { 949 | this.LOG("Failed to request inference stop."); 950 | return; 951 | } 952 | const response = this.waitResponse(ResponseType.Direct, cmdName, false, 30, timeout); 953 | if (response && response["code"] == OperationCode.Success) { 954 | this.deviceStatus = DeviceStatus.Idle; 955 | return; 956 | } 957 | this.LOG("Failed to stop inference."); 958 | } 959 | 960 | public fetchInferenceResult( 961 | onDetection: (results: DetectionResult[]) => void = null, 962 | onClassification: (results: ClassificationResult[]) => void = null, 963 | onError: (code: number) => void = null, 964 | maxResults: number = 15, 965 | timeout: number = 1000, 966 | cleanupIf: number = 0, 967 | ): boolean { 968 | if (this.deviceStatus == DeviceStatus.Disconnected) { 969 | return false; 970 | } 971 | if (this.deviceStatus != DeviceStatus.Inferencing) { 972 | return false; 973 | } 974 | 975 | this.transport.cleanup(cleanupIf); 976 | 977 | const event = this.waitResponse(ResponseType.Event, "INVOKE", true, 30, timeout); 978 | if (!event) { 979 | this.LOG("Failed to get inference result."); 980 | if (onError) { 981 | onError(OperationCode.Timeout); 982 | } 983 | return false; 984 | } 985 | 986 | if (event["code"] != OperationCode.Success) { 987 | this.LOG("Inference error: " + event["code"]); 988 | if (onError) { 989 | onError(event["code"]); 990 | } 991 | this.deviceStatus = DeviceStatus.Idle; 992 | return false; 993 | } 994 | 995 | const result = event["data"]; 996 | if (!result || typeof result != "object") { 997 | this.LOG("Invalid inference result format: " + typeof result); 998 | if (onError) { 999 | onError(OperationCode.Again); 1000 | } 1001 | return false; 1002 | } 1003 | 1004 | let resolution: number[]; 1005 | const unsafeResolution: any[] = result["resolution"]; 1006 | if (unsafeResolution && 1007 | typeof unsafeResolution == "object" && 1008 | unsafeResolution.length >= 2 && 1009 | typeof unsafeResolution[0] == "number" && 1010 | typeof unsafeResolution[1] == "number" 1011 | ) { 1012 | resolution = unsafeResolution as number[]; 1013 | } else { 1014 | this.LOG("Invalid resolution format: " + typeof result["resolution"]); 1015 | if (onError) { 1016 | onError(OperationCode.Unknown); 1017 | } 1018 | return false; 1019 | } 1020 | 1021 | if (onDetection) { 1022 | let detections: DetectionResult[] = []; 1023 | const unsafeDetections: any[] = result["boxes"]; 1024 | if (unsafeDetections && typeof unsafeDetections == "object") { 1025 | const maxDetections = Math.min(unsafeDetections.length, maxResults); 1026 | for (let i = 0; i < maxDetections; ++i) { 1027 | const unsafeDetection: any[] = unsafeDetections[i]; 1028 | if (!unsafeDetection || unsafeDetection.length < 6) { 1029 | continue; 1030 | } 1031 | let badResult = false; 1032 | for (let j = 0; j < 6; ++j) { 1033 | const value = unsafeDetection[j]; 1034 | if (typeof value != "number") { 1035 | badResult = true; 1036 | break; 1037 | } 1038 | } 1039 | if (badResult) { 1040 | continue; 1041 | } 1042 | const x = unsafeDetection[0] / resolution[0]; 1043 | const y = unsafeDetection[1] / resolution[1]; 1044 | const w = unsafeDetection[2] / resolution[0]; 1045 | const h = unsafeDetection[3] / resolution[1]; 1046 | const score = unsafeDetection[4]; 1047 | const id = unsafeDetection[5]; 1048 | const label = this.cachedModelInfo.getLabel(id); 1049 | 1050 | detections.push(new DetectionResult(x, y, w, h, score, id, label)); 1051 | } 1052 | } 1053 | 1054 | detections.sort((a, b) => { 1055 | return b.score - a.score; 1056 | }); 1057 | onDetection(detections); 1058 | } 1059 | 1060 | if (onClassification) { 1061 | let classifications: ClassificationResult[] = []; 1062 | const unsafeClassifications: any[] = result["classes"]; 1063 | if (unsafeClassifications && typeof unsafeClassifications == "object") { 1064 | const maxClassifications = Math.min(unsafeClassifications.length, maxResults); 1065 | for (let i = 0; i < maxClassifications; ++i) { 1066 | const unsafeClassification: any[] = unsafeClassifications[i]; 1067 | if (!unsafeClassification || unsafeClassification.length < 2) { 1068 | continue; 1069 | } 1070 | let badResult = false; 1071 | for (let j = 0; j < 2; ++j) { 1072 | const value = unsafeClassification[j]; 1073 | if (typeof value != "number") { 1074 | badResult = true; 1075 | break; 1076 | } 1077 | } 1078 | if (badResult) { 1079 | continue; 1080 | } 1081 | const score = unsafeClassification[0]; 1082 | const id = unsafeClassification[1]; 1083 | const label = this.cachedModelInfo.getLabel(id); 1084 | 1085 | classifications.push(new ClassificationResult(score, id, label)); 1086 | } 1087 | } 1088 | 1089 | classifications.sort((a, b) => { 1090 | return b.score - a.score; 1091 | }); 1092 | onClassification(classifications); 1093 | } 1094 | 1095 | return true; 1096 | } 1097 | }; 1098 | 1099 | 1100 | } 1101 | 1102 | } 1103 | 1104 | } 1105 | -------------------------------------------------------------------------------- /pxt.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Grove", 3 | "version": "0.7.1", 4 | "description": "A Microsoft MakeCode package for Seeed Studio Grove module", 5 | "license": "MIT", 6 | "dependencies": { 7 | "core": "*" 8 | }, 9 | "files": [ 10 | "README.md", 11 | "main.ts", 12 | "blocks/GroveAHT20.ts", 13 | "blocks/GroveLCD1602v1.ts", 14 | "blocks/GroveVisionAIV2.ts", 15 | "plugins/SSCMA.ts", 16 | "blocks/GroveLCD1602v1.ts", 17 | "blocks/GroveDHT11.ts", 18 | "_locales/ja/Grove-strings.json", 19 | "_locales/de/Grove-strings.json", 20 | "sensors/AHT20.ts", 21 | "sensors/DHT11.cpp", 22 | "sensors/DHT11Helper.ts" 23 | ], 24 | "testFiles": [ 25 | "test.ts" 26 | ], 27 | "public": true, 28 | "targetVersions": { 29 | "target": "7.0.51", 30 | "targetId": "microbit" 31 | }, 32 | "supportedTargets": [ 33 | "calliopemini", 34 | "microbit" 35 | ], 36 | "preferredEditor": "tsprj" 37 | } 38 | -------------------------------------------------------------------------------- /sensors/AHT20.ts: -------------------------------------------------------------------------------- 1 | namespace grove 2 | { 3 | export namespace sensors 4 | { 5 | 6 | export class AHT20 7 | { 8 | public constructor(address: number = 0x38) 9 | { 10 | this._Address = address; 11 | } 12 | 13 | public Initialization(): AHT20 14 | { 15 | const buf = pins.createBuffer(3); 16 | buf[0] = 0xbe; 17 | buf[1] = 0x08; 18 | buf[2] = 0x00; 19 | pins.i2cWriteBuffer(this._Address, buf, false); 20 | basic.pause(10); 21 | 22 | return this; 23 | } 24 | 25 | public TriggerMeasurement(): AHT20 26 | { 27 | const buf = pins.createBuffer(3); 28 | buf[0] = 0xac; 29 | buf[1] = 0x33; 30 | buf[2] = 0x00; 31 | pins.i2cWriteBuffer(this._Address, buf, false); 32 | basic.pause(80); 33 | 34 | return this; 35 | } 36 | 37 | public GetState(): { Busy: boolean, Calibrated: boolean } 38 | { 39 | const buf = pins.i2cReadBuffer(this._Address, 1, false); 40 | const busy = buf[0] & 0x80 ? true : false; 41 | const calibrated = buf[0] & 0x08 ? true : false; 42 | 43 | return { Busy: busy, Calibrated: calibrated }; 44 | } 45 | 46 | public Read(): { Humidity: number, Temperature: number } 47 | { 48 | const buf = pins.i2cReadBuffer(this._Address, 7, false); 49 | 50 | const crc8 = AHT20.CalcCRC8(buf, 0, 6); 51 | if (buf[6] != crc8) return null; 52 | 53 | const humidity = ((buf[1] << 12) + (buf[2] << 4) + (buf[3] >> 4)) * 100 / 1048576; 54 | const temperature = (((buf[3] & 0x0f) << 16) + (buf[4] << 8) + buf[5]) * 200 / 1048576 - 50; 55 | 56 | return { Humidity: humidity, Temperature: temperature }; 57 | } 58 | 59 | private _Address: number; 60 | 61 | private static CalcCRC8(buf: Buffer, offset: number, size: number): number 62 | { 63 | let crc8 = 0xff; 64 | for (let i = 0; i < size; ++i) 65 | { 66 | crc8 ^= buf[offset + i]; 67 | for (let j = 0; j < 8; ++j) 68 | { 69 | if (crc8 & 0x80) 70 | { 71 | crc8 <<= 1; 72 | crc8 ^= 0x31; 73 | } 74 | else 75 | { 76 | crc8 <<= 1; 77 | } 78 | crc8 &= 0xff; 79 | } 80 | } 81 | 82 | return crc8; 83 | } 84 | 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /sensors/DHT11.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Grove Temperature & Humidity Sensor (DHT11) support by @nullptr, 2025.5.19 3 | */ 4 | 5 | #include "pxt.h" 6 | #include "MicroBitConfig.h" 7 | #include "MicroBit.h" 8 | 9 | #if MICROBIT_CODAL 10 | #include "nrf.h" 11 | #include "nrf_soc.h" 12 | #include "nrf_systick.h" 13 | 14 | #define _DHT11_D_CLOCK_IMPL_VER 2 15 | 16 | #else 17 | #if defined(__arm) 18 | #define _DHT11_D_CLOCK_IMPL_VER 3 19 | #else 20 | #define _DHT11_D_CLOCK_IMPL_VER 0 21 | #endif 22 | 23 | #endif 24 | 25 | #define _DHT11_LIKELY(x) __builtin_expect(!!(x), 1) 26 | #define _DHT11_UNLIKELY(x) __builtin_expect(!!(x), 0) 27 | 28 | #define _DHT11_D_IMPL_VER 0 29 | 30 | #define _DHT11_F_PIN_DIGITAL_WRITE_LOW pin->setDigitalValue(0) 31 | #define _DHT11_F_PIN_DIGITAL_READ pin->getDigitalValue() 32 | #define _DHT11_T_TIME_MICROS uint32_t 33 | 34 | #if _DHT11_D_CLOCK_IMPL_VER == 0 35 | #define _DHT11_C_TIME_MICROS_MASK 0x3fffffff 36 | 37 | static uint32_t _dht11_system_timer_snapshot = 0; 38 | 39 | inline __attribute__((always_inline)) void _dht11_system_timer_sync() 40 | { 41 | _dht11_system_timer_snapshot = system_timer_current_time_us() & _DHT11_C_TIME_MICROS_MASK; 42 | } 43 | 44 | inline __attribute__((always_inline)) uint32_t _dht11_system_timer_as_micros() 45 | { 46 | const uint32_t val = system_timer_current_time_us() & _DHT11_C_TIME_MICROS_MASK; 47 | const uint64_t period = val < _dht11_system_timer_snapshot ? static_cast(_DHT11_C_TIME_MICROS_MASK) - _dht11_system_timer_snapshot + val : static_cast(val) - _dht11_system_timer_snapshot; 48 | return static_cast(period); 49 | } 50 | 51 | #define _DHT11_F_TIME_MICROS_SYNC _dht11_system_timer_sync() 52 | #define _DHT11_F_TIME_MICROS (_dht11_system_timer_as_micros() & _DHT11_C_TIME_MICROS_MASK) 53 | 54 | #elif _DHT11_D_CLOCK_IMPL_VER == 1 55 | #include "mbed.h" 56 | 57 | #define _DHT11_C_TIME_MICROS_MASK 0x3fffffff 58 | 59 | static uint32_t _dht11_system_timer_snapshot = 0; 60 | 61 | inline __attribute__((always_inline)) void _dht11_system_timer_sync() 62 | { 63 | _dht11_system_timer_snapshot = us_ticker_read() & _DHT11_C_TIME_MICROS_MASK; 64 | } 65 | 66 | inline __attribute__((always_inline)) uint32_t _dht11_system_timer_as_micros() 67 | { 68 | const uint32_t val = us_ticker_read() & _DHT11_C_TIME_MICROS_MASK; 69 | const uint64_t period = val < _dht11_system_timer_snapshot ? static_cast(_DHT11_C_TIME_MICROS_MASK) - _dht11_system_timer_snapshot + val : static_cast(val) - _dht11_system_timer_snapshot; 70 | return static_cast(period); 71 | } 72 | 73 | #define _DHT11_F_TIME_MICROS_SYNC _dht11_system_timer_sync() 74 | #define _DHT11_F_TIME_MICROS (_dht11_system_timer_as_micros() & _DHT11_C_TIME_MICROS_MASK) 75 | 76 | #elif _DHT11_D_CLOCK_IMPL_VER == 2 77 | #define _DHT11_C_SYSTICK_WRAP_MIN_MS 30 78 | #define _DHT11_C_TIME_MICROS_MASK 0x3fffffff 79 | 80 | extern "C" 81 | { 82 | 83 | extern uint32_t SystemCoreClock; 84 | 85 | static uint32_t _dht11_systick_snapshot = 0; 86 | 87 | inline __attribute__((always_inline)) void _dht11_systick_sync() 88 | { 89 | _dht11_systick_snapshot = nrf_systick_val_get() & NRF_SYSTICK_VAL_MASK; 90 | } 91 | 92 | inline __attribute__((always_inline)) int _dht11_systick_init() 93 | { 94 | if (_DHT11_UNLIKELY(!SystemCoreClock)) 95 | { 96 | return 0b0011; 97 | } 98 | 99 | const uint32_t ctrl = nrf_systick_csr_get(); 100 | if (_DHT11_UNLIKELY((ctrl & NRF_SYSTICK_CSR_ENABLE_MASK) == NRF_SYSTICK_CSR_DISABLE)) 101 | { 102 | nrf_systick_load_set(NRF_SYSTICK_VAL_MASK); 103 | nrf_systick_csr_set( 104 | NRF_SYSTICK_CSR_CLKSOURCE_CPU | 105 | NRF_SYSTICK_CSR_TICKINT_DISABLE | 106 | NRF_SYSTICK_CSR_ENABLE); 107 | } 108 | 109 | const uint32_t load = nrf_systick_load_get() & NRF_SYSTICK_VAL_MASK; 110 | const uint32_t wrap_period_ms = (static_cast(load) * 1e3) / SystemCoreClock; 111 | if (_DHT11_UNLIKELY(wrap_period_ms < _DHT11_C_SYSTICK_WRAP_MIN_MS)) 112 | { 113 | return 0b0111; 114 | } 115 | 116 | _dht11_systick_sync(); 117 | 118 | return 0; 119 | } 120 | 121 | inline __attribute__((always_inline)) 122 | uint32_t 123 | _dht11_systick_as_micros() 124 | { 125 | const uint32_t val = nrf_systick_val_get() & NRF_SYSTICK_VAL_MASK; 126 | const uint64_t period = val < _dht11_systick_snapshot ? static_cast(_dht11_systick_snapshot - val) : static_cast(NRF_SYSTICK_VAL_MASK) + _dht11_systick_snapshot - val; 127 | return static_cast((static_cast(period) * 1e6) / SystemCoreClock); 128 | } 129 | 130 | } // extern "C" 131 | 132 | #define _DHT11_F_TIME_MICROS_INIT _dht11_systick_init() 133 | #define _DHT11_F_TIME_MICROS_SYNC _dht11_systick_sync() 134 | #define _DHT11_F_TIME_MICROS (_dht11_systick_as_micros() & _DHT11_C_TIME_MICROS_MASK) 135 | 136 | #elif _DHT11_D_CLOCK_IMPL_VER == 3 137 | #define _DHT11_C_SYSTICK_WRAP_MIN_MS 30 138 | #define _DHT11_C_TIME_MICROS_MASK 0x3fffffff 139 | 140 | extern "C" 141 | { 142 | 143 | extern uint32_t SystemCoreClock; 144 | 145 | static uint32_t _dht11_systick_snapshot = 0; 146 | 147 | inline __attribute__((always_inline)) void _dht11_systick_sync() 148 | { 149 | _dht11_systick_snapshot = SysTick->VAL & SysTick_VAL_CURRENT_Msk; 150 | } 151 | 152 | inline __attribute__((always_inline)) int _dht11_systick_init() 153 | { 154 | if (_DHT11_UNLIKELY(!SystemCoreClock)) 155 | { 156 | return 0b0011; 157 | } 158 | 159 | const uint32_t ctrl = SysTick->CTRL; 160 | if (_DHT11_UNLIKELY((ctrl & SysTick_CTRL_ENABLE_Msk) == (1U << SysTick_CTRL_ENABLE_Pos))) 161 | { 162 | SysTick->LOAD = SysTick_VAL_CURRENT_Msk; 163 | SysTick->CTRL = ((1U << SysTick_CTRL_CLKSOURCE_Pos) | 164 | (0U << SysTick_CTRL_TICKINT_Pos) | 165 | (1U << SysTick_CTRL_ENABLE_Pos)); 166 | } 167 | 168 | const uint32_t load = (SysTick->LOAD) & SysTick_VAL_CURRENT_Msk; 169 | const uint32_t wrap_period_ms = (static_cast(load) * 1e3) / SystemCoreClock; 170 | if (_DHT11_UNLIKELY(wrap_period_ms < _DHT11_C_SYSTICK_WRAP_MIN_MS)) 171 | { 172 | return 0b0111; 173 | } 174 | 175 | _dht11_systick_sync(); 176 | 177 | return 0; 178 | } 179 | 180 | inline __attribute__((always_inline)) 181 | uint32_t 182 | _dht11_systick_as_micros() 183 | { 184 | const uint32_t val = SysTick->VAL & SysTick_VAL_CURRENT_Msk; 185 | const uint64_t period = val < _dht11_systick_snapshot ? static_cast(_dht11_systick_snapshot - val) : static_cast(SysTick_VAL_CURRENT_Msk) + _dht11_systick_snapshot - val; 186 | return static_cast((static_cast(period) * 1e6) / SystemCoreClock); 187 | } 188 | 189 | } // extern "C" 190 | 191 | #define _DHT11_F_TIME_MICROS_INIT _dht11_systick_init() 192 | #define _DHT11_F_TIME_MICROS_SYNC _dht11_systick_sync() 193 | #define _DHT11_F_TIME_MICROS (_dht11_systick_as_micros() & _DHT11_C_TIME_MICROS_MASK) 194 | 195 | #else 196 | #error "Unsupported _DHT11_D_CLOCK_IMPL_VER" 197 | 198 | #endif 199 | 200 | #define _DHT11_C_PULLDOWN_TIME 20000 201 | #define _DHT11_C_ACK_1_TIMEOUT 300 202 | #define _DHT11_C_ACK_2_TIMEOUT 40 203 | #define _DHT11_C_DATA_BITS_WAIT_DELAY 90 204 | #define _DHT11_C_DATA_BITS_LOW_TIMEOUT 70 205 | #define _DHT11_C_DATA_BITS_HIGH_DELAY 30 206 | #define _DHT11_C_DATA_BITS_HIGH_TIMEOUT 90 207 | 208 | using namespace pxt; 209 | 210 | namespace grove 211 | { 212 | 213 | namespace sensors 214 | { 215 | 216 | #if _DHT11_D_IMPL_VER == 1 217 | // __attribute__((noinline, long_call, section(".ramfuncs"), optimize("s"))) 218 | int64_t 219 | __dht11_read_impl_v1(const int pin_num) 220 | { 221 | MicroBitPin *pin = getPin(pin_num); 222 | if (_DHT11_UNLIKELY(!pin)) 223 | return 1ll << 40; 224 | 225 | register int64_t result = 0; 226 | 227 | _DHT11_T_TIME_MICROS start_time = 0; 228 | 229 | _DHT11_F_PIN_DIGITAL_WRITE_LOW; 230 | const bool irq_enabled = __get_PRIMASK() ^ 1; 231 | __disable_irq(); 232 | 233 | #ifdef _DHT11_F_TIME_MICROS_SYNC 234 | _DHT11_F_TIME_MICROS_SYNC; 235 | #endif 236 | start_time = _DHT11_F_TIME_MICROS; 237 | while (static_cast<_DHT11_T_TIME_MICROS>(_DHT11_F_TIME_MICROS - start_time) < _DHT11_C_PULLDOWN_TIME) 238 | ; 239 | start_time = _DHT11_F_TIME_MICROS; 240 | 241 | #if MICROBIT_CODAL 242 | pin->setPull(codal::PullMode::Up); 243 | #else 244 | pin->setPull(PinMode::PullUp); 245 | #endif 246 | 247 | while (_DHT11_F_PIN_DIGITAL_READ ^ 0) 248 | { 249 | if (_DHT11_UNLIKELY(static_cast<_DHT11_T_TIME_MICROS>(_DHT11_F_TIME_MICROS - start_time) > _DHT11_C_ACK_1_TIMEOUT)) 250 | { 251 | if (irq_enabled) 252 | { 253 | __enable_irq(); 254 | } 255 | return 1ll << 41; 256 | } 257 | } 258 | 259 | while (_DHT11_F_PIN_DIGITAL_READ ^ 1) 260 | { 261 | if (_DHT11_UNLIKELY(static_cast<_DHT11_T_TIME_MICROS>(_DHT11_F_TIME_MICROS - start_time) > _DHT11_C_ACK_1_TIMEOUT)) 262 | { 263 | if (irq_enabled) 264 | { 265 | __enable_irq(); 266 | } 267 | return 1ll << 42; 268 | } 269 | } 270 | start_time = _DHT11_F_TIME_MICROS; 271 | 272 | while (static_cast<_DHT11_T_TIME_MICROS>(_DHT11_F_TIME_MICROS - start_time) < _DHT11_C_ACK_2_TIMEOUT) 273 | ; 274 | 275 | if (_DHT11_UNLIKELY(_DHT11_F_PIN_DIGITAL_READ ^ 1)) 276 | { 277 | if (irq_enabled) 278 | { 279 | __enable_irq(); 280 | } 281 | return 1ll << 43; 282 | } 283 | 284 | while (static_cast<_DHT11_T_TIME_MICROS>(_DHT11_F_TIME_MICROS - start_time) < _DHT11_C_DATA_BITS_WAIT_DELAY) 285 | ; 286 | 287 | for (int i = 39; i >= 0; --i) 288 | { 289 | start_time = _DHT11_F_TIME_MICROS; 290 | while (_DHT11_F_PIN_DIGITAL_READ ^ 1) 291 | { 292 | if (_DHT11_UNLIKELY(static_cast<_DHT11_T_TIME_MICROS>(_DHT11_F_TIME_MICROS - start_time) > _DHT11_C_DATA_BITS_LOW_TIMEOUT)) 293 | { 294 | if (irq_enabled) 295 | { 296 | __enable_irq(); 297 | } 298 | return 1ll << 44; 299 | } 300 | } 301 | start_time = _DHT11_F_TIME_MICROS; 302 | while (static_cast<_DHT11_T_TIME_MICROS>(_DHT11_F_TIME_MICROS - start_time) < _DHT11_C_DATA_BITS_HIGH_DELAY) 303 | ; 304 | 305 | result |= static_cast(_DHT11_F_PIN_DIGITAL_READ) << i; 306 | 307 | while (_DHT11_F_PIN_DIGITAL_READ ^ 0) 308 | { 309 | if (_DHT11_UNLIKELY(static_cast<_DHT11_T_TIME_MICROS>(_DHT11_F_TIME_MICROS - start_time) > _DHT11_C_DATA_BITS_HIGH_TIMEOUT)) 310 | { 311 | if (irq_enabled) 312 | { 313 | __enable_irq(); 314 | } 315 | return 1ll << 45; 316 | } 317 | } 318 | } 319 | if (irq_enabled) 320 | { 321 | __enable_irq(); 322 | } 323 | 324 | if (((result >> 32) + ((result >> 24) & 0xff) + ((result >> 16) & 0xff) + 325 | ((result >> 8) & 0xff)) ^ 326 | (result & 0xff)) 327 | return result | (1ll << 46); 328 | 329 | return result >> 8; 330 | } 331 | #endif 332 | 333 | #if _DHT11_D_IMPL_VER == 0 334 | // __attribute__((noinline, long_call, section(".ramfuncs"))) 335 | int64_t 336 | __dht11_read_impl_v0(const int pin_num) 337 | { 338 | MicroBitPin *pin = getPin((int)pin_num); 339 | if (!pin) 340 | return 1ll << 40; 341 | 342 | bool data_bits[40]; 343 | 344 | { 345 | _DHT11_T_TIME_MICROS start_time = 0; 346 | 347 | _DHT11_F_PIN_DIGITAL_WRITE_LOW; 348 | const bool irq_enabled = __get_PRIMASK() ^ 1; 349 | __disable_irq(); 350 | 351 | #ifdef _DHT11_F_TIME_MICROS_SYNC 352 | _DHT11_F_TIME_MICROS_SYNC; 353 | #endif 354 | start_time = _DHT11_F_TIME_MICROS; 355 | while (static_cast<_DHT11_T_TIME_MICROS>(_DHT11_F_TIME_MICROS - start_time) < _DHT11_C_PULLDOWN_TIME) 356 | ; 357 | start_time = _DHT11_F_TIME_MICROS; 358 | 359 | #if MICROBIT_CODAL 360 | pin->setPull(codal::PullMode::Up); 361 | #else 362 | pin->setPull(PinMode::PullUp); 363 | #endif 364 | 365 | while (_DHT11_F_PIN_DIGITAL_READ ^ 0) 366 | { 367 | if (static_cast<_DHT11_T_TIME_MICROS>(_DHT11_F_TIME_MICROS - start_time) > _DHT11_C_ACK_1_TIMEOUT) 368 | { 369 | if (irq_enabled) 370 | { 371 | __enable_irq(); 372 | } 373 | return 1ll << 41; 374 | } 375 | } 376 | 377 | while (_DHT11_F_PIN_DIGITAL_READ ^ 1) 378 | { 379 | if (static_cast<_DHT11_T_TIME_MICROS>(_DHT11_F_TIME_MICROS - start_time) > _DHT11_C_ACK_1_TIMEOUT) 380 | { 381 | if (irq_enabled) 382 | { 383 | __enable_irq(); 384 | } 385 | return 1ll << 42; 386 | } 387 | } 388 | start_time = _DHT11_F_TIME_MICROS; 389 | 390 | while (static_cast<_DHT11_T_TIME_MICROS>(_DHT11_F_TIME_MICROS - start_time) < _DHT11_C_ACK_2_TIMEOUT) 391 | ; 392 | 393 | if (_DHT11_F_PIN_DIGITAL_READ ^ 1) 394 | { 395 | if (irq_enabled) 396 | { 397 | __enable_irq(); 398 | } 399 | return 1ll << 43; 400 | } 401 | 402 | while (static_cast<_DHT11_T_TIME_MICROS>(_DHT11_F_TIME_MICROS - start_time) < _DHT11_C_DATA_BITS_WAIT_DELAY) 403 | ; 404 | 405 | for (int i = 0; i < 40; ++i) 406 | { 407 | start_time = _DHT11_F_TIME_MICROS; 408 | while (_DHT11_F_PIN_DIGITAL_READ ^ 1) 409 | { 410 | if (static_cast<_DHT11_T_TIME_MICROS>(_DHT11_F_TIME_MICROS - start_time) > _DHT11_C_DATA_BITS_LOW_TIMEOUT) 411 | { 412 | if (irq_enabled) 413 | { 414 | __enable_irq(); 415 | } 416 | return 1ll << 44; 417 | } 418 | } 419 | start_time = _DHT11_F_TIME_MICROS; 420 | while (static_cast<_DHT11_T_TIME_MICROS>(_DHT11_F_TIME_MICROS - start_time) < _DHT11_C_DATA_BITS_HIGH_DELAY) 421 | ; 422 | 423 | data_bits[i] = _DHT11_F_PIN_DIGITAL_READ; 424 | 425 | while (_DHT11_F_PIN_DIGITAL_READ ^ 0) 426 | { 427 | if (static_cast<_DHT11_T_TIME_MICROS>(_DHT11_F_TIME_MICROS - start_time) > _DHT11_C_DATA_BITS_HIGH_TIMEOUT) 428 | { 429 | if (irq_enabled) 430 | { 431 | __enable_irq(); 432 | } 433 | return 1ll << 45; 434 | } 435 | } 436 | } 437 | 438 | if (irq_enabled) 439 | { 440 | __enable_irq(); 441 | } 442 | } 443 | 444 | uint8_t data_bytes[5] = {0}; 445 | for (int i = 0; i < 5; ++i) 446 | { 447 | for (int j = 0; j < 8; ++j) 448 | { 449 | data_bytes[i] <<= 1; 450 | data_bytes[i] |= data_bits[i * 8 + j]; 451 | } 452 | } 453 | 454 | if (((data_bytes[0] + data_bytes[1] + data_bytes[2] + data_bytes[3]) & 0xff) != data_bytes[4]) 455 | return (static_cast(data_bytes[0]) << 32) | 456 | (static_cast(data_bytes[1]) << 24) | 457 | (static_cast(data_bytes[2]) << 16) | 458 | (static_cast(data_bytes[3]) << 8) | static_cast(data_bytes[4]) | 459 | (1ll << 46); 460 | 461 | return (static_cast(data_bytes[0]) << 24) | 462 | (static_cast(data_bytes[1]) << 16) | 463 | (static_cast(data_bytes[2]) << 8) | static_cast(data_bytes[3]); 464 | } 465 | #endif 466 | 467 | } // namespace sensors 468 | 469 | //% advanced=true 470 | //% 471 | Buffer DHT11InternalRead(int signalPin) 472 | { 473 | int64_t result = 0; 474 | 475 | result |= static_cast(_DHT11_D_CLOCK_IMPL_VER) << 48; 476 | result |= static_cast(_DHT11_D_IMPL_VER) << 56; 477 | 478 | #if defined(_DHT11_F_TIME_MICROS_INIT) 479 | int ret = _DHT11_F_TIME_MICROS_INIT; 480 | if (_DHT11_UNLIKELY(ret != 0)) 481 | { 482 | result |= static_cast(ret & 0xff) << 40; 483 | return mkBuffer(reinterpret_cast(&result), sizeof(result)); 484 | } 485 | #endif 486 | 487 | #if _DHT11_D_IMPL_VER == 0 488 | result |= grove::sensors::__dht11_read_impl_v0(signalPin); 489 | #elif _DHT11_D_IMPL_VER == 1 490 | result |= grove::sensors::__dht11_read_impl_v1(signalPin); 491 | #else 492 | result |= 0b0011; 493 | #endif 494 | 495 | return mkBuffer(reinterpret_cast(&result), sizeof(result)); 496 | } 497 | 498 | } // namespace grove 499 | 500 | namespace sensors 501 | { 502 | 503 | //% advanced=true 504 | //% 505 | Buffer DHT11InternalRead(int signalPin) 506 | { 507 | return grove::DHT11InternalRead(signalPin); 508 | } 509 | 510 | } // namespace sensors 511 | -------------------------------------------------------------------------------- /sensors/DHT11Helper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Grove Temperature & Humidity Sensor (DHT11) support by @nullptr, 2025.5.19 3 | */ 4 | 5 | namespace grove { 6 | 7 | //% advanced=true 8 | //% shim=sensors::DHT11InternalRead 9 | export function DHT11InternalRead(signalPin: DigitalPin): Buffer { 10 | let resultBuffer: Buffer = control.createBuffer(8); 11 | resultBuffer.fill(0); 12 | resultBuffer.setNumber(NumberFormat.Int8LE, 5, 1 << 1); 13 | return resultBuffer; 14 | } 15 | 16 | export namespace sensors { 17 | 18 | export class DHT11Helper { 19 | 20 | private signalPin: DigitalPin; 21 | private serialLogging: boolean = false; 22 | 23 | private lastSyncTime: number = 0; 24 | private lastSuccessSyncTime: number = 0; 25 | 26 | private _humidity: number = NaN; 27 | private _temperature: number = NaN; 28 | 29 | private static INIT_WAIT_TIME_MS: number = 1000; 30 | private static RESAMPLE_WAIT_TIME_MS: number = 2000; 31 | 32 | private static TEMP_MIN: number = 0; 33 | private static TEMP_MAX: number = 50; 34 | 35 | constructor(signalPin: DigitalPin, serialLogging: boolean = false) { 36 | this.signalPin = signalPin; 37 | this.serialLogging = serialLogging; 38 | 39 | this.lastSyncTime = input.runningTime(); 40 | } 41 | 42 | private LOG(message: string) { 43 | if (this.serialLogging) { 44 | serial.writeLine(message); 45 | } 46 | } 47 | 48 | get humidity(): number { 49 | return this._humidity; 50 | } 51 | 52 | get temperature(): number { 53 | return this._temperature; 54 | } 55 | 56 | getLastSuccessSyncTime(): number { 57 | return this.lastSuccessSyncTime; 58 | } 59 | 60 | public readSensorData(forceRead: boolean = false, retryTimes: number = 3, retryDelayMs: number = 2000): boolean { 61 | if (!forceRead) { 62 | const currentTime = input.runningTime(); 63 | const isInit = isNaN(this._humidity) || isNaN(this._temperature); 64 | const timeToWait = (isInit ? DHT11Helper.INIT_WAIT_TIME_MS : DHT11Helper.RESAMPLE_WAIT_TIME_MS) - (currentTime - this.lastSyncTime); 65 | if (timeToWait > 0) { 66 | if (!isInit) { 67 | this.LOG("Use cached DHT11 data, request new after " + timeToWait + "ms"); 68 | return true; 69 | } 70 | this.LOG(`Waiting for ${timeToWait}ms before reading sensor data.`); 71 | basic.pause(timeToWait); 72 | } 73 | this.lastSyncTime = currentTime; 74 | } 75 | 76 | this.LOG("Calling DHT11 internal driver..."); 77 | 78 | let retryCount = 0; 79 | let resultBuffer: Buffer; 80 | while (true) { 81 | if (retryCount > retryTimes) { 82 | this.LOG("DHT11 read failed after " + retryCount.toString() + " tries, max " + retryTimes.toString()); 83 | return false; 84 | } 85 | ++retryCount; 86 | resultBuffer = grove.DHT11InternalRead(this.signalPin); 87 | if (!resultBuffer || resultBuffer.length != 8) { 88 | this.LOG("DHT11 (" + retryCount.toString() + ") result buffer length error: " + resultBuffer.length.toString()); 89 | basic.pause(retryDelayMs); 90 | continue; 91 | } 92 | 93 | if (this.serialLogging) { 94 | const bufferLength = resultBuffer.length; 95 | serial.writeLine("DHT11 result buffer length: " + bufferLength.toString()); 96 | serial.writeString("DHT11 result buffer: "); 97 | for (let i = 0; i < bufferLength; ++i) { 98 | const byte = resultBuffer.getNumber(NumberFormat.Int8LE, i); 99 | serial.writeString(byte.toString() + " "); 100 | } 101 | serial.writeLine(""); 102 | } 103 | 104 | const clockImplVersion = resultBuffer.getNumber(NumberFormat.Int8LE, 6); 105 | this.LOG("DHT11 clock implementation version: " + clockImplVersion.toString()); 106 | const syncImplVersion = resultBuffer.getNumber(NumberFormat.Int8LE, 7); 107 | this.LOG("DHT11 sync implementation version: " + syncImplVersion.toString()); 108 | 109 | const returnCode = resultBuffer.getNumber(NumberFormat.Int8LE, 5); 110 | if (returnCode == 0) { 111 | this.lastSuccessSyncTime = input.runningTime(); 112 | this.LOG("DHT11 read success in " + retryCount.toString() + " tries, max " + retryTimes.toString()); 113 | break; 114 | } 115 | switch (returnCode) { 116 | case 1: 117 | this.LOG("DHT11 (" + retryCount.toString() + ") pin not found " + this.signalPin.toString()); 118 | return false; 119 | case 1 << 1: 120 | this.LOG("DHT11 (" + retryCount.toString() + ") sensor connection error, no response"); 121 | return false; 122 | case 1 << 2: 123 | this.LOG("DHT11 (" + retryCount.toString() + ") wait ack low timeout"); 124 | return false; 125 | case 1 << 3: 126 | this.LOG("DHT11 (" + retryCount.toString() + ") wait ack high timeout"); 127 | return false; 128 | case 1 << 4: 129 | this.LOG("DHT11 (" + retryCount.toString() + ") wait data high timeout"); 130 | break; 131 | case 1 << 5: 132 | this.LOG("DHT11 (" + retryCount.toString() + ") wait data low timeout"); 133 | break; 134 | case 1 << 6: 135 | this.LOG("DHT11 (" + retryCount.toString() + ") checksum error"); 136 | break; 137 | default: 138 | this.LOG("DHT11 (" + retryCount.toString() + ") internal error: " + returnCode.toString()); 139 | break; 140 | } 141 | basic.pause(retryDelayMs); 142 | } 143 | 144 | const humidityHigh = resultBuffer.getNumber(NumberFormat.Int8LE, 3); 145 | const humidityLow = resultBuffer.getNumber(NumberFormat.Int8LE, 2); 146 | const temperatureHigh = resultBuffer.getNumber(NumberFormat.Int8LE, 1); 147 | const temperatureLow = resultBuffer.getNumber(NumberFormat.Int8LE, 0); 148 | 149 | this._humidity = humidityHigh + (humidityLow * 0.01); 150 | this._temperature = temperatureHigh + (temperatureLow * 0.01); 151 | this._temperature = Math.max(DHT11Helper.TEMP_MIN, Math.min(DHT11Helper.TEMP_MAX, this._temperature)); 152 | 153 | this.LOG("DHT11 humidity: " + this._humidity.toString()); 154 | this.LOG("DHT11 temperature: " + this._temperature.toString()); 155 | 156 | return true; 157 | } 158 | 159 | }; 160 | 161 | } 162 | 163 | } 164 | -------------------------------------------------------------------------------- /test.ts: -------------------------------------------------------------------------------- 1 | let _4Digit = grove.createDisplay(DigitalPin.P0, DigitalPin.P1) 2 | grove.setupWifi( 3 | SerialPin.P15, 4 | SerialPin.P1, 5 | BaudRate.BaudRate115200, 6 | "test-ssid", 7 | "test-passwd" 8 | ) 9 | 10 | basic.forever(function () { 11 | _4Digit.bit(6, 1) 12 | }) 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "noImplicitAny": true, 5 | "outDir": "built", 6 | "rootDir": "." 7 | }, 8 | "exclude": ["pxt_modules/**/*test.ts"] 9 | } 10 | --------------------------------------------------------------------------------