669 lines
20 KiB
Markdown
669 lines
20 KiB
Markdown
# NiceBadge Tutorial
|
||
|
||
Step-by-step guide to TinyGo programming on the NiceBadge.
|
||
|
||
---
|
||
|
||
## What you need
|
||
|
||
- A **NiceBadge** board (nice!nano + display + peripherals assembled)
|
||
- A USB-C cable
|
||
- [Go](https://go.dev/dl/) ≥ 1.22
|
||
- [TinyGo](https://tinygo.org/getting-started/) ≥ 0.32
|
||
|
||
---
|
||
|
||
## Setup
|
||
|
||
### Install dependencies
|
||
|
||
From inside the `tutorial/` directory, fetch all Go module dependencies once:
|
||
|
||
```sh
|
||
cd tutorial
|
||
go mod tidy
|
||
```
|
||
|
||
### Flash a program
|
||
|
||
Every example is flashed the same way. From inside `tutorial/`:
|
||
|
||
```sh
|
||
tinygo flash -target nicenano ./basics/step0
|
||
```
|
||
|
||
Replace `./basics/step0` with the path to any step you want to run.
|
||
|
||
### NiceBadge hardware map
|
||
|
||
| Peripheral | Pin(s) | Notes |
|
||
|---|---|---|
|
||
| Button A | P1_06 | Active LOW, internal pull-up |
|
||
| Button B | P1_04 | Active LOW, internal pull-up |
|
||
| Rotary push | P0_22 | Active LOW, internal pull-up |
|
||
| Rotary encoder | P1_00 (A), P0_24 (B) | Quadrature via interrupt |
|
||
| WS2812 LEDs (×2) | P1_11 | Data signal |
|
||
| Buzzer | P0_31 | Passive, toggled at audio frequency |
|
||
| Joystick X | P0_02 | ADC, 0–65535 |
|
||
| Joystick Y | P0_29 | ADC, 0–65535 |
|
||
| Display (SPI) | SCK=P1_01, SDO=P1_02 | ST7789, 240×135 px |
|
||
| Display control | RST=P1_15, DC=P1_13, CS=P0_10, BL=P0_09 | |
|
||
| I2C (StemmQT) | SDA=P0_17, SCL=P0_20 | I2C1, 3.3 V |
|
||
|
||
---
|
||
|
||
## Basics
|
||
|
||
---
|
||
|
||
### step0 — Blink the built-in LED
|
||
|
||
**Goal:** confirm the toolchain works and the badge can be flashed.
|
||
|
||
The nice!nano has a small LED soldered directly on the microcontroller board. Blinking it is the embedded equivalent of "Hello, World!".
|
||
|
||
```go
|
||
package main
|
||
|
||
import (
|
||
"machine"
|
||
"time"
|
||
)
|
||
|
||
func main() {
|
||
led := machine.LED
|
||
led.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||
|
||
for {
|
||
led.Low()
|
||
time.Sleep(time.Millisecond * 500)
|
||
led.High()
|
||
time.Sleep(time.Millisecond * 500)
|
||
}
|
||
}
|
||
```
|
||
|
||
```sh
|
||
tinygo flash -target nicenano ./basics/step0
|
||
```
|
||
|
||
The LED on the nice!nano blinks once per second.
|
||
|
||
**Key concepts**
|
||
- `machine.PinOutput` — configure a pin so your program can drive it HIGH or LOW.
|
||
- `led.High()` / `led.Low()` — set the pin voltage.
|
||
- `time.Sleep` — pause execution without busy-waiting.
|
||
|
||
---
|
||
|
||
### step1 — LED + button A
|
||
|
||
**Goal:** read a digital input and use it to control an output.
|
||
|
||
```go
|
||
package main
|
||
|
||
import (
|
||
"machine"
|
||
"time"
|
||
)
|
||
|
||
func main() {
|
||
led := machine.LED
|
||
led.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||
|
||
btnA := machine.P1_06
|
||
btnA.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
|
||
|
||
for {
|
||
if !btnA.Get() { // LOW = pressed (active LOW with pull-up)
|
||
led.High()
|
||
} else {
|
||
led.Low()
|
||
}
|
||
time.Sleep(time.Millisecond * 10)
|
||
}
|
||
}
|
||
```
|
||
|
||
```sh
|
||
tinygo flash -target nicenano ./basics/step1
|
||
```
|
||
|
||
Hold button A — the LED turns on. Release it — the LED turns off.
|
||
|
||
**Key concepts**
|
||
- `machine.PinInputPullup` — the pin floats HIGH internally; pressing the button connects it to GND → LOW.
|
||
- `!btnA.Get()` — because the logic is inverted (active LOW), we negate the reading.
|
||
|
||
**Challenge:** modify the code so that the LED turns on when button **B** is pressed instead.
|
||
|
||
---
|
||
|
||
### step2 — WS2812 RGB LEDs
|
||
|
||
**Goal:** drive the two addressable RGB LEDs.
|
||
|
||
WS2812 (SK6812MINI-E) LEDs are controlled with a single data wire using a timed pulse protocol. The `ws2812` driver handles the timing; you just provide colors.
|
||
|
||
```go
|
||
package main
|
||
|
||
import (
|
||
"image/color"
|
||
"machine"
|
||
"time"
|
||
|
||
"tinygo.org/x/drivers/ws2812"
|
||
)
|
||
|
||
func main() {
|
||
neo := machine.P1_11
|
||
neo.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||
|
||
leds := ws2812.New(neo)
|
||
ledColors := make([]color.RGBA, 2)
|
||
|
||
red := color.RGBA{255, 0, 0, 255}
|
||
green := color.RGBA{0, 255, 0, 255}
|
||
|
||
rg := false
|
||
for {
|
||
for i := 0; i < 2; i++ {
|
||
if rg {
|
||
ledColors[i] = red
|
||
} else {
|
||
ledColors[i] = green
|
||
}
|
||
rg = !rg
|
||
}
|
||
leds.WriteColors(ledColors)
|
||
rg = !rg
|
||
time.Sleep(time.Millisecond * 300)
|
||
}
|
||
}
|
||
```
|
||
|
||
```sh
|
||
tinygo flash -target nicenano ./basics/step2
|
||
```
|
||
|
||
The two LEDs alternate between red and green every 300 ms.
|
||
|
||
**Key concepts**
|
||
- `color.RGBA{R, G, B, A}` — standard Go color type; A (alpha) is always 255 for LEDs.
|
||
- `leds.WriteColors(slice)` — pushes the entire color slice to the strip in one call.
|
||
|
||
**Challenge:** add a third color (blue) and cycle through all three.
|
||
|
||
---
|
||
|
||
### step3 — WS2812 LEDs + buttons
|
||
|
||
**Goal:** combine inputs and outputs — each button sets a different LED color.
|
||
|
||
```sh
|
||
tinygo flash -target nicenano ./basics/step3
|
||
```
|
||
|
||
- Press **A** → both LEDs turn red.
|
||
- Press **B** → both LEDs turn blue.
|
||
- Press the **rotary button** → both LEDs turn green.
|
||
- Release all → LEDs stay on the last color.
|
||
|
||
**Key concepts**
|
||
- Multiple inputs read in the same loop.
|
||
- State is kept across loop iterations with the `c` variable.
|
||
|
||
---
|
||
|
||
### step3b — Rainbow LEDs
|
||
|
||
**Goal:** generate smooth color transitions (hue wheel) and let buttons scroll through them.
|
||
|
||
The `getRainbowRGB` function maps a `uint8` (0–255) to a point on the RGB color wheel, dividing it into three 85-step segments: red→green, green→blue, blue→red.
|
||
|
||
```sh
|
||
tinygo flash -target nicenano ./basics/step3b
|
||
```
|
||
|
||
- Hold **A** → hue advances (warm colors).
|
||
- Hold **B** → hue recedes (cool colors).
|
||
- The two LEDs are always offset by 10 hue steps apart.
|
||
|
||
**Challenge:** increase the offset between the two LEDs to 128 (opposite colors on the wheel).
|
||
|
||
---
|
||
|
||
### step4 — Display: text
|
||
|
||
**Goal:** initialize the ST7789 display and render text.
|
||
|
||
The display talks over SPI and needs explicit pin configuration on the nice!nano. After configuration, the drawable area is **240 × 135 pixels** in landscape orientation.
|
||
|
||
```go
|
||
package main
|
||
|
||
import (
|
||
"image/color"
|
||
"machine"
|
||
|
||
"tinygo.org/x/drivers/st7789"
|
||
"tinygo.org/x/tinyfont"
|
||
"tinygo.org/x/tinyfont/freesans"
|
||
)
|
||
|
||
func main() {
|
||
machine.SPI0.Configure(machine.SPIConfig{
|
||
SCK: machine.P1_01,
|
||
SDO: machine.P1_02,
|
||
Frequency: 8000000,
|
||
Mode: 0,
|
||
})
|
||
|
||
display := st7789.New(machine.SPI0,
|
||
machine.P1_15, // RST
|
||
machine.P1_13, // DC
|
||
machine.P0_10, // CS
|
||
machine.P0_09) // backlight
|
||
|
||
display.Configure(st7789.Config{
|
||
Rotation: st7789.ROTATION_90,
|
||
Width: 135,
|
||
Height: 240,
|
||
RowOffset: 40,
|
||
ColumnOffset: 53,
|
||
})
|
||
|
||
display.FillScreen(color.RGBA{0, 0, 0, 255})
|
||
|
||
tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 10, 50,
|
||
"Hello", color.RGBA{255, 255, 0, 255})
|
||
tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 10, 90,
|
||
"Gophers!", color.RGBA{255, 0, 255, 255})
|
||
}
|
||
```
|
||
|
||
```sh
|
||
tinygo flash -target nicenano ./basics/step4
|
||
```
|
||
|
||
"Hello" and "Gophers!" appear on the display in yellow and magenta.
|
||
|
||
**Key concepts**
|
||
- `RowOffset` / `ColumnOffset` — the ST7789 physical memory does not always start at (0,0); these offsets align the driver to the actual pixel grid of this display module.
|
||
- `tinyfont.WriteLine(&display, &font, x, y, text, color)` — `x`, `y` are the **baseline** position of the text (not the top-left corner).
|
||
|
||
**Challenge:** change the font to `freesans.Regular9pt7b` and add a third line.
|
||
|
||
---
|
||
|
||
### step5 — Display + buttons
|
||
|
||
**Goal:** update the display in real time based on button state.
|
||
|
||
Three filled circles represent the three buttons. When a button is pressed a ring appears around its circle.
|
||
|
||
```sh
|
||
tinygo flash -target nicenano ./basics/step5
|
||
```
|
||
|
||
- Press **B** (left circle), **rotary** (center), or **A** (right circle) to see the corresponding ring.
|
||
|
||
**Key concepts**
|
||
- `tinydraw.FilledCircle` / `tinydraw.Circle` — drawing primitives from `tinygo.org/x/tinydraw`.
|
||
- Re-drawing a shape in the background color erases it — no need to clear the whole screen.
|
||
|
||
---
|
||
|
||
### step6 — Analog joystick
|
||
|
||
**Goal:** read the joystick's two analog axes and visualize the position on the display.
|
||
|
||
The joystick outputs a voltage proportional to its position on each axis. The ADC converts that voltage to a 16-bit integer (0–65535). The center resting position is approximately 32767.
|
||
|
||
```go
|
||
machine.InitADC()
|
||
ax := machine.ADC{Pin: machine.P0_02} // X axis
|
||
ay := machine.ADC{Pin: machine.P0_29} // Y axis
|
||
ax.Configure(machine.ADCConfig{})
|
||
ay.Configure(machine.ADCConfig{})
|
||
|
||
// map 0-65535 → display width/height
|
||
dotX := int16(uint32(ax.Get()) * 240 / 65535)
|
||
dotY := int16(uint32(ay.Get()) * 135 / 65535)
|
||
```
|
||
|
||
```sh
|
||
tinygo flash -target nicenano ./basics/step6
|
||
```
|
||
|
||
A green dot follows the joystick across the display.
|
||
|
||
**Key concepts**
|
||
- `machine.InitADC()` — must be called once before using any ADC pin.
|
||
- The cast chain `uint32(ax.Get()) * 240 / 65535` avoids integer overflow during the mapping (a plain `int16` multiplication would overflow).
|
||
|
||
---
|
||
|
||
### step7 — Rotary encoder
|
||
|
||
**Goal:** use the quadrature encoder to cycle through hue values on the LEDs.
|
||
|
||
A rotary encoder outputs two square waves (A and B) 90° out of phase. The `encoders` driver decodes the direction and accumulates a signed position value using interrupts.
|
||
|
||
```go
|
||
enc := encoders.NewQuadratureViaInterrupt(machine.P1_00, machine.P0_24)
|
||
enc.Configure(encoders.QuadratureConfig{Precision: 4})
|
||
|
||
// in the loop:
|
||
k = uint8(enc.Position())
|
||
```
|
||
|
||
```sh
|
||
tinygo flash -target nicenano ./basics/step7
|
||
```
|
||
|
||
- Turn the encoder knob → LED hue shifts smoothly.
|
||
- Press the encoder knob → resets position to zero (both LEDs snap back to the same starting hue).
|
||
|
||
**Key concepts**
|
||
- `Precision: 4` — number of encoder pulses per detent; adjust if your encoder feels too coarse or too fine.
|
||
- `enc.Position()` returns a signed `int32`; casting to `uint8` wraps around naturally (256 → 0), which is exactly what we want for the hue wheel.
|
||
|
||
---
|
||
|
||
### step8 — Buzzer
|
||
|
||
**Goal:** generate audio tones by toggling the buzzer pin at audio frequencies.
|
||
|
||
The buzzer is passive: it only makes sound when driven with an alternating signal. We create a tone by toggling the pin HIGH/LOW at the target frequency.
|
||
|
||
```go
|
||
// tone at 1046 Hz ≈ C6
|
||
func tone(freq int) {
|
||
for i := 0; i < 10; i++ {
|
||
bzrPin.High()
|
||
time.Sleep(time.Duration(freq) * time.Microsecond)
|
||
bzrPin.Low()
|
||
time.Sleep(time.Duration(freq) * time.Microsecond)
|
||
}
|
||
}
|
||
```
|
||
|
||
The half-period in microseconds equals `1_000_000 / (2 * freq_Hz)`, but here `freq` is passed directly as the half-period value in microseconds, so `freq=1046` means a half-period of 1046 µs ≈ 478 Hz. Use this table to pick a value:
|
||
|
||
| Note | Approx. half-period (µs) |
|
||
|------|--------------------------|
|
||
| C5 | 523 |
|
||
| F#5 | 739 |
|
||
| C6 | 1046 |
|
||
|
||
```sh
|
||
tinygo flash -target nicenano ./basics/step8
|
||
```
|
||
|
||
Each button plays a different note while held.
|
||
|
||
**Challenge:** compose a short melody by chaining `tone()` calls with `time.Sleep` pauses between them.
|
||
|
||
---
|
||
|
||
### step9 — Serial monitor
|
||
|
||
**Goal:** use `fmt.Println` and `fmt.Printf` to send human-readable events from the badge to your computer over USB serial.
|
||
|
||
The `-monitor` flag tells TinyGo to open the serial port immediately after flashing, so you see the output without extra steps.
|
||
|
||
```go
|
||
// button press (falling-edge detection)
|
||
a := btnA.Get()
|
||
b := btnB.Get()
|
||
c := btnC.Get() // rotary encoder push button
|
||
if !a && prevA {
|
||
println("button A pressed")
|
||
}
|
||
if !b && prevB {
|
||
println("button B pressed")
|
||
}
|
||
if !c && prevC {
|
||
println("encoder button pressed")
|
||
}
|
||
|
||
// rotary encoder — print on every position change
|
||
pos := enc.Position()
|
||
if pos != prevPos {
|
||
println("encoder:", pos)
|
||
prevPos = pos
|
||
}
|
||
|
||
// joystick — print while outside the dead zone
|
||
rawX := int(ax.Get()) - 32767
|
||
rawY := int(ay.Get()) - 32767
|
||
if rawX > deadzone || rawX < -deadzone || rawY > deadzone || rawY < -deadzone {
|
||
println("joystick:", "x=", rawX, "y=", rawY)
|
||
}
|
||
```
|
||
|
||
```sh
|
||
tinygo flash -target nicenano -monitor ./basics/step9
|
||
```
|
||
|
||
Move the joystick, turn the encoder knob, or press A, B, or the encoder button. Each event prints to your terminal in real time.
|
||
|
||
**Key concepts**
|
||
- `println` is a TinyGo built-in that writes directly to the USB serial port with no imports needed — prefer it over `fmt.Print*` in embedded code.
|
||
- `-monitor` keeps the serial connection open after flashing — equivalent to running `tinygo monitor` right after.
|
||
- Falling-edge detection (`!a && prevA`) prints once per press instead of flooding the terminal while the button is held.
|
||
- A dead zone (`const deadzone = 5000`) suppresses joystick noise around the center resting position.
|
||
|
||
---
|
||
|
||
### step10 — USB MIDI
|
||
|
||
**Goal:** make the badge appear as a MIDI instrument over USB.
|
||
|
||
When flashed with this program the badge enumerates as a standard USB MIDI device. Any app or DAW that supports USB MIDI will detect it automatically.
|
||
|
||
```go
|
||
notes := []midi.Note{midi.C4, midi.E4, midi.G4}
|
||
midichannel := uint8(1)
|
||
|
||
// on button press:
|
||
midi.Midi.NoteOn(0, midichannel, notes[note], 50)
|
||
// on button release:
|
||
midi.Midi.NoteOff(0, midichannel, notes[oldNote], 50)
|
||
```
|
||
|
||
```sh
|
||
tinygo flash -target nicenano ./basics/step10
|
||
```
|
||
|
||
Open any online MIDI player (e.g. [muted.io/piano](https://muted.io/piano/)) or connect to a DAW. The three buttons play C4, E4, and G4 (a C major triad).
|
||
|
||
**Key concepts**
|
||
- MIDI NoteOn/NoteOff must be paired: always send NoteOff for the previous note before sending a new NoteOn, otherwise notes get stuck.
|
||
- Velocity (last parameter, `50`) controls how hard the note is "hit" (0–127).
|
||
|
||
**Challenge:** map the rotary encoder position to an octave shift (transpose all notes up or down by 12 semitones per detent).
|
||
|
||
---
|
||
|
||
### step11 — USB HID mouse
|
||
|
||
**Goal:** use the joystick as a mouse pointer and buttons as mouse clicks.
|
||
|
||
The ADC center resting position (~32767) produces a raw offset of 0. A dead zone filters out the natural jitter around center so the cursor stays still when you're not touching the stick.
|
||
|
||
```go
|
||
const DEADZONE = 5000
|
||
|
||
rawX := int(ax.Get()) - 32767
|
||
var dx int
|
||
if rawX > DEADZONE || rawX < -DEADZONE {
|
||
dx = rawX / 2048
|
||
}
|
||
mouseDevice.Move(dx, dy)
|
||
```
|
||
|
||
```sh
|
||
tinygo flash -target nicenano ./basics/step11
|
||
```
|
||
|
||
Connect the badge to a computer. The joystick moves the mouse cursor; button A is left click, button B is right click.
|
||
|
||
**Key concepts**
|
||
- The dead zone prevents cursor drift when the joystick is at rest.
|
||
- `rawX / 2048` scales down the ±32767 range to ±16, giving a comfortable cursor speed. Decrease the divisor for faster movement.
|
||
|
||
---
|
||
|
||
## BLE
|
||
|
||
The nice!nano's nRF52840 chip has built-in Bluetooth Low Energy. These examples use the [`github.com/tinygo-org/bluetooth`](https://github.com/tinygo-org/bluetooth) library.
|
||
|
||
**Recommended mobile apps**
|
||
|
||
| App | Platform | Best for |
|
||
|-----|----------|----------|
|
||
| nRF Connect | iOS / Android | Inspecting services, reading/writing characteristics |
|
||
| nRF Toolbox | iOS / Android | Nordic UART Service (NUS) terminal |
|
||
| Serial Bluetooth Terminal | Android | NUS text terminal |
|
||
| LightBlue | iOS / Android | Browsing and writing custom characteristics |
|
||
|
||
---
|
||
|
||
### BLE concepts
|
||
|
||
Before diving in, a few terms:
|
||
|
||
- **Peripheral** — the badge; it advertises its presence and waits for connections.
|
||
- **Central** — the mobile phone or computer that initiates the connection.
|
||
- **Service** — a logical grouping of related data (identified by a UUID).
|
||
- **Characteristic** — a single data value within a service. Can be readable, writable, and/or notify-able.
|
||
- **Notification** — the peripheral pushes a new value to the central without the central polling.
|
||
- **UUID** — 128-bit identifier for services and characteristics. Custom UUIDs are usually 128-bit; standard Bluetooth ones are 16-bit.
|
||
|
||
---
|
||
|
||
### BLE step1 — Counter with display
|
||
|
||
**Goal:** advertise a BLE service, send periodic notifications, and display connection status.
|
||
|
||
This example implements the **Nordic UART Service (NUS)** — a de-facto standard for sending text over BLE, supported by many apps out of the box.
|
||
|
||
**Service:** `6E400001-B5A3-F393-E0A9-E50E24DCCA9E`
|
||
| Characteristic | UUID | Properties | Role |
|
||
|---|---|---|---|
|
||
| RX | `6E400002-…` | Write, WriteWithoutResponse | Central → Badge |
|
||
| TX | `6E400003-…` | Notify, Read | Badge → Central |
|
||
|
||
The counter increments every second. `txChar.Write()` sends a notification to any subscribed central. If `Write` returns an error no device is listening — that is how connection state is tracked.
|
||
|
||
```go
|
||
_, err := txChar.Write([]byte(strconv.Itoa(counter) + "\n"))
|
||
connected = err == nil
|
||
```
|
||
|
||
```sh
|
||
tinygo flash -target nicenano ./ble/step1
|
||
```
|
||
|
||
**How to test**
|
||
1. Flash the badge. The display shows `BLE: Advertising...`.
|
||
2. Open **nRF Toolbox** → UART → Connect → search for `NiceBadge`.
|
||
3. Once connected the display shows `BLE: Connected` and the counter appears in the terminal.
|
||
4. Type `reset` and send it — the counter resets to zero.
|
||
|
||
**Key concepts**
|
||
- `adapter.Enable()` — starts the BLE stack (SoftDevice on nRF52840). Must be called before anything else.
|
||
- `adapter.AddService` — registers the GATT service and its characteristics.
|
||
- `adv.Start()` — begins advertising; the badge is now discoverable.
|
||
- The `WriteEvent` callback runs when the central writes to a characteristic. It runs in a BLE interrupt context — keep it short.
|
||
|
||
---
|
||
|
||
### BLE step2 — LED color control
|
||
|
||
**Goal:** receive data from a mobile app and use it to set the LED color.
|
||
|
||
A custom service exposes a single writable characteristic. The central writes 3 bytes `[R, G, B]`; the badge lights both WS2812 LEDs immediately and shows the color + RGB values on the display.
|
||
|
||
**Service:** `BADA5501-B5A3-F393-E0A9-E50E24DCCA9E`
|
||
| Characteristic | UUID | Properties | Role |
|
||
|---|---|---|---|
|
||
| LED Color | `BADA5502-…` | Write, WriteWithoutResponse | Central → Badge |
|
||
|
||
```go
|
||
WriteEvent: func(client bluetooth.Connection, offset int, value []byte) {
|
||
if len(value) < 3 {
|
||
return
|
||
}
|
||
ledColor = color.RGBA{value[0], value[1], value[2], 255}
|
||
setLEDs(ledColor)
|
||
drawColor(ledColor)
|
||
},
|
||
```
|
||
|
||
After all services are registered the main goroutine simply blocks with `select {}` — all activity is driven by the BLE callback.
|
||
|
||
```sh
|
||
tinygo flash -target nicenano ./ble/step2
|
||
```
|
||
|
||
**How to test**
|
||
1. Flash and open **nRF Connect** (or LightBlue).
|
||
2. Connect to `NiceBadge` and expand the custom service (`BADA5501…`).
|
||
3. Write to the color characteristic. In nRF Connect, enter raw bytes in hex: `FF0000` = red, `00FF00` = green, `0000FF` = blue, `FF0080` = pink.
|
||
4. The LEDs and display update instantly.
|
||
|
||
**Key concepts**
|
||
- Custom 128-bit UUIDs let you define entirely private services not shared with any standard profile.
|
||
- `select {}` is idiomatic Go for blocking forever; it is more explicit than `for {}` with a sleep.
|
||
- Always validate `len(value)` in `WriteEvent` — a malformed write should not panic.
|
||
|
||
---
|
||
|
||
### BLE step3 — Scanner
|
||
|
||
**Goal:** put the radio in observer mode and display nearby BLE devices.
|
||
|
||
In scanner mode the badge does not advertise — it only listens. `adapter.Scan` is a blocking call, so it runs in a goroutine while the main loop updates the display every 500 ms.
|
||
|
||
```go
|
||
go func() {
|
||
adapter.Scan(func(a *bluetooth.Adapter, result bluetooth.ScanResult) {
|
||
name := result.LocalName()
|
||
if name == "" {
|
||
name = result.Address.String()
|
||
}
|
||
// deduplicate by address, update RSSI
|
||
})
|
||
}()
|
||
```
|
||
|
||
RSSI (Received Signal Strength Indicator) is expressed in dBm — closer to 0 is stronger. The display colors devices by signal quality:
|
||
|
||
| RSSI | Color | Meaning |
|
||
|------|-------|---------|
|
||
| > −60 dBm | green | Strong (< ~3 m) |
|
||
| −60 to −80 dBm | yellow | Medium |
|
||
| < −80 dBm | white | Weak |
|
||
|
||
```sh
|
||
tinygo flash -target nicenano ./ble/step3
|
||
```
|
||
|
||
Up to 5 nearby devices are listed by name and signal strength. Press **button A** to clear the list and start fresh.
|
||
|
||
**Key concepts**
|
||
- `result.LocalName()` returns the advertised name, if any. Devices that don't advertise a name are identified by their MAC address.
|
||
- TinyGo uses a cooperative scheduler — goroutines yield at blocking calls (`Scan`, `Sleep`, channel ops). Shared variables accessed from both the goroutine and the main loop are safe here because the scheduler is cooperative, but in general you should use channels or atomics.
|
||
|
||
---
|
||
|
||
## Next steps
|
||
|
||
- **Snake game tutorial** — coming soon.
|
||
- **Examples** (thermal camera, CO2 sensor, rubber duck) — see [`tutorial/examples/`](examples/).
|
||
- Combine what you learned: use the rotary encoder to scroll through a BLE device list, or send joystick data over NUS to a web app.
|