initial commit

This commit is contained in:
lucarin91
2024-01-13 14:55:03 +01:00
parent d1c918f43d
commit 87b90f8015
11 changed files with 743 additions and 0 deletions

92
scratchlink/ble.go Normal file
View File

@@ -0,0 +1,92 @@
package scratchlink
import (
"encoding/base64"
"github.com/google/uuid"
"github.com/rs/zerolog/log"
"golang.org/x/net/websocket"
"tinygo.org/x/bluetooth"
"github.com/lucarin91/scratch-link4linux/jsonrpc"
)
func matchDevice(device bluetooth.ScanResult, filters []DiscoverFilter) bool {
//TODO: implement match device
for _, filter := range filters {
if len(filter.Name) != 0 && filter.Name != device.LocalName() {
return false
}
for _, service := range filter.Services {
if !device.HasServiceUUID(bluetooth.NewUUID(service)) {
return false
}
}
}
return true
}
func getDeviceCharacteristic(device bluetooth.Device, serviceID, characteristicID bluetooth.UUID) (bluetooth.DeviceCharacteristic, error) {
services, err := device.DiscoverServices([]bluetooth.UUID{serviceID})
if err != nil {
return bluetooth.DeviceCharacteristic{}, err
}
chars, err := services[0].DiscoverCharacteristics([]bluetooth.UUID{characteristicID})
if err != nil {
return bluetooth.DeviceCharacteristic{}, err
}
return chars[0], nil
}
func notificationCallback(c *websocket.Conn, serviceID, characteristicID uuid.UUID) func(buf []byte) {
return func(buf []byte) {
_ = jsonrpc.WsSend(c, jsonrpc.NewMsg("characteristicDidChange", UpdateParams{
ServiceID: serviceID,
CharacteristicID: characteristicID,
Message: base64.StdEncoding.EncodeToString(buf),
Encoding: "base64",
}))
}
}
func startAsyncScan(adapter *bluetooth.Adapter, filter []DiscoverFilter) <-chan Device {
// Stop previus scan (if any).
_ = adapter.StopScan()
devices := make(chan Device, 10)
go func() {
defer close(devices)
err := adapter.Scan(func(adapter *bluetooth.Adapter, device bluetooth.ScanResult) {
if len(device.LocalName()) == 0 {
return
}
log.Debug().
Str("name", device.LocalName()).
Str("address", device.Address.String()).
Int16("RSSI", device.RSSI).
Msg("found device")
if !matchDevice(device, filter) {
return
}
devices <- Device{
PeripheralID: device.Address.String(),
Name: device.LocalName(),
RSSI: device.RSSI,
}
})
if err != nil {
log.Error().Err(err).Msg("scan error")
}
}()
return devices
}

198
scratchlink/handler.go Normal file
View File

@@ -0,0 +1,198 @@
package scratchlink
import (
"encoding/base64"
"github.com/lucarin91/scratch-link4linux/jsonrpc"
"github.com/rs/zerolog/log"
"golang.org/x/net/websocket"
"tinygo.org/x/bluetooth"
)
func GetHandler(adapter *bluetooth.Adapter) websocket.Handler {
return websocket.Handler(func(c *websocket.Conn) {
log.Info().Msgf("client connected from %q", c.RemoteAddr())
var DEVICE *bluetooth.Device
msgs := jsonrpc.WsReadLoop(c)
for msg := range msgs {
log.Debug().Msgf("get message: %v", msg)
switch msg.Method {
case "getVersion":
_ = jsonrpc.WsSend(c, msg.Respond(map[string]string{"protocol": "1.3"}))
case "discover":
params, err := DiscoverParamsFromJSON(msg.Params)
if err != nil {
_ = jsonrpc.WsSend(c, msg.Error(err.Error()))
continue
}
devices := startAsyncScan(adapter, params.Filters)
go func() {
for device := range devices {
_ = jsonrpc.WsSend(c, jsonrpc.NewMsg("didDiscoverPeripheral", device))
}
}()
_ = jsonrpc.WsSend(c, msg.Respond(nil))
case "connect":
params, err := ConnectParamsFromJSON(msg.Params)
if err != nil {
_ = jsonrpc.WsSend(c, msg.Error(err.Error()))
continue
}
_ = adapter.StopScan()
mac := bluetooth.Address{}
mac.Set(params.PeripheralID)
DEVICE, err = adapter.Connect(mac, bluetooth.ConnectionParams{
ConnectionTimeout: 0,
MinInterval: 0,
MaxInterval: 0,
})
if err != nil {
log.Error().Msgf("ble connect error: %s", err)
_ = jsonrpc.WsSend(c, msg.Error(err.Error()))
continue
}
_ = jsonrpc.WsSend(c, msg.Respond(nil))
case "startNotifications":
params, err := NotificationsParamsFromJSON(msg.Params)
if err != nil {
_ = jsonrpc.WsSend(c, msg.Error(err.Error()))
continue
}
log.Debug().Msgf("startNotifications params: %+v", params)
char, err := getDeviceCharacteristic(*DEVICE, bluetooth.NewUUID(params.ServiceID), bluetooth.NewUUID(params.CharacteristicID))
if err != nil {
log.Error().Msgf("get device characteristic error: %s", err)
_ = jsonrpc.WsSend(c, msg.Error(err.Error()))
continue
}
err = char.EnableNotifications(notificationCallback(c, params.CharacteristicID, params.CharacteristicID))
if err != nil {
log.Error().Msgf("enable notification error: %s", err)
_ = jsonrpc.WsSend(c, msg.Error(err.Error()))
continue
}
_ = jsonrpc.WsSend(c, msg.Respond(nil))
case "write":
params, err := UpdateParamsFromJSON(msg.Params)
if err != nil {
_ = jsonrpc.WsSend(c, msg.Error(err.Error()))
continue
}
log.Debug().Msgf("write params: %+v", params)
if params.Encoding != "base64" {
log.Error().Msgf("encoding format %q not supported", params.Encoding)
continue
}
services, err := DEVICE.DiscoverServices([]bluetooth.UUID{bluetooth.NewUUID(params.ServiceID)})
if err != nil {
log.Error().Msgf("discover service error: %s", err)
_ = jsonrpc.WsSend(c, msg.Error(err.Error()))
continue
}
chars, err := services[0].DiscoverCharacteristics([]bluetooth.UUID{bluetooth.NewUUID(params.CharacteristicID)})
if err != nil {
log.Error().Msgf("discovert characteristics error: %s", err)
_ = jsonrpc.WsSend(c, msg.Error(err.Error()))
continue
}
char := chars[0]
buf, err := base64.StdEncoding.DecodeString(params.Message)
if err != nil {
_ = jsonrpc.WsSend(c, msg.Error(err.Error()))
continue
}
// TODO: handle params.WithResponse
n, err := char.WriteWithoutResponse(buf)
if err != nil {
_ = jsonrpc.WsSend(c, msg.Error(err.Error()))
continue
}
_ = jsonrpc.WsSend(c, msg.Respond(n))
case "read":
params, err := ReadParamsFromJSON(msg.Params)
if err != nil {
_ = jsonrpc.WsSend(c, msg.Error(err.Error()))
continue
}
log.Debug().Msgf("read params: %+v", params)
char, err := getDeviceCharacteristic(*DEVICE, bluetooth.NewUUID(params.ServiceID), bluetooth.NewUUID(params.CharacteristicID))
if err != nil {
log.Error().Msgf("get device characteristic error: %s", err)
_ = jsonrpc.WsSend(c, msg.Error(err.Error()))
continue
}
if params.StartNotifications {
err = char.EnableNotifications(notificationCallback(c, params.CharacteristicID, params.CharacteristicID))
if err != nil {
log.Error().Msgf("enable notification error: %s", err)
_ = jsonrpc.WsSend(c, msg.Error(err.Error()))
continue
}
}
buf := make([]byte, 512)
n, err := char.Read(buf)
if err != nil {
log.Error().Msgf("read characteristic error: %s", err)
_ = jsonrpc.WsSend(c, msg.Error(err.Error()))
continue
}
_ = jsonrpc.WsSend(c, msg.RespondBytes(buf[:n]))
case "stopNotifications":
params, err := NotificationsParamsFromJSON(msg.Params)
if err != nil {
_ = jsonrpc.WsSend(c, msg.Error(err.Error()))
continue
}
log.Debug().Msgf("stopNotifications params: %+v", params)
char, err := getDeviceCharacteristic(*DEVICE, bluetooth.NewUUID(params.ServiceID), bluetooth.NewUUID(params.CharacteristicID))
if err != nil {
log.Error().Msgf("get device characteristic error: %s", err)
_ = jsonrpc.WsSend(c, msg.Error(err.Error()))
continue
}
err = char.EnableNotifications(nil)
if err != nil {
_ = jsonrpc.WsSend(c, msg.Error(err.Error()))
continue
}
_ = jsonrpc.WsSend(c, msg.Respond(nil))
default:
log.Error().Msgf("unknown command '%s' with params: %+v", msg.Method, msg.DebugParams())
}
}
log.Info().Msgf("client disconnected from %q", c.RemoteAddr())
})
}

101
scratchlink/types.go Normal file
View File

@@ -0,0 +1,101 @@
package scratchlink
import (
"encoding/json"
"github.com/google/uuid"
)
type Device struct {
PeripheralID string `json:"peripheralId"`
Name string `json:"name"`
RSSI int16 `json:"rssi"`
}
type DiscoverParams struct {
Filters []DiscoverFilter `json:"filters"`
}
func DiscoverParamsFromJSON(j json.RawMessage) (DiscoverParams, error) {
var params DiscoverParams
err := json.Unmarshal(j, &params)
if err != nil {
return DiscoverParams{}, err
}
return params, nil
}
type DiscoverFilter struct {
Name string `json:"name"`
NamePrefix string `json:"namePrefix"`
Services []uuid.UUID `json:"services"`
}
type ConnectParams struct {
PeripheralID string `json:"peripheralId"`
}
func ConnectParamsFromJSON(j json.RawMessage) (ConnectParams, error) {
var params ConnectParams
err := json.Unmarshal(j, &params)
if err != nil {
return ConnectParams{}, err
}
return params, nil
}
type NotificationsParams struct {
ServiceID uuid.UUID `json:"serviceId"`
CharacteristicID uuid.UUID `json:"characteristicId"`
}
func NotificationsParamsFromJSON(j json.RawMessage) (NotificationsParams, error) {
var params NotificationsParams
err := json.Unmarshal(j, &params)
if err != nil {
return NotificationsParams{}, err
}
return params, nil
}
type UpdateParams struct {
ServiceID uuid.UUID `json:"serviceId"`
CharacteristicID uuid.UUID `json:"characteristicId"`
Message string `json:"message"`
Encoding string `json:"encoding,omitempty"`
WithResponse bool `json:"withResponse"`
}
func UpdateParamsFromJSON(j json.RawMessage) (UpdateParams, error) {
var params UpdateParams
err := json.Unmarshal(j, &params)
if err != nil {
return UpdateParams{}, err
}
return params, nil
}
type ReadParams struct {
ServiceID uuid.UUID `json:"serviceId"`
CharacteristicID uuid.UUID `json:"characteristicId"`
StartNotifications bool `json:"startNotifications"`
}
func ReadParamsFromJSON(j json.RawMessage) (ReadParams, error) {
var params ReadParams
err := json.Unmarshal(j, &params)
if err != nil {
return ReadParams{}, err
}
return params, nil
}