added tutorial and examples

This commit is contained in:
Daniel Esteban 2026-04-18 13:01:06 +02:00
parent f4bdf4f5ad
commit 3cbc50cd3e
22 changed files with 1453 additions and 12 deletions

View file

@ -35,7 +35,10 @@ nicebadge/
├── hardware/ # KiCad PCB design files
│ └── PCB-kicad/
│ └── production/ # Gerbers, BOM, pick-and-place files
└── tutorials/ # Step-by-step examples (coming soon)
└── tutorial/ # TinyGo tutorials and examples
├── basics/ # Step-by-step basics (step0 → step10)
├── ble/ # Bluetooth Low Energy examples
└── examples/ # Standalone examples (sensors, HID, etc.)
```
---
@ -94,17 +97,59 @@ Production files (Gerbers, BOM, positions) ready for fabrication are in [hardwar
## Tutorials & examples
The tutorial series walks you through every peripheral of the badge, starting from blinking an LED and ending with a Bluetooth HID device:
All code lives under [`tutorial/`](tutorial/). Flash any step from inside that directory:
1. Hello, Badge! — blink the RGB LEDs
2. Drawing on the display — TinyGo + TinyDraw
3. Reading buttons and the joystick
4. Playing sounds with the buzzer
5. I2C expansion via StemmQT
6. Going wireless — BLE advertisements and GATT services
7. USB HID — turn the badge into a keyboard
```sh
cd tutorial
tinygo flash -target nicenano ./basics/step0
```
*(Tutorials are coming soon — contributions welcome!)*
Run `go mod tidy` once inside `tutorial/` to fetch all dependencies.
---
### Basics
A progressive series that introduces every peripheral one at a time.
| Step | What it does |
|------|--------------|
| [step0](tutorial/basics/step0/) | Blink the built-in LED — verifies the flash toolchain works |
| [step1](tutorial/basics/step1/) | Built-in LED controlled by button A |
| [step2](tutorial/basics/step2/) | WS2812 RGB LEDs alternating red and green |
| [step3](tutorial/basics/step3/) | WS2812 LEDs change color with buttons A, B, and rotary |
| [step3b](tutorial/basics/step3b/) | Rainbow cycle on the LEDs, A/B scroll through the hue |
| [step4](tutorial/basics/step4/) | "Hello Gophers!" text on the display |
| [step5](tutorial/basics/step5/) | Display shows a circle per button; rings appear on press |
| [step6](tutorial/basics/step6/) | Analog joystick — a dot follows the stick on the display |
| [step7](tutorial/basics/step7/) | Rotary encoder — turning cycles LED colors, push resets |
| [step8](tutorial/basics/step8/) | Passive buzzer plays a note per button press |
| [step9](tutorial/basics/step9/) | USB MIDI — badge sends notes C4 / E4 / G4 to any DAW |
| [step10](tutorial/basics/step10/) | USB HID mouse — joystick moves the cursor, A/B click |
---
### BLE
Bluetooth Low Energy examples using the nRF52840 on the nice!nano. Compatible apps: **nRF Connect**, **nRF Toolbox**, **Serial Bluetooth Terminal**, **LightBlue**.
| Step | What it does |
|------|--------------|
| [step1](tutorial/ble/step1/) | **Counter** — advertises as Nordic UART Service (NUS), sends an incrementing counter via BLE notifications every second; display shows connection status and current value; send `reset` from the app to restart the count |
| [step2](tutorial/ble/step2/) | **LED color control** — mobile writes 3 bytes (R, G, B) to a custom characteristic; both WS2812 LEDs and the display update immediately |
| [step3](tutorial/ble/step3/) | **Scanner** — lists nearby BLE devices with name and RSSI, color-coded by signal strength; button A clears the list |
---
### Examples
Standalone, feature-complete programs.
| Example | What it does | Extra hardware |
|---------|--------------|----------------|
| [thermal-camera](tutorial/examples/thermal-camera/) | AMG88xx 8×8 IR sensor upscaled with bilinear interpolation and rendered with an iron-palette color map on the full display | AMG88xx sensor on I2C1 (SDA=P0_17, SCL=P0_20) |
| [rubber-duck](tutorial/examples/rubber-duck/) | USB HID keyboard automation (Ubuntu/GNOME target): opens a text editor, types a message, and launches a URL. Press button A to trigger | — |
| [co2-sensor](tutorial/examples/co2-sensor/) | SCD4x CO2/temperature/humidity sensor: display background turns green/yellow/red by CO2 level, LEDs mirror the color, buzzer warns above 1500 ppm | SCD4x sensor on I2C1 (SDA=P0_17, SCL=P0_20) |
---

169
examples/co2-sensor/main.go Normal file
View file

@ -0,0 +1,169 @@
package main
// CO2 sensor example using SCD4x (I2C).
// Connect the sensor to I2C1: SDA=P0_17, SCL=P0_20 (3.3V power).
//
// The display background changes color based on CO2 level:
// green → < 800 ppm (good air quality)
// yellow → < 1500 ppm (ventilate soon)
// red → ≥ 1500 ppm (ventilate now!)
//
// The WS2812 LEDs mirror the same color, and the buzzer sounds a
// warning tone when CO2 exceeds the danger threshold.
import (
"image/color"
"machine"
"strconv"
"time"
"tinygo.org/x/drivers/scd4x"
"tinygo.org/x/drivers/st7789"
"tinygo.org/x/drivers/ws2812"
"tinygo.org/x/tinyfont"
"tinygo.org/x/tinyfont/freesans"
)
var (
display st7789.Device
leds ws2812.Device
bzrPin machine.Pin
co2dev *scd4x.Device
)
func main() {
time.Sleep(3 * time.Second)
machine.SPI0.Configure(machine.SPIConfig{
SCK: machine.P1_01,
SDO: machine.P1_02,
Frequency: 8000000,
Mode: 0,
})
machine.I2C1.Configure(machine.I2CConfig{
SDA: machine.P0_17,
SCL: machine.P0_20,
Frequency: 400000,
})
display = st7789.New(machine.SPI0,
machine.P1_15, // TFT_RESET
machine.P1_13, // TFT_DC
machine.P0_10, // TFT_CS
machine.P0_09) // TFT_LITE
display.Configure(st7789.Config{
Rotation: st7789.ROTATION_90,
Width: 135,
Height: 240,
RowOffset: 40,
ColumnOffset: 53,
})
neo := machine.P1_11
neo.Configure(machine.PinConfig{Mode: machine.PinOutput})
leds = ws2812.New(neo)
bzrPin = machine.P0_31
bzrPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
co2dev = scd4x.New(machine.I2C1)
co2dev.Configure()
display.FillScreen(color.RGBA{0, 0, 0, 255})
if err := co2dev.StartPeriodicMeasurement(); err != nil {
println(err)
}
var (
co2 int32
temp int32
hum int32
err error
bg color.RGBA
oldbg = color.RGBA{R: 0xff, G: 0xff, B: 0xff}
black = color.RGBA{0, 0, 0, 255}
)
for {
// SCD4x needs up to 5s between measurements; retry a few times
for i := 0; i < 5; i++ {
co2, err = co2dev.ReadCO2()
temp, _ = co2dev.ReadTemperature()
hum, _ = co2dev.ReadHumidity()
if err != nil {
println(err)
}
if co2 != 0 {
break
}
time.Sleep(200 * time.Millisecond)
}
switch {
case co2 < 800:
bg = color.RGBA{0, 220, 0, 255}
case co2 < 1500:
bg = color.RGBA{220, 220, 0, 255}
default:
bg = color.RGBA{220, 0, 0, 255}
}
if bg != oldbg {
display.FillScreen(bg)
oldbg = bg
}
// mirror CO2 status on the LEDs
leds.WriteColors([]color.RGBA{bg, bg})
// buzzer warning when CO2 is dangerously high
if co2 >= 1500 {
warn()
}
// display 240x135, text layout:
// y=40 CO2 level (most prominent)
// y=75 Temperature
// y=110 Humidity
tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 10, 40, "CO2: "+strconv.Itoa(int(co2))+" ppm", black)
tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 10, 75, "Temp: "+formatMilliC(temp), black)
tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 10, 110, "Hum: "+formatMilliPct(hum), black)
time.Sleep(time.Second)
// erase text by redrawing in background color (avoids full FillScreen flicker)
tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 10, 40, "CO2: "+strconv.Itoa(int(co2))+" ppm", bg)
tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 10, 75, "Temp: "+formatMilliC(temp), bg)
tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 10, 110, "Hum: "+formatMilliPct(hum), bg)
}
}
// formatMilliC formats a millidegree Celsius value as "XX.X C".
func formatMilliC(mc int32) string {
if mc < 0 {
mc = -mc
return "-" + strconv.Itoa(int(mc/1000)) + "." + strconv.Itoa(int((mc%1000)/100)) + " C"
}
return strconv.Itoa(int(mc/1000)) + "." + strconv.Itoa(int((mc%1000)/100)) + " C"
}
// formatMilliPct formats a millipercent value as "XX.X %".
func formatMilliPct(mp int32) string {
return strconv.Itoa(int(mp/1000)) + "." + strconv.Itoa(int((mp%1000)/100)) + " %"
}
// warn sounds two short beeps on the buzzer.
func warn() {
for beep := 0; beep < 2; beep++ {
for i := 0; i < 100; i++ {
bzrPin.High()
time.Sleep(500 * time.Microsecond)
bzrPin.Low()
time.Sleep(500 * time.Microsecond)
}
time.Sleep(200 * time.Millisecond)
}
}

View file

@ -0,0 +1,83 @@
package main
// Rubber Duck attack - USB HID keyboard automation.
// The badge presents itself as a USB keyboard and executes a sequence of
// keystrokes on the connected computer.
//
// TARGETS UBUNTU/GNOME. For other platforms adjust the key sequences.
// NOTE: keyboard scan codes are US layout; non-US layouts may produce
// different characters for symbols (/, -, :, etc.).
//
// Press button A to trigger the attack. The badge waits on startup
// so you have time to plug it in safely.
import (
"machine"
"machine/usb/hid/keyboard"
"time"
)
func main() {
// wait for USB enumeration
time.Sleep(2 * time.Second)
// button A arms the attack — only runs when pressed
btnA := machine.P1_06
btnA.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
kb := keyboard.Port()
for {
if !btnA.Get() {
runAttack(kb)
// only run once per press
for !btnA.Get() {
time.Sleep(50 * time.Millisecond)
}
}
time.Sleep(10 * time.Millisecond)
}
}
func runAttack(kb keyboard.Device) {
// open application launcher (Super key on Ubuntu/GNOME)
kb.Down(keyboard.KeyLeftGUI)
time.Sleep(time.Second)
kb.Up(keyboard.KeyLeftGUI)
time.Sleep(500 * time.Millisecond)
// search for text editor
kb.Write([]byte("text"))
time.Sleep(1500 * time.Millisecond)
kb.Press(keyboard.KeyEnter)
time.Sleep(time.Second)
// type the ominous message
kb.Write([]byte("Please wait while you are being hacked"))
time.Sleep(2 * time.Second)
// open run dialog (Alt+F2 on GNOME) and launch xdg-open
// NOTE: symbols below assume US keyboard layout
kb.Down(keyboard.KeyLeftAlt)
kb.Down(keyboard.KeyF2)
time.Sleep(time.Second)
kb.Up(keyboard.KeyF2)
kb.Up(keyboard.KeyLeftAlt)
time.Sleep(time.Second)
kb.Write([]byte("xdg"))
kb.Press(keyboard.KeypadMinus)
kb.Write([]byte("open https>"))
kb.Press(keyboard.KeypadSlash)
kb.Press(keyboard.KeypadSlash)
kb.Write([]byte("www.youtube.com"))
kb.Press(keyboard.KeypadSlash)
kb.Write([]byte("watch_v)dQw4w9WgXcQ"))
kb.Press(keyboard.KeyEnter)
// turn the volume up
time.Sleep(500 * time.Millisecond)
for i := 0; i < 12; i++ {
kb.Press(keyboard.KeyMediaVolumeInc)
}
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,102 @@
package main
// Thermal camera example using AMG88xx 8x8 IR sensor.
// Connect the sensor to I2C1: SDA=P0_17, SCL=P0_20 (3.3V power).
//
// The 8x8 sensor data is scaled to 24x24 with bilinear interpolation and
// rendered with an iron-palette color map on the 240x135 display.
// Each scaled pixel is drawn as a 10x5 px block → 240x120 total, centered.
import (
"image"
"machine"
draw2 "golang.org/x/image/draw"
"tinygo.org/x/drivers/amg88xx"
"tinygo.org/x/drivers/st7789"
)
const (
sensorSize = 8 // AMG88xx is 8x8
scaledSize = 24 // intermediate bilinear-scaled image
blockW = 10 // px per scaled pixel (horizontal): 24*10 = 240
blockH = 5 // px per scaled pixel (vertical): 24*5 = 120
yOffset = 7 // center 120px vertically in 135px display: (135-120)/2
)
var (
display st7789.Device
data [sensorSize * sensorSize]int16
)
func main() {
machine.SPI0.Configure(machine.SPIConfig{
SCK: machine.P1_01,
SDO: machine.P1_02,
Frequency: 8000000,
Mode: 0,
})
machine.I2C1.Configure(machine.I2CConfig{
SDA: machine.P0_17,
SCL: machine.P0_20,
Frequency: 400000,
})
display = st7789.New(machine.SPI0,
machine.P1_15, // TFT_RESET
machine.P1_13, // TFT_DC
machine.P0_10, // TFT_CS
machine.P0_09) // TFT_LITE
display.Configure(st7789.Config{
Rotation: st7789.ROTATION_90,
Width: 135,
Height: 240,
RowOffset: 40,
ColumnOffset: 53,
})
camera := amg88xx.New(machine.I2C1)
camera.Configure(amg88xx.Config{})
src := image.NewRGBA(image.Rect(0, 0, sensorSize, sensorSize))
dst := image.NewRGBA(image.Rect(0, 0, scaledSize, scaledSize))
for {
camera.ReadPixels(&data)
// map each sensor pixel to a palette color
for j := 0; j < sensorSize; j++ {
for i := 0; i < sensorSize; i++ {
v := data[63-(i+j*sensorSize)]
// clamp to 18°C33°C range → index 0432
if v < 18000 {
v = 0
} else {
v = (v - 18000) / 36
if v > 432 {
v = 432
}
}
src.Set(i, j, colors[v])
}
}
// bilinear upscale 8x8 → 24x24
draw2.BiLinear.Scale(dst, dst.Bounds(), src, src.Bounds(), draw2.Over, nil)
// draw horizontally mirrored (acts as a selfie mirror)
for j := 0; j < scaledSize; j++ {
for i := 0; i < scaledSize; i++ {
display.FillRectangle(
int16((scaledSize-1-i)*blockW),
yOffset+int16(j)*blockH,
blockW, blockH,
dst.RGBAAt(i, j),
)
}
}
}
}

View file

@ -0,0 +1,29 @@
package main
import (
"machine"
"time"
)
func main() {
// get the built-in LED pin on the nice!nano board
led := machine.LED
// configure the LED pin for output
led.Configure(machine.PinConfig{Mode: machine.PinOutput})
for {
// turn off the LED
led.Low()
// wait 500 ms
time.Sleep(time.Millisecond * 500)
// turn on the LED
led.High()
// wait 500 ms
time.Sleep(time.Millisecond * 500)
}
}

View file

@ -0,0 +1,27 @@
package main
import (
"machine"
"time"
)
func main() {
led := machine.LED
led.Configure(machine.PinConfig{Mode: machine.PinOutput})
// button A is on pin P1_06, active LOW (internal pull-up)
btnA := machine.P1_06
btnA.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
for {
// button reads LOW when pressed
if !btnA.Get() {
led.High()
} else {
led.Low()
}
time.Sleep(time.Millisecond * 10)
}
}

View file

@ -0,0 +1,52 @@
package main
import (
"machine"
"machine/usb/hid/mouse"
"time"
)
const DEADZONE = 5000
func main() {
// joystick analog axes
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{})
// A = left click, B = right click
btnA := machine.P1_06
btnB := machine.P1_04
btnA.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
btnB.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
mouseDevice := mouse.Port()
for {
// ADC center is ~32767; compute offset from center
rawX := int(ax.Get()) - 32767
rawY := int(ay.Get()) - 32767
var dx, dy int
if rawX > DEADZONE || rawX < -DEADZONE {
dx = rawX / 2048
}
if rawY > DEADZONE || rawY < -DEADZONE {
dy = rawY / 2048
}
mouseDevice.Move(dx, dy)
if !btnA.Get() {
mouseDevice.Click(mouse.Left)
}
if !btnB.Get() {
mouseDevice.Click(mouse.Right)
}
time.Sleep(100 * time.Millisecond)
}
}

View file

@ -0,0 +1,42 @@
package main
import (
"image/color"
"machine"
"time"
"tinygo.org/x/drivers/ws2812"
)
const (
NumberOfLEDs = 2
)
var (
red = color.RGBA{255, 0, 0, 255}
green = color.RGBA{0, 255, 0, 255}
)
func main() {
// WS2812 LEDs are on pin P1_11
neo := machine.P1_11
neo.Configure(machine.PinConfig{Mode: machine.PinOutput})
leds := ws2812.New(neo)
ledColors := make([]color.RGBA, NumberOfLEDs)
rg := false
for {
for i := 0; i < NumberOfLEDs; i++ {
if rg {
ledColors[i] = red
} else {
ledColors[i] = green
}
rg = !rg
}
leds.WriteColors(ledColors)
rg = !rg
time.Sleep(time.Millisecond * 300)
}
}

View file

@ -0,0 +1,69 @@
package main
import (
"image/color"
"machine"
"time"
"tinygo.org/x/drivers/ws2812"
)
const (
Red = iota
Green
Blue
Yellow
Cyan
Purple
White
Off
)
var colors = [...]color.RGBA{
{255, 0, 0, 255}, // Red
{0, 255, 0, 255}, // Green
{0, 0, 255, 255}, // Blue
{255, 255, 0, 255}, // Yellow
{0, 255, 255, 255}, // Cyan
{255, 0, 255, 255}, // Purple
{255, 255, 255, 255}, // White
{0, 0, 0, 255}, // Off
}
func main() {
// WS2812 LEDs are on pin P1_11
neo := machine.P1_11
neo.Configure(machine.PinConfig{Mode: machine.PinOutput})
leds := ws2812.New(neo)
ledColors := make([]color.RGBA, 2)
// buttons: active LOW (internal pull-up)
btnA := machine.P1_06
btnB := machine.P1_04
btnRot := machine.P0_22 // rotary encoder push button
btnA.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
btnB.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
btnRot.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
c := Off
for {
if !btnA.Get() {
c = Red
}
if !btnB.Get() {
c = Blue
}
if !btnRot.Get() {
c = Green
}
for i := range ledColors {
ledColors[i] = colors[c]
}
leds.WriteColors(ledColors)
time.Sleep(30 * time.Millisecond)
}
}

View file

@ -0,0 +1,52 @@
package main
import (
"image/color"
"machine"
"time"
"tinygo.org/x/drivers/ws2812"
)
func main() {
// WS2812 LEDs are on pin P1_11
neo := machine.P1_11
neo.Configure(machine.PinConfig{Mode: machine.PinOutput})
leds := ws2812.New(neo)
ledColors := make([]color.RGBA, 2)
// A cycles forward, B cycles backward through the rainbow
btnA := machine.P1_06
btnB := machine.P1_04
btnA.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
btnB.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
var k uint8
for {
if !btnA.Get() {
k++
}
if !btnB.Get() {
k--
}
ledColors[0] = getRainbowRGB(k)
ledColors[1] = getRainbowRGB(k + 10)
leds.WriteColors(ledColors)
time.Sleep(10 * time.Millisecond)
}
}
func getRainbowRGB(i uint8) color.RGBA {
if i < 85 {
return color.RGBA{i * 3, 255 - i*3, 0, 255}
} else if i < 170 {
i -= 85
return color.RGBA{255 - i*3, 0, i * 3, 255}
}
i -= 170
return color.RGBA{0, i * 3, 255 - i*3, 255}
}

View file

@ -0,0 +1,40 @@
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, // TFT_RESET
machine.P1_13, // TFT_DC
machine.P0_10, // TFT_CS
machine.P0_09) // TFT_LITE
display.Configure(st7789.Config{
Rotation: st7789.ROTATION_90,
Width: 135,
Height: 240,
RowOffset: 40,
ColumnOffset: 53,
})
// clear the screen to black
display.FillScreen(color.RGBA{0, 0, 0, 255})
// the display is 240x135 pixels in landscape orientation
tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 10, 50, "Hello", color.RGBA{R: 255, G: 255, B: 0, A: 255})
tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 10, 90, "Gophers!", color.RGBA{R: 255, G: 0, B: 255, A: 255})
}

View file

@ -0,0 +1,72 @@
package main
import (
"image/color"
"machine"
"time"
"tinygo.org/x/drivers/st7789"
"tinygo.org/x/tinydraw"
)
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, // TFT_RESET
machine.P1_13, // TFT_DC
machine.P0_10, // TFT_CS
machine.P0_09) // TFT_LITE
display.Configure(st7789.Config{
Rotation: st7789.ROTATION_90,
Width: 135,
Height: 240,
RowOffset: 40,
ColumnOffset: 53,
})
// buttons: active LOW (internal pull-up)
btnA := machine.P1_06
btnB := machine.P1_04
btnRot := machine.P0_22 // rotary encoder push button
btnA.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
btnB.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
btnRot.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
white := color.RGBA{255, 255, 255, 255}
circle := color.RGBA{0, 100, 250, 255}
ring := color.RGBA{200, 0, 0, 255}
display.FillScreen(white)
// draw a circle for each button on the 240x135 display
tinydraw.FilledCircle(&display, 60, 67, 14, circle) // B (left)
tinydraw.FilledCircle(&display, 120, 67, 14, circle) // rotary button (center)
tinydraw.FilledCircle(&display, 180, 67, 14, circle) // A (right)
for {
if !btnB.Get() {
tinydraw.Circle(&display, 60, 67, 16, ring)
} else {
tinydraw.Circle(&display, 60, 67, 16, white)
}
if !btnRot.Get() {
tinydraw.Circle(&display, 120, 67, 16, ring)
} else {
tinydraw.Circle(&display, 120, 67, 16, white)
}
if !btnA.Get() {
tinydraw.Circle(&display, 180, 67, 16, ring)
} else {
tinydraw.Circle(&display, 180, 67, 16, white)
}
time.Sleep(50 * time.Millisecond)
}
}

View file

@ -0,0 +1,63 @@
package main
import (
"image/color"
"machine"
"time"
"tinygo.org/x/drivers/st7789"
"tinygo.org/x/tinydraw"
)
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, // TFT_RESET
machine.P1_13, // TFT_DC
machine.P0_10, // TFT_CS
machine.P0_09) // TFT_LITE
display.Configure(st7789.Config{
Rotation: st7789.ROTATION_90,
Width: 135,
Height: 240,
RowOffset: 40,
ColumnOffset: 53,
})
// joystick analog axes
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{})
black := color.RGBA{0, 0, 0, 255}
dot := color.RGBA{0, 255, 100, 255}
display.FillScreen(black)
var prevX, prevY int16
for {
// ADC returns 0-65535; map to display dimensions (240x135)
dotX := int16(uint32(ax.Get()) * 240 / 65535)
dotY := int16(uint32(ay.Get()) * 135 / 65535)
// erase previous dot
tinydraw.FilledCircle(&display, prevX, prevY, 5, black)
// draw new dot
tinydraw.FilledCircle(&display, dotX, dotY, 5, dot)
prevX = dotX
prevY = dotY
time.Sleep(30 * time.Millisecond)
}
}

View file

@ -0,0 +1,54 @@
package main
import (
"image/color"
"machine"
"time"
"tinygo.org/x/drivers/encoders"
"tinygo.org/x/drivers/ws2812"
)
func main() {
// WS2812 LEDs are on pin P1_11
neo := machine.P1_11
neo.Configure(machine.PinConfig{Mode: machine.PinOutput})
leds := ws2812.New(neo)
ledColors := make([]color.RGBA, 2)
// rotary encoder: A=P1_00, B=P0_24; push button=P0_22
enc := encoders.NewQuadratureViaInterrupt(machine.P1_00, machine.P0_24)
enc.Configure(encoders.QuadratureConfig{Precision: 4})
btnRot := machine.P0_22
btnRot.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
var k uint8
for {
// pressing the rotary button resets position to zero
if !btnRot.Get() {
enc.SetPosition(0)
}
// encoder position cycles through the rainbow
k = uint8(enc.Position())
ledColors[0] = getRainbowRGB(k)
ledColors[1] = getRainbowRGB(k + 85)
leds.WriteColors(ledColors)
time.Sleep(20 * time.Millisecond)
}
}
func getRainbowRGB(i uint8) color.RGBA {
if i < 85 {
return color.RGBA{i * 3, 255 - i*3, 0, 255}
} else if i < 170 {
i -= 85
return color.RGBA{255 - i*3, 0, i * 3, 255}
}
i -= 170
return color.RGBA{0, i * 3, 255 - i*3, 255}
}

View file

@ -0,0 +1,46 @@
package main
import (
"machine"
"time"
)
var bzrPin machine.Pin
var btnA, btnB, btnRot machine.Pin
func main() {
// buttons: active LOW (internal pull-up)
btnA = machine.P1_06
btnB = machine.P1_04
btnRot = machine.P0_22 // rotary encoder push button
btnA.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
btnB.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
btnRot.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
// passive buzzer on P0_31
bzrPin = machine.P0_31
bzrPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
for {
if !btnA.Get() {
tone(1046) // C6
}
if !btnB.Get() {
tone(739) // F#5
}
if !btnRot.Get() {
tone(523) // C5
}
}
}
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)
}
}

View file

@ -0,0 +1,49 @@
package main
import (
"machine"
"machine/usb/adc/midi"
"time"
)
func main() {
// buttons: active LOW (internal pull-up)
btnA := machine.P1_06
btnB := machine.P1_04
btnRot := machine.P0_22 // rotary encoder push button
btnA.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
btnB.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
btnRot.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
// C major triad: C4, E4, G4
notes := []midi.Note{midi.C4, midi.E4, midi.G4}
midichannel := uint8(1)
note := -1
oldNote := -1
for {
note = -1
if !btnA.Get() {
note = 0 // C4
}
if !btnB.Get() {
note = 1 // E4
}
if !btnRot.Get() {
note = 2 // G4
}
if note != oldNote {
if oldNote != -1 {
midi.Midi.NoteOff(0, midichannel, notes[oldNote], 50)
}
if note != -1 {
midi.Midi.NoteOn(0, midichannel, notes[note], 50)
}
oldNote = note
}
time.Sleep(100 * time.Millisecond)
}
}

145
tutorial/ble/step1/main.go Normal file
View file

@ -0,0 +1,145 @@
package main
// BLE counter example using the Nordic UART Service (NUS).
// Compatible apps: nRF Toolbox, Serial Bluetooth Terminal (Android/iOS).
// - Subscribe to TX notifications to receive the counter value every second.
// - Send "reset" to the RX characteristic to reset the counter to zero.
import (
"image/color"
"machine"
"strconv"
"time"
"github.com/tinygo-org/bluetooth"
"tinygo.org/x/drivers/st7789"
"tinygo.org/x/tinyfont"
"tinygo.org/x/tinyfont/freesans"
)
// Nordic UART Service UUIDs (6E400001-B5A3-F393-E0A9-E50E24DCCA9E)
var (
adapter = bluetooth.DefaultAdapter
serviceUUID = bluetooth.NewUUID([16]byte{
0x6E, 0x40, 0x00, 0x01, 0xB5, 0xA3, 0xF3, 0x93,
0xE0, 0xA9, 0xE5, 0x0E, 0x24, 0xDC, 0xCA, 0x9E,
})
rxUUID = bluetooth.NewUUID([16]byte{
0x6E, 0x40, 0x00, 0x02, 0xB5, 0xA3, 0xF3, 0x93,
0xE0, 0xA9, 0xE5, 0x0E, 0x24, 0xDC, 0xCA, 0x9E,
})
txUUID = bluetooth.NewUUID([16]byte{
0x6E, 0x40, 0x00, 0x03, 0xB5, 0xA3, 0xF3, 0x93,
0xE0, 0xA9, 0xE5, 0x0E, 0x24, 0xDC, 0xCA, 0x9E,
})
)
var (
display st7789.Device
connected bool
counter int
)
var (
black = color.RGBA{0, 0, 0, 255}
cyan = color.RGBA{0, 200, 255, 255}
yellow = color.RGBA{255, 220, 0, 255}
gray = color.RGBA{150, 150, 150, 255}
)
func main() {
initDisplay()
drawStatus("Advertising...")
drawCounter(0)
must("enable BLE", adapter.Enable())
var txChar bluetooth.Characteristic
must("add service", adapter.AddService(&bluetooth.Service{
UUID: serviceUUID,
Characteristics: []bluetooth.CharacteristicConfig{
{
UUID: rxUUID,
Flags: bluetooth.CharacteristicWritePermission | bluetooth.CharacteristicWriteWithoutResponsePermission,
WriteEvent: func(client bluetooth.Connection, offset int, value []byte) {
cmd := string(value)
if cmd == "reset" || cmd == "reset\n" {
counter = 0
}
},
},
{
Handle: &txChar,
UUID: txUUID,
Flags: bluetooth.CharacteristicNotifyPermission | bluetooth.CharacteristicReadPermission,
},
},
}))
adv := adapter.DefaultAdvertisement()
must("configure adv", adv.Configure(bluetooth.AdvertisementOptions{
LocalName: "NiceBadge",
ServiceUUIDs: []bluetooth.UUID{serviceUUID},
}))
must("start adv", adv.Start())
for {
counter++
drawCounter(counter)
// Write sends a BLE notification to subscribed centrals.
// If no device is subscribed, Write returns an error.
_, err := txChar.Write([]byte(strconv.Itoa(counter) + "\n"))
wasConnected := connected
connected = err == nil
if wasConnected != connected {
if connected {
drawStatus("Connected ")
} else {
drawStatus("Advertising...")
must("restart adv", adv.Start())
}
}
time.Sleep(time.Second)
}
}
func initDisplay() {
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, machine.P1_13, machine.P0_10, machine.P0_09)
display.Configure(st7789.Config{
Rotation: st7789.ROTATION_90,
Width: 135,
Height: 240,
RowOffset: 40,
ColumnOffset: 53,
})
display.FillScreen(black)
}
func drawStatus(status string) {
display.FillRectangle(0, 0, 240, 32, black)
tinyfont.WriteLine(&display, &freesans.Regular9pt7b, 10, 22, "BLE: "+status, cyan)
}
func drawCounter(n int) {
display.FillRectangle(0, 38, 240, 97, black)
tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 10, 68, "Counter", gray)
tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 10, 110, strconv.Itoa(n), yellow)
}
func must(action string, err error) {
if err != nil {
panic("failed to " + action + ": " + err.Error())
}
}

143
tutorial/ble/step2/main.go Normal file
View file

@ -0,0 +1,143 @@
package main
// BLE LED color control example.
// A mobile app writes 3 bytes (R, G, B) to the color characteristic
// and both WS2812 LEDs light up with that color.
//
// Service UUID: BADA5501-B5A3-F393-E0A9-E50E24DCCA9E
// Color char: BADA5502-B5A3-F393-E0A9-E50E24DCCA9E
//
// Compatible apps: nRF Connect, LightBlue (iOS/Android).
// To set color: write 3 hex bytes to the color characteristic, e.g. FF0080 = red:255 green:0 blue:128.
import (
"image/color"
"machine"
"strconv"
"github.com/tinygo-org/bluetooth"
"tinygo.org/x/drivers/st7789"
"tinygo.org/x/drivers/ws2812"
"tinygo.org/x/tinyfont"
"tinygo.org/x/tinyfont/freesans"
)
var (
adapter = bluetooth.DefaultAdapter
ledServiceUUID = bluetooth.NewUUID([16]byte{
0xBA, 0xDA, 0x55, 0x01, 0xB5, 0xA3, 0xF3, 0x93,
0xE0, 0xA9, 0xE5, 0x0E, 0x24, 0xDC, 0xCA, 0x9E,
})
colorCharUUID = bluetooth.NewUUID([16]byte{
0xBA, 0xDA, 0x55, 0x02, 0xB5, 0xA3, 0xF3, 0x93,
0xE0, 0xA9, 0xE5, 0x0E, 0x24, 0xDC, 0xCA, 0x9E,
})
)
var (
display st7789.Device
leds ws2812.Device
connected bool
ledColor color.RGBA
)
var (
black = color.RGBA{0, 0, 0, 255}
cyan = color.RGBA{0, 200, 255, 255}
white = color.RGBA{255, 255, 255, 255}
gray = color.RGBA{150, 150, 150, 255}
)
func main() {
initDisplay()
initLEDs()
drawStatus("Advertising...")
drawColor(color.RGBA{0, 0, 0, 255})
must("enable BLE", adapter.Enable())
must("add service", adapter.AddService(&bluetooth.Service{
UUID: ledServiceUUID,
Characteristics: []bluetooth.CharacteristicConfig{
{
UUID: colorCharUUID,
Flags: bluetooth.CharacteristicWritePermission | bluetooth.CharacteristicWriteWithoutResponsePermission,
WriteEvent: func(client bluetooth.Connection, offset int, value []byte) {
if len(value) < 3 {
return
}
// value[0]=R, value[1]=G, value[2]=B
ledColor = color.RGBA{value[0], value[1], value[2], 255}
setLEDs(ledColor)
drawColor(ledColor)
if !connected {
connected = true
drawStatus("Connected ")
}
},
},
},
}))
adv := adapter.DefaultAdvertisement()
must("configure adv", adv.Configure(bluetooth.AdvertisementOptions{
LocalName: "NiceBadge",
ServiceUUIDs: []bluetooth.UUID{ledServiceUUID},
}))
must("start adv", adv.Start())
// wait forever; all logic is driven by the BLE WriteEvent callback
select {}
}
func initDisplay() {
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, machine.P1_13, machine.P0_10, machine.P0_09)
display.Configure(st7789.Config{
Rotation: st7789.ROTATION_90,
Width: 135,
Height: 240,
RowOffset: 40,
ColumnOffset: 53,
})
display.FillScreen(black)
}
func initLEDs() {
neo := machine.P1_11
neo.Configure(machine.PinConfig{Mode: machine.PinOutput})
leds = ws2812.New(neo)
}
func setLEDs(c color.RGBA) {
leds.WriteColors([]color.RGBA{c, c})
}
func drawStatus(status string) {
display.FillRectangle(0, 0, 240, 32, black)
tinyfont.WriteLine(&display, &freesans.Regular9pt7b, 10, 22, "BLE: "+status, cyan)
}
func drawColor(c color.RGBA) {
// show a filled rectangle with the current color and its RGB values below
display.FillRectangle(0, 38, 240, 60, color.RGBA{c.R, c.G, c.B, 255})
display.FillRectangle(0, 100, 240, 35, black)
rgb := "R:" + strconv.Itoa(int(c.R)) +
" G:" + strconv.Itoa(int(c.G)) +
" B:" + strconv.Itoa(int(c.B))
tinyfont.WriteLine(&display, &freesans.Regular9pt7b, 10, 122, rgb, white)
}
func must(action string, err error) {
if err != nil {
panic("failed to " + action + ": " + err.Error())
}
}

150
tutorial/ble/step3/main.go Normal file
View file

@ -0,0 +1,150 @@
package main
// BLE scanner example.
// Scans for nearby BLE devices and shows their names, addresses and signal
// strength (RSSI) on the display. Press button A to clear the list.
import (
"image/color"
"machine"
"strconv"
"time"
"github.com/tinygo-org/bluetooth"
"tinygo.org/x/drivers/st7789"
"tinygo.org/x/tinyfont"
"tinygo.org/x/tinyfont/freesans"
)
const maxDevices = 5
type bleDevice struct {
name string
addr string
rssi int16
}
var (
adapter = bluetooth.DefaultAdapter
display st7789.Device
devices [maxDevices]bleDevice
count int
)
var (
black = color.RGBA{0, 0, 0, 255}
cyan = color.RGBA{0, 200, 255, 255}
white = color.RGBA{255, 255, 255, 255}
yellow = color.RGBA{255, 220, 0, 255}
green = color.RGBA{0, 220, 100, 255}
gray = color.RGBA{150, 150, 150, 255}
)
func main() {
initDisplay()
btnA := machine.P1_06
btnA.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
must("enable BLE", adapter.Enable())
drawHeader()
// scan runs in a goroutine; found devices are stored and displayed in the main loop
go func() {
adapter.Scan(func(a *bluetooth.Adapter, result bluetooth.ScanResult) {
name := result.LocalName()
if name == "" {
name = result.Address.String()
}
// update existing entry if already seen, otherwise add new
found := false
for i := 0; i < count; i++ {
if devices[i].addr == result.Address.String() {
devices[i].rssi = result.RSSI
found = true
break
}
}
if !found && count < maxDevices {
devices[count] = bleDevice{
name: name,
addr: result.Address.String(),
rssi: result.RSSI,
}
count++
}
})
}()
for {
// button A clears the list
if !btnA.Get() {
count = 0
display.FillRectangle(0, 32, 240, 103, black)
time.Sleep(200 * time.Millisecond)
}
drawDevices()
time.Sleep(500 * time.Millisecond)
}
}
func initDisplay() {
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, machine.P1_13, machine.P0_10, machine.P0_09)
display.Configure(st7789.Config{
Rotation: st7789.ROTATION_90,
Width: 135,
Height: 240,
RowOffset: 40,
ColumnOffset: 53,
})
display.FillScreen(black)
}
func drawHeader() {
tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 10, 24, "BLE Scanner", cyan)
}
func drawDevices() {
// clear device list area
display.FillRectangle(0, 32, 240, 103, black)
if count == 0 {
tinyfont.WriteLine(&display, &freesans.Regular9pt7b, 10, 55, "No devices found...", gray)
return
}
for i := 0; i < count; i++ {
y := int16(52 + i*20)
name := devices[i].name
if len(name) > 16 {
name = name[:16]
}
rssi := " " + strconv.Itoa(int(devices[i].rssi)) + "dB"
// color by signal strength
c := white
if devices[i].rssi > -60 {
c = green
} else if devices[i].rssi > -80 {
c = yellow
}
tinyfont.WriteLine(&display, &freesans.Regular9pt7b, 5, y, name+rssi, c)
}
}
func must(action string, err error) {
if err != nil {
panic("failed to " + action + ": " + err.Error())
}
}

3
tutorial/go.mod Normal file
View file

@ -0,0 +1,3 @@
module code.madriguera.me/GoEducation/nicebadge/tutorial
go 1.22.1

0
tutorial/go.sum Normal file
View file