Skip to content

Commit 223221f

Browse files
committed
feature: add tinyscan hardware scanner
Signed-off-by: deadprogram <[email protected]>
1 parent 721a18d commit 223221f

File tree

9 files changed

+354
-0
lines changed

9 files changed

+354
-0
lines changed

tinyscan/README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# TinyScan
2+
3+
![tinyscan](../images/tinyscan.gif)
4+
5+
Scanner for local FindMy devices that runs on small microcontrollers that have Bluetooth and also a screen attached.
6+
7+
Looks for any devices nearby that are broadcasting the correct manufacturer data, and displays the MAC address and the public key for that device on the display.
8+
9+
## Supported hardware
10+
11+
The following devices currently work with the Go Haystack TinyScan firmware.
12+
13+
### Pimoroni Badger-2040W
14+
15+
https://shop.pimoroni.com/products/badger-2040-w?variant=40514062188627
16+
17+
18+
```shell
19+
tinygo flash -target badger2040-w -stack-size 8kb .
20+
```
21+
22+
### Adafruit Clue
23+
24+
25+
https://www.adafruit.com/clue
26+
27+
28+
```shell
29+
tinygo flash -target clue -stack-size 8kb .
30+
```
31+
32+
33+
### Adafruit PyBadge with Airlift Featherwing
34+
35+
36+
https://www.adafruit.com/product/4200
37+
38+
https://www.adafruit.com/product/4264
39+
40+
41+
```shell
42+
tinygo flash -target pybadge -stack-size 8kb .
43+
```
44+
45+
### Adafruit Pyportal
46+
47+
48+
https://www.adafruit.com/product/4116
49+
50+
51+
```shell
52+
tinygo flash -target pyportal -stack-size 8kb .
53+
```

tinyscan/badger2040w.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//go:build badger2040_w
2+
3+
package main
4+
5+
import (
6+
"machine"
7+
8+
"tinygo.org/x/tinyfont"
9+
"tinygo.org/x/tinyterm"
10+
"tinygo.org/x/tinyterm/displays"
11+
)
12+
13+
var (
14+
font = &tinyfont.Picopixel
15+
)
16+
17+
func initTerminal() {
18+
led3v3 := machine.ENABLE_3V3
19+
led3v3.Configure(machine.PinConfig{Mode: machine.PinOutput})
20+
led3v3.High()
21+
22+
display := displays.Init()
23+
24+
terminal = tinyterm.NewTerminal(display)
25+
terminal.Configure(&tinyterm.Config{
26+
Font: font,
27+
FontHeight: 8,
28+
FontOffset: 4,
29+
UseSoftwareScroll: true,
30+
})
31+
}

tinyscan/clue.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//go:build clue_alpha
2+
3+
package main
4+
5+
import (
6+
"tinygo.org/x/tinyfont/proggy"
7+
"tinygo.org/x/tinyterm"
8+
"tinygo.org/x/tinyterm/displays"
9+
)
10+
11+
var (
12+
font = &proggy.TinySZ8pt7b
13+
)
14+
15+
func initTerminal() {
16+
display := displays.Init()
17+
18+
terminal = tinyterm.NewTerminal(display)
19+
terminal.Configure(&tinyterm.Config{
20+
Font: font,
21+
FontHeight: 10,
22+
FontOffset: 6,
23+
UseSoftwareScroll: true,
24+
})
25+
}

tinyscan/findmy.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
6+
"tinygo.org/x/bluetooth"
7+
)
8+
9+
const (
10+
// Apple, Inc.
11+
appleCompanyID = 0x004C
12+
13+
// Offline Finding type
14+
findMyPayloadType = 0x12
15+
16+
// Length of the payload
17+
findMyPayloadLength = 0x19
18+
19+
// Hint byte
20+
findMyHint = 0x00
21+
)
22+
23+
func parseData(address bluetooth.Address, data []byte) (byte, []byte, error) {
24+
if len(data) < 27 {
25+
return 0, nil, errors.New("data is too short")
26+
}
27+
28+
if data[0] != findMyPayloadType {
29+
return 0, nil, errors.New("invalid payload type")
30+
}
31+
32+
if data[1] != findMyPayloadLength {
33+
return 0, nil, errors.New("invalid payload length")
34+
}
35+
36+
if data[26] != findMyHint {
37+
return 0, nil, errors.New("invalid hint")
38+
}
39+
40+
findMyStatus := data[2]
41+
var key [28]byte
42+
copy(key[6:], data[3:25])
43+
44+
// turn address into key bytes
45+
key[0] = address.MAC[5]
46+
key[1] = address.MAC[4]
47+
key[2] = address.MAC[3]
48+
key[3] = address.MAC[2]
49+
key[4] = address.MAC[1]
50+
key[5] = address.MAC[0]
51+
52+
return findMyStatus, key[:], nil
53+
}

