parent
6b08161955
commit
314ca89209
8 changed files with 427 additions and 5 deletions
2
Makefile
2
Makefile
|
@ -58,6 +58,8 @@ smoketest-windows:
|
||||||
GOOS=windows go build -o /tmp/go-build-discard ./examples/scanner
|
GOOS=windows go build -o /tmp/go-build-discard ./examples/scanner
|
||||||
GOOS=windows go build -o /tmp/go-build-discard ./examples/discover
|
GOOS=windows go build -o /tmp/go-build-discard ./examples/discover
|
||||||
GOOS=windows go build -o /tmp/go-build-discard ./examples/heartrate-monitor
|
GOOS=windows go build -o /tmp/go-build-discard ./examples/heartrate-monitor
|
||||||
|
GOOS=windows go build -o /tmp/go-build-discard ./examples/advertisement
|
||||||
|
GOOS=windows go build -o /tmp/go-build-discard ./examples/heartrate
|
||||||
|
|
||||||
smoketest-macos:
|
smoketest-macos:
|
||||||
# Test on macos.
|
# Test on macos.
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package bluetooth
|
package bluetooth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/go-ole/go-ole"
|
"github.com/go-ole/go-ole"
|
||||||
|
@ -13,6 +14,8 @@ type Adapter struct {
|
||||||
watcher *advertisement.BluetoothLEAdvertisementWatcher
|
watcher *advertisement.BluetoothLEAdvertisementWatcher
|
||||||
|
|
||||||
connectHandler func(device Device, connected bool)
|
connectHandler func(device Device, connected bool)
|
||||||
|
|
||||||
|
defaultAdvertisement *Advertisement
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultAdapter is the default adapter on the system.
|
// DefaultAdapter is the default adapter on the system.
|
||||||
|
@ -56,3 +59,8 @@ func awaitAsyncOperation(asyncOperation *foundation.IAsyncOperation, genericPara
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Adapter) Address() (MACAddress, error) {
|
||||||
|
// TODO: get mac address
|
||||||
|
return MACAddress{}, errors.New("not implemented")
|
||||||
|
}
|
||||||
|
|
102
gap_windows.go
102
gap_windows.go
|
@ -18,6 +18,108 @@ type Address struct {
|
||||||
MACAddress
|
MACAddress
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Advertisement struct {
|
||||||
|
advertisement *advertisement.BluetoothLEAdvertisement
|
||||||
|
publisher *advertisement.BluetoothLEAdvertisementPublisher
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultAdvertisement returns the default advertisement instance but does not
|
||||||
|
// configure it.
|
||||||
|
func (a *Adapter) DefaultAdvertisement() *Advertisement {
|
||||||
|
if a.defaultAdvertisement == nil {
|
||||||
|
a.defaultAdvertisement = &Advertisement{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.defaultAdvertisement
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure this advertisement.
|
||||||
|
// on Windows we're only able to set "Manufacturer Data" for advertisements.
|
||||||
|
// https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.advertisement.bluetoothleadvertisementpublisher?view=winrt-22621#remarks
|
||||||
|
// following this c# source for this implementation: https://github.com/microsoft/Windows-universal-samples/blob/main/Samples/BluetoothAdvertisement/cs/Scenario2_Publisher.xaml.cs
|
||||||
|
// adding service data / localname leads to errors when starting the advertisement.
|
||||||
|
func (a *Advertisement) Configure(options AdvertisementOptions) error {
|
||||||
|
// we can only advertise manufacturer / company data on windows, so no need to continue if we have none
|
||||||
|
if len(options.ManufacturerData) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.publisher != nil {
|
||||||
|
a.publisher.Release()
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.advertisement != nil {
|
||||||
|
a.advertisement.Release()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub, err := advertisement.NewBluetoothLEAdvertisementPublisher()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.publisher = pub
|
||||||
|
|
||||||
|
ad, err := a.publisher.GetAdvertisement()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.advertisement = ad
|
||||||
|
|
||||||
|
vec, err := ad.GetManufacturerData()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, optManData := range options.ManufacturerData {
|
||||||
|
writer, err := streams.NewDataWriter()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer writer.Release()
|
||||||
|
|
||||||
|
err = writer.WriteBytes(uint32(len(optManData.Data)), optManData.Data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := writer.DetachBuffer()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
manData, err := advertisement.BluetoothLEManufacturerDataCreate(optManData.CompanyID, buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = vec.Append(unsafe.Pointer(&manData.IUnknown.RawVTable)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start advertisement. May only be called after it has been configured.
|
||||||
|
func (a *Advertisement) Start() error {
|
||||||
|
// publisher will be present if we actually have manufacturer data to advertise.
|
||||||
|
if a.publisher != nil {
|
||||||
|
return a.publisher.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop advertisement. May only be called after it has been started.
|
||||||
|
func (a *Advertisement) Stop() error {
|
||||||
|
if a.publisher != nil {
|
||||||
|
return a.publisher.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Scan starts a BLE scan. It is stopped by a call to StopScan. A common pattern
|
// Scan starts a BLE scan. It is stopped by a call to StopScan. A common pattern
|
||||||
// is to cancel the scan when a particular device has been found.
|
// is to cancel the scan when a particular device has been found.
|
||||||
func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) (err error) {
|
func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) (err error) {
|
||||||
|
|
4
gatts.go
4
gatts.go
|
@ -7,6 +7,8 @@ type Service struct {
|
||||||
Characteristics []CharacteristicConfig
|
Characteristics []CharacteristicConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WriteEvent = func(client Connection, offset int, value []byte)
|
||||||
|
|
||||||
// CharacteristicConfig contains some parameters for the configuration of a
|
// CharacteristicConfig contains some parameters for the configuration of a
|
||||||
// single characteristic.
|
// single characteristic.
|
||||||
//
|
//
|
||||||
|
@ -17,7 +19,7 @@ type CharacteristicConfig struct {
|
||||||
UUID
|
UUID
|
||||||
Value []byte
|
Value []byte
|
||||||
Flags CharacteristicPermissions
|
Flags CharacteristicPermissions
|
||||||
WriteEvent func(client Connection, offset int, value []byte)
|
WriteEvent WriteEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
// CharacteristicPermissions lists a number of basic permissions/capabilities
|
// CharacteristicPermissions lists a number of basic permissions/capabilities
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
//go:build !linux
|
//go:build !linux && !windows
|
||||||
|
|
||||||
package bluetooth
|
package bluetooth
|
||||||
|
|
||||||
|
|
308
gatts_windows.go
Normal file
308
gatts_windows.go
Normal file
|
@ -0,0 +1,308 @@
|
||||||
|
package bluetooth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/go-ole/go-ole"
|
||||||
|
"github.com/saltosystems/winrt-go"
|
||||||
|
"github.com/saltosystems/winrt-go/windows/devices/bluetooth/genericattributeprofile"
|
||||||
|
"github.com/saltosystems/winrt-go/windows/foundation"
|
||||||
|
"github.com/saltosystems/winrt-go/windows/foundation/collections"
|
||||||
|
"github.com/saltosystems/winrt-go/windows/storage/streams"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Characteristic is a single characteristic in a service. It has an UUID and a
|
||||||
|
// value.
|
||||||
|
type Characteristic struct {
|
||||||
|
wintCharacteristic *genericattributeprofile.GattLocalCharacteristic
|
||||||
|
writeEvent WriteEvent
|
||||||
|
flags CharacteristicPermissions
|
||||||
|
|
||||||
|
valueMtx *sync.Mutex
|
||||||
|
value []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddService creates a new service with the characteristics listed in the
|
||||||
|
// Service struct.
|
||||||
|
func (a *Adapter) AddService(s *Service) error {
|
||||||
|
gattServiceOp, err := genericattributeprofile.GattServiceProviderCreateAsync(syscallUUIDFromUUID(s.UUID))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = awaitAsyncOperation(gattServiceOp, genericattributeprofile.SignatureGattServiceProviderResult); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := gattServiceOp.GetResults()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceProviderResult := (*genericattributeprofile.GattServiceProviderResult)(res)
|
||||||
|
serviceProvider, err := serviceProviderResult.GetServiceProvider()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
localService, err := serviceProvider.GetService()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: "ParameterizedInstanceGUID" + "foundation.NewTypedEventHandler"
|
||||||
|
// seems to always return the same instance, need to figure out how to get different instances each time...
|
||||||
|
// was following c# source for this flow: https://github.com/microsoft/Windows-universal-samples/blob/main/Samples/BluetoothLE/cs/Scenario3_ServerForeground.xaml.cs
|
||||||
|
// which relies on instanced event handlers. for now we'll manually setup our handlers with a map of golang characteristics
|
||||||
|
//
|
||||||
|
// TypedEventHandler<GattLocalCharacteristic,GattWriteRequestedEventArgs>
|
||||||
|
guid := winrt.ParameterizedInstanceGUID(
|
||||||
|
foundation.GUIDTypedEventHandler,
|
||||||
|
genericattributeprofile.SignatureGattLocalCharacteristic,
|
||||||
|
genericattributeprofile.SignatureGattWriteRequestedEventArgs)
|
||||||
|
|
||||||
|
goChars := map[syscall.GUID]*Characteristic{}
|
||||||
|
|
||||||
|
writeRequestedHandler := foundation.NewTypedEventHandler(ole.NewGUID(guid), func(instance *foundation.TypedEventHandler, sender, args unsafe.Pointer) {
|
||||||
|
writeReqArgs := (*genericattributeprofile.GattWriteRequestedEventArgs)(args)
|
||||||
|
reqAsyncOp, err := writeReqArgs.GetRequestAsync()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = awaitAsyncOperation(reqAsyncOp, genericattributeprofile.SignatureGattWriteRequest); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := reqAsyncOp.GetResults()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gattWriteRequest := (*genericattributeprofile.GattWriteRequest)(res)
|
||||||
|
|
||||||
|
buf, err := gattWriteRequest.GetValue()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
offset, err := gattWriteRequest.GetOffset()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
characteristic := (*genericattributeprofile.GattLocalCharacteristic)(sender)
|
||||||
|
uuid, err := characteristic.GetUuid()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
goChar, ok := goChars[uuid]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if goChar.writeEvent != nil {
|
||||||
|
// TODO: connection?
|
||||||
|
goChar.writeEvent(0, int(offset), bufferToSlice(buf))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
guid = winrt.ParameterizedInstanceGUID(
|
||||||
|
foundation.GUIDTypedEventHandler,
|
||||||
|
genericattributeprofile.SignatureGattLocalCharacteristic,
|
||||||
|
genericattributeprofile.SignatureGattReadRequestedEventArgs)
|
||||||
|
|
||||||
|
readRequestedHandler := foundation.NewTypedEventHandler(ole.NewGUID(guid), func(instance *foundation.TypedEventHandler, sender, args unsafe.Pointer) {
|
||||||
|
readReqArgs := (*genericattributeprofile.GattReadRequestedEventArgs)(args)
|
||||||
|
reqAsyncOp, err := readReqArgs.GetRequestAsync()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = awaitAsyncOperation(reqAsyncOp, genericattributeprofile.SignatureGattReadRequest); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := reqAsyncOp.GetResults()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gattReadRequest := (*genericattributeprofile.GattReadRequest)(res)
|
||||||
|
|
||||||
|
characteristic := (*genericattributeprofile.GattLocalCharacteristic)(sender)
|
||||||
|
uuid, err := characteristic.GetUuid()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
goChar, ok := goChars[uuid]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writer, err := streams.NewDataWriter()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer writer.Release()
|
||||||
|
|
||||||
|
goChar.valueMtx.Lock()
|
||||||
|
defer goChar.valueMtx.Unlock()
|
||||||
|
if len(goChar.value) > 0 {
|
||||||
|
if err = writer.WriteBytes(uint32(len(goChar.value)), goChar.value); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := writer.DetachBuffer()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gattReadRequest.RespondWithValue(buf)
|
||||||
|
buf.Release()
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, char := range s.Characteristics {
|
||||||
|
params, err := genericattributeprofile.NewGattLocalCharacteristicParameters()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = params.SetCharacteristicProperties(genericattributeprofile.GattCharacteristicProperties(char.Flags)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
uuid := syscallUUIDFromUUID(char.UUID)
|
||||||
|
createCharOp, err := localService.CreateCharacteristicAsync(uuid, params)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = awaitAsyncOperation(createCharOp, genericattributeprofile.SignatureGattLocalCharacteristicResult); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := createCharOp.GetResults()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
characteristicResults := (*genericattributeprofile.GattLocalCharacteristicResult)(res)
|
||||||
|
characteristic, err := characteristicResults.GetCharacteristic()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = characteristic.AddWriteRequested(writeRequestedHandler)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = characteristic.AddReadRequested(readRequestedHandler)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the object around for Characteristic.Write.
|
||||||
|
if char.Handle != nil {
|
||||||
|
char.Handle.wintCharacteristic = characteristic
|
||||||
|
char.Handle.value = char.Value
|
||||||
|
char.Handle.valueMtx = &sync.Mutex{}
|
||||||
|
char.Handle.flags = char.Flags
|
||||||
|
goChars[uuid] = char.Handle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params, err := genericattributeprofile.NewGattServiceProviderAdvertisingParameters()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = params.SetIsConnectable(true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = params.SetIsDiscoverable(true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return serviceProvider.StartAdvertisingWithParameters(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write replaces the characteristic value with a new value.
|
||||||
|
func (c *Characteristic) Write(p []byte) (n int, err error) {
|
||||||
|
length := len(p)
|
||||||
|
|
||||||
|
if length == 0 {
|
||||||
|
return 0, nil // nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.writeEvent != nil {
|
||||||
|
c.writeEvent(0, 0, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// writes are only actually processed on read events from clients, we just set a variable here.
|
||||||
|
c.valueMtx.Lock()
|
||||||
|
defer c.valueMtx.Unlock()
|
||||||
|
c.value = p
|
||||||
|
|
||||||
|
// only notify if it's enabled, otherwise the below leads to an error
|
||||||
|
if c.flags&CharacteristicNotifyPermission != 0 {
|
||||||
|
writer, err := streams.NewDataWriter()
|
||||||
|
if err != nil {
|
||||||
|
return length, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer writer.Release()
|
||||||
|
err = writer.WriteBytes(uint32(len(p)), p)
|
||||||
|
if err != nil {
|
||||||
|
return length, err
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := writer.DetachBuffer()
|
||||||
|
if err != nil {
|
||||||
|
return length, err
|
||||||
|
}
|
||||||
|
defer buf.Release()
|
||||||
|
|
||||||
|
op, err := c.wintCharacteristic.NotifyValueAsync(buf)
|
||||||
|
if err != nil {
|
||||||
|
return length, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// IVectorView<GattClientNotificationResult>
|
||||||
|
signature := fmt.Sprintf("pinterface({%s};%s)", collections.GUIDIVectorView, genericattributeprofile.SignatureGattClientNotificationResult)
|
||||||
|
if err = awaitAsyncOperation(op, signature); err != nil {
|
||||||
|
return length, err
|
||||||
|
}
|
||||||
|
defer op.Release()
|
||||||
|
|
||||||
|
res, err := op.GetResults()
|
||||||
|
if err != nil {
|
||||||
|
return length, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: process notification results, just getting this to release
|
||||||
|
vec := (*collections.IVectorView)(res)
|
||||||
|
vec.Release()
|
||||||
|
}
|
||||||
|
|
||||||
|
return length, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func syscallUUIDFromUUID(uuid UUID) syscall.GUID {
|
||||||
|
guid := ole.NewGUID(uuid.String())
|
||||||
|
return syscall.GUID{
|
||||||
|
Data1: guid.Data1,
|
||||||
|
Data2: guid.Data2,
|
||||||
|
Data3: guid.Data3,
|
||||||
|
Data4: guid.Data4,
|
||||||
|
}
|
||||||
|
}
|
2
go.mod
2
go.mod
|
@ -5,7 +5,7 @@ go 1.18
|
||||||
require (
|
require (
|
||||||
github.com/go-ole/go-ole v1.2.6
|
github.com/go-ole/go-ole v1.2.6
|
||||||
github.com/godbus/dbus/v5 v5.1.0
|
github.com/godbus/dbus/v5 v5.1.0
|
||||||
github.com/saltosystems/winrt-go v0.0.0-20240320113951-a2e4fc03f5f4
|
github.com/saltosystems/winrt-go v0.0.0-20240509090452-45c2d7a6235a
|
||||||
github.com/tinygo-org/cbgo v0.0.4
|
github.com/tinygo-org/cbgo v0.0.4
|
||||||
golang.org/x/crypto v0.12.0
|
golang.org/x/crypto v0.12.0
|
||||||
tinygo.org/x/drivers v0.26.1-0.20230922160320-ed51435c2ef6
|
tinygo.org/x/drivers v0.26.1-0.20230922160320-ed51435c2ef6
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -10,8 +10,8 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/saltosystems/winrt-go v0.0.0-20240320113951-a2e4fc03f5f4 h1:zurEWtOr/OYiTb5bcD7eeHLOfj6vCR30uldlwse1cSM=
|
github.com/saltosystems/winrt-go v0.0.0-20240509090452-45c2d7a6235a h1:i7iQTpf9AlppS14KWzsTvSS8jx4BLPCAwM2lywguZCY=
|
||||||
github.com/saltosystems/winrt-go v0.0.0-20240320113951-a2e4fc03f5f4/go.mod h1:CIltaIm7qaANUIvzr0Vmz71lmQMAIbGJ7cvgzX7FMfA=
|
github.com/saltosystems/winrt-go v0.0.0-20240509090452-45c2d7a6235a/go.mod h1:CIltaIm7qaANUIvzr0Vmz71lmQMAIbGJ7cvgzX7FMfA=
|
||||||
github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
|
github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
|
Loading…
Reference in a new issue