mirror of
https://github.com/lucarin91/scratch-link4linux.git
synced 2025-07-21 17:41:18 +02:00
initial commit
This commit is contained in:
92
scratchlink/ble.go
Normal file
92
scratchlink/ble.go
Normal 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
198
scratchlink/handler.go
Normal 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
101
scratchlink/types.go
Normal 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, ¶ms)
|
||||
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, ¶ms)
|
||||
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, ¶ms)
|
||||
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, ¶ms)
|
||||
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, ¶ms)
|
||||
if err != nil {
|
||||
return ReadParams{}, err
|
||||
}
|
||||
|
||||
return params, nil
|
||||
}
|
Reference in New Issue
Block a user