tinyscan/go.mod

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
module github.com/hybridgroup/go-haystack/tinyscan
2+
3+
go 1.23.0
4+
5+
require (
6+
tinygo.org/x/bluetooth v0.10.1-0.20250110155930-faf2ed3d797d
7+
tinygo.org/x/drivers v0.29.0
8+
tinygo.org/x/tinyfont v0.5.0
9+
tinygo.org/x/tinyterm v0.4.1-0.20250110161638-0af95c3b0d98
10+
)
11+
12+
require (
13+
github.com/go-ole/go-ole v1.2.6 // indirect
14+
github.com/godbus/dbus/v5 v5.1.0 // indirect
15+
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
16+
github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b // indirect
17+
github.com/sirupsen/logrus v1.9.3 // indirect
18+
github.com/soypat/cyw43439 v0.0.0-20241116210509-ae1ce0e084c5 // indirect
19+
github.com/soypat/seqs v0.0.0-20240527012110-1201bab640ef // indirect
20+
github.com/tinygo-org/cbgo v0.0.4 // indirect
21+
github.com/tinygo-org/pio v0.0.0-20231216154340-cd888eb58899 // indirect
22+
golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 // indirect
23+
golang.org/x/sys v0.11.0 // indirect
24+
)

tinyscan/go.sum

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4+
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
5+
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
6+
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
7+
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
8+
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
9+
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
10+
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
11+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
12+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
13+
github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b h1:du3zG5fd8snsFN6RBoLA7fpaYV9ZQIsyH9snlk2Zvik=
14+
github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b/go.mod h1:CIltaIm7qaANUIvzr0Vmz71lmQMAIbGJ7cvgzX7FMfA=
15+
github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
16+
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
17+
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
18+
github.com/soypat/cyw43439 v0.0.0-20241116210509-ae1ce0e084c5 h1:arwJFX1x5zq+wUp5ADGgudhMQEXKNMQOmTh+yYgkwzw=
19+
github.com/soypat/cyw43439 v0.0.0-20241116210509-ae1ce0e084c5/go.mod h1:1Otjk6PRhfzfcVHeWMEeku/VntFqWghUwuSQyivb2vE=
20+
github.com/soypat/seqs v0.0.0-20240527012110-1201bab640ef h1:phH95I9wANjTYw6bSYLZDQfNvao+HqYDom8owbNa0P4=
21+
github.com/soypat/seqs v0.0.0-20240527012110-1201bab640ef/go.mod h1:oCVCNGCHMKoBj97Zp9znLbQ1nHxpkmOY9X+UAGzOxc8=
22+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
23+
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
24+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
25+
github.com/stretchr/testify v1.7.5 h1:s5PTfem8p8EbKQOctVV53k6jCJt3UX4IEJzwh+C324Q=
26+
github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
27+
github.com/tinygo-org/cbgo v0.0.4 h1:3D76CRYbH03Rudi8sEgs/YO0x3JIMdyq8jlQtk/44fU=
28+
github.com/tinygo-org/cbgo v0.0.4/go.mod h1:7+HgWIHd4nbAz0ESjGlJ1/v9LDU1Ox8MGzP9mah/fLk=
29+
github.com/tinygo-org/pio v0.0.0-20231216154340-cd888eb58899 h1:/DyaXDEWMqoVUVEJVJIlNk1bXTbFs8s3Q4GdPInSKTQ=
30+
github.com/tinygo-org/pio v0.0.0-20231216154340-cd888eb58899/go.mod h1:LU7Dw00NJ+N86QkeTGjMLNkYcEYMor6wTDpTCu0EaH8=
31+
golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 h1:/yRP+0AN7mf5DkD3BAI6TOFnd51gEoDEb8o35jIFtgw=
32+
golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
33+
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
34+
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
35+
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
36+
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
37+
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
38+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
39+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
40+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
41+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
42+
tinygo.org/x/bluetooth v0.10.1-0.20250110155930-faf2ed3d797d h1:vYBLOWzn/IIjua31fep+htcCYUkzyuuHWNMfvE7DURM=
43+
tinygo.org/x/bluetooth v0.10.1-0.20250110155930-faf2ed3d797d/go.mod h1:XLRopLvxWmIbofpZSXc7BGGCpgFOV5lrZ1i/DQN0BCw=
44+
tinygo.org/x/drivers v0.29.0 h1:xHuq8Fr1D/D2+1V/3d+aXufqP81/CLi1itdVbrYgrE0=
45+
tinygo.org/x/drivers v0.29.0/go.mod h1:q/mU8G/wz821p8xXqbkBACOlmZFDHXd//DnYnCW+dDQ=
46+
tinygo.org/x/tinyfont v0.5.0 h1:+ApIQzuUuibx/LACLnGY5MWZ/zFwed0RJAnCJCRi2bk=
47+
tinygo.org/x/tinyfont v0.5.0/go.mod h1:2mKugz6aud3EO2IIBNQ2AbDv13kRD+s7R1U1FZ21Lkw=
48+
tinygo.org/x/tinyterm v0.4.1-0.20250110161638-0af95c3b0d98 h1:tE0DNmV83qHun9JF743ZFFMxq0UT2FXUhc398oQgTNc=
49+
tinygo.org/x/tinyterm v0.4.1-0.20250110161638-0af95c3b0d98/go.mod h1:gSsU5YIh0NsiU5cstRrcecrpXoJsdsQUNrOZw00/5TU=

tinyscan/main.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package main
2+
3+
import (
4+
"encoding/hex"
5+
"fmt"
6+
"image/color"
7+
"time"
8+
9+
"tinygo.org/x/bluetooth"
10+
"tinygo.org/x/tinyterm"
11+
)
12+
13+
var (
14+
terminal *tinyterm.Terminal
15+
16+
black = color.RGBA{0, 0, 0, 255}
17+
adapter = bluetooth.DefaultAdapter
18+
)
19+
20+
func main() {
21+
initTerminal()
22+
23+
terminalOutput("enable interface...")
24+
25+
must("enable BLE interface", adapter.Enable())
26+
time.Sleep(time.Second)
27+
28+
terminalOutput("start scan...")
29+
30+
must("start scan", adapter.Scan(scanHandler))
31+
32+
for {
33+
time.Sleep(time.Minute)
34+
terminalOutput("scanning...")
35+
}
36+
}
37+
38+
func scanHandler(adapter *bluetooth.Adapter, device bluetooth.ScanResult) {
39+
if device.ManufacturerData() != nil && device.ManufacturerData()[0].CompanyID == appleCompanyID {
40+
status, key, err := parseData(device.Address, device.ManufacturerData()[0].Data)
41+
switch {
42+
case err != nil:
43+
terminalOutput("ERROR: failed to parse data:" + err.Error())
44+
default:
45+
terminalOutput("--------------------------------")
46+
terminalOutput(fmt.Sprintf("%s %d", device.Address.String(), device.RSSI))
47+
terminalOutput(fmt.Sprintf("%s %s", hex.EncodeToString(key), hex.EncodeToString([]byte{status})))
48+
}
49+
}
50+
}
51+
52+
func must(action string, err error) {
53+
if err != nil {
54+
for {
55+
terminalOutput("failed to " + action + ": " + err.Error())
56+
57+
time.Sleep(time.Second)
58+
}
59+
}
60+
}
61+
62+
func terminalOutput(s string) {
63+
println(s)
64+
fmt.Fprintf(terminal, "\n%s", s)
65+
66+
terminal.Display()
67+
}

tinyscan/pybadge.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//go:build pybadge
2+
3+
package main
4+
5+
import (
6+
"tinygo.org/x/tinyfont"
7+
"tinygo.org/x/tinyterm"
8+
"tinygo.org/x/tinyterm/displays"
9+
)
10+
11+
var (
12+
font = &tinyfont.Picopixel
13+
)
14+
15+
func initTerminal() {
16+
display := displays.Init()
17+
18+
terminal = tinyterm.NewTerminal(display)
19+
terminal.Configure(&tinyterm.Config{
20+
Font: font,
21+
FontHeight: 8,
22+
FontOffset: 4,
23+
UseSoftwareScroll: true,
24+
})
25+
}

tinyscan/pyportal.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//go:build pyportal
2+
3+
package main
4+
5+
import (
6+
"tinygo.org/x/drivers/ili9341"
7+
"tinygo.org/x/tinyfont/proggy"
8+
"tinygo.org/x/tinyterm"
9+
"tinygo.org/x/tinyterm/displays"
10+
)
11+
12+
var (
13+
font = &proggy.TinySZ8pt7b
14+
)
15+
16+
func initTerminal() {
17+
display := displays.Init()
18+
display.SetRotation(ili9341.Rotation270)
19+
20+
terminal = tinyterm.NewTerminal(display)
21+
terminal.Configure(&tinyterm.Config{
22+
Font: font,
23+
FontHeight: 10,
24+
FontOffset: 6,
25+
UseSoftwareScroll: true,
26+
})
27+
}

0 commit comments

Comments
 (0)