From 74efaa024a734670577d5fe784feb9ca307f3e51 Mon Sep 17 00:00:00 2001 From: Anthony Hinsinger Date: Thu, 18 Dec 2025 08:42:37 +0100 Subject: [PATCH] first Working FTMS --- src/adv.js | 23 +++++--- src/bluez-gatt.js | 51 +++++++++++++++- src/ftms-service.js | 137 +++++++++++++++++++++++++++++++++++++++++++ src/hr-service.js | 45 ++++++++++++++ src/index.js | 104 +++++++++----------------------- src/power-service.js | 45 ++++++++++++++ 6 files changed, 318 insertions(+), 87 deletions(-) create mode 100644 src/ftms-service.js create mode 100644 src/hr-service.js create mode 100644 src/power-service.js diff --git a/src/adv.js b/src/adv.js index 478bb27..fd46b63 100644 --- a/src/adv.js +++ b/src/adv.js @@ -1,21 +1,28 @@ import { BaseInterface } from "./base"; -import dbus from "dbus-next"; +import dbus, { interface as iface } from "dbus-next"; +const { property, method } = iface; export class Advertisement extends BaseInterface { - @dbus.interface.property({ signature: "s" }) + @property({ signature: "s" }) Type = "peripheral"; - @dbus.interface.property({ signature: "s" }) + @property({ signature: "s" }) LocalName = "TestDevice"; - @dbus.interface.property({ signature: "as" }) - ServiceUUIDs = ["139fc001-a4ed-11ed-b9df-0242ac120003"]; - //ServiceUUIDs = ["1816", "1818", "1826"]; + @property({ signature: "as" }) + ServiceUUIDs = []; - @dbus.interface.method({ inSignature: "", outSignature: "" }) + @property({ signature: "a{sv}" }) + ServiceData = { 1826: new dbus.Variant("ay", [0x01, 0x20, 0x00]) }; + + @property({ signature: "as" }) + Includes = ["tx-power"]; + + @method({ inSignature: "", outSignature: "" }) Release() {} - constructor(bus, path) { + constructor(bus, path, uuids) { super(bus, path, "org.bluez.LEAdvertisement1"); + this.ServiceUUIDs = uuids; } } diff --git a/src/bluez-gatt.js b/src/bluez-gatt.js index 9b8b0d6..058bc3e 100644 --- a/src/bluez-gatt.js +++ b/src/bluez-gatt.js @@ -1,6 +1,35 @@ import { BaseInterface } from "./base"; import { Variant, interface as iface } from "dbus-next"; -const { method, property } = iface; +const { method, property, Interface } = iface; + +export class BaseApplication extends BaseInterface { + services = []; + + constructor(bus, path) { + super(bus, path, "org.freedesktop.DBus.ObjectManager"); + } + + addService(service) { + this.services.push(service); + } + + @method({ outSignature: "a{oa{sa{sv}}}" }) + GetManagedObjects(val) { + const reply = {}; + for (const service of this.services) { + reply[service.path] = { + [service.$name]: service.getProperties(), + }; + + for (const char of service.characteristics) { + reply[char.path] = { + [char.$name]: char.getProperties(), + }; + } + } + return reply; + } +} export class BaseService extends BaseInterface { @property({ signature: "s" }) @@ -46,6 +75,9 @@ export class BaseCharacteritic extends BaseInterface { @property({ signature: "b" }) Notifying = false; + @property({ signature: "ay" }) + Value = []; + constructor(bus, path, service, uuid, flags) { super(bus, path, "org.bluez.GattCharacteristic1"); this.UUID = uuid; @@ -73,12 +105,25 @@ export class BaseCharacteritic extends BaseInterface { @method({}) StartNotify() { - console.log("not definied"); + if (this.startNotify) { + this.startNotify(); + } else { + console.log("not definied"); + } } @method({}) StopNotify() { - console.log("not definied"); + if (this.stopNotify) { + this.stopNotify(); + } else { + console.log("not definied"); + } + } + + notify(value) { + this.Value = value; + Interface.emitPropertiesChanged(this, { Value: value }); } getProperties() { diff --git a/src/ftms-service.js b/src/ftms-service.js new file mode 100644 index 0000000..07bece1 --- /dev/null +++ b/src/ftms-service.js @@ -0,0 +1,137 @@ +import { BaseInterface } from "./base"; +import { BaseService, BaseCharacteritic, BaseApplication } from "./bluez-gatt"; + +export class FTMSService extends BaseService { + UUID = "00001826-0000-1000-8000-00805f9b34fb"; + Primary = true; + + constructor(bus, path) { + super(bus, path); + this.addCharacteristic(new FTMSFeature(bus, this.path + "/char0", this)); + this.addCharacteristic(new IndoorBikeData(bus, this.path + "/char1", this)); + this.addCharacteristic( + new FTMSControlPoint(bus, this.path + "/char2", this) + ); + } +} + +class FTMSFeature extends BaseCharacteritic { + constructor(bus, path, service) { + super(bus, path, service, "00002acc-0000-1000-8000-00805f9b34fb", ["read"]); + } + + read() { + console.log("read FTMS Feature"); + // 32bits features + 32bits target supported + // 0x0a 0x44 = feature: cadence (1), inclinaison (3), hr (10), power (14) + // 0x0a 0x20 = target: inclinaison (1), power (3), simulation parameters (13) + return [0x0a, 0x44, 0x00, 0x00, 0x0a, 0x20, 0x00, 0x00]; + } +} + +class FTMSControlPoint extends BaseCharacteritic { + wind = 0; + grade = 0; + crr = 0; + cw = 0; + + constructor(bus, path, service) { + super(bus, path, service, "00002ad9-0000-1000-8000-00805f9b34fb", [ + "write", + "indicate", + ]); + } + + write(value, options) { + console.log("FTMS Write Control Point"); + console.log(value); + const arraybuf = value.buffer.slice( + value.byteOffset, + value.byteOffset + value.byteLength + ); + const view = new DataView(arraybuf); + + if (value[0] === 0x00) { + // take control + this.notify([0x80, 0x00, 0x01]); + } else if (value[0] === 0x01) { + // reset machine + this.notify([0x80, 0x01, 0x01]); + } else if (value[0] === 0x07) { + // start/resume + this.notify([0x80, 0x07, 0x01]); + } else if (value[0] === 0x11) { + // set simulations param (wind, grade, etc) + const wind = view.getInt16(1, true); + const grade = view.getInt16(3, true); + const crr = view.getUint8(5); + const cw = view.getUint8(6); + + this.wind = wind * 0.001; + this.grade = grade * 0.01; + this.crr = crr * 0.001; + this.cw = cw * 0.01; + + console.log(wind * 0.001); + console.log(grade * 0.01); + console.log(crr * 0.0001); + console.log(cw * 0.01); + + this.notify([0x80, 0x11, 0x01]); + } else { + console.log("FTMS Control point op code " + value[0] + "not supported"); + } + } + + startNotify() { + console.log("FTMS Start Notify CP"); + } + + stopNotify() { + console.log("FTMS Stop Notify CP"); + } +} + +class IndoorBikeData extends BaseCharacteritic { + constructor(bus, path, service) { + super(bus, path, service, "00002ad2-0000-1000-8000-00805f9b34fb", [ + "notify", + ]); + } + + startNotify() { + if (this.Notifying) { + return; + } + + this.Notifying = true; + + this.interval = setInterval(() => { + console.log("FTMS Notif Indoor Bike"); + // 0x44 0x02 = fields included: cadence (2), power (6), hr (9) + // 0x00 0x00 = average speed, always included + // cadence is *2 (resolution 0.5 s^-1) + this.notify([ + 0x44, + 0x02, + 0x00, + 0x00, + Math.round(160 + Math.random() * 10), + 0x00, + Math.round(160 + Math.random() * 40), + 0x00, + Math.round(120 + Math.random() * 10), + ]); + }, 1000); + } + + stopNotify() { + if (!this.Notifying) { + return; + } + + this.Notifying = false; + + clearInterval(this.interval); + } +} diff --git a/src/hr-service.js b/src/hr-service.js new file mode 100644 index 0000000..9aa1747 --- /dev/null +++ b/src/hr-service.js @@ -0,0 +1,45 @@ +import { BaseInterface } from "./base"; +import { BaseService, BaseCharacteritic, BaseApplication } from "./bluez-gatt"; + +export class HRService extends BaseService { + UUID = "0000180d-0000-1000-8000-00805f9b34fb"; + Primary = true; + + constructor(bus, path) { + super(bus, path); + this.addCharacteristic(new HRMeasure(bus, this.path + "/char0", this)); + //this.addCharacteristic(new WriteChar(bus, "/eu/atoy/hrservice/char1", this)); + //this.addCharacteristic(new NotifChar(bus, "/eu/atoy/hrservice/char2", this)); + } +} + +class HRMeasure extends BaseCharacteritic { + constructor(bus, path, service) { + super(bus, path, service, "00002a37-0000-1000-8000-00805f9b34fb", [ + "notify", + ]); + } + + startNotify() { + if (this.Notifying) { + return; + } + + this.Notifying = true; + + this.interval = setInterval(() => { + console.log("HR Notify"); + this.notify([0x06, Math.round(120 + Math.random() * 10)]); + }, 1000); + } + + stopNotify() { + if (!this.Notifying) { + return; + } + + this.Notifying = false; + + clearInterval(this.interval); + } +} diff --git a/src/index.js b/src/index.js index 642cc5e..e7e573f 100644 --- a/src/index.js +++ b/src/index.js @@ -1,84 +1,29 @@ -import { systemBus, Variant, interface as iface } from "dbus-next"; - -const { Interface, method, property } = iface; - -import { BaseService, BaseCharacteritic } from "./bluez-gatt"; +import { systemBus } from "dbus-next"; +import { BaseApplication } from "./bluez-gatt"; import { Advertisement } from "./adv"; -import { BaseInterface } from "./base"; import { Bluez } from "./bluez"; +import { HRService } from "./hr-service"; +import { PWRService } from "./power-service"; +import { FTMSService } from "./ftms-service"; -class Application extends BaseInterface { - constructor(bus, path) { - super(bus, path, "org.freedesktop.DBus.ObjectManager"); - - const gattService = new MyService(bus, "/eu/atoy/service0"); - - this.services = [gattService]; - } - - @method({ outSignature: "a{oa{sa{sv}}}" }) - GetManagedObjects(val) { - const reply = {}; - for (const service of this.services) { - reply[service.path] = { - [service.$name]: service.getProperties(), - }; - - for (const char of service.characteristics) { - reply[char.path] = { - [char.$name]: char.getProperties(), - }; - } - } - return reply; - } -} - -class MyService extends BaseService { - UUID = "139fc001-a4ed-11ed-b9df-0242ac120003"; - Primary = true; - - @property({ signature: "ao" }) - get Characteristics() { - return this.characteristics.map((c) => c.path); - } - - characteristics = []; - +class BikeApplication extends BaseApplication { constructor(bus, path) { super(bus, path); - this.addCharacteristic(new PowerChar(bus, "/eu/atoy/service0/char0", this)); - this.addCharacteristic(new WriteChar(bus, "/eu/atoy/service0/char1", this)); - } -} - -class PowerChar extends BaseCharacteritic { - constructor(bus, path, service) { - super(bus, path, service, "139fc002-a4ed-11ed-b9df-0242ac120003", ["read"]); - } - - read(options) { - return [0xaa, 0xcc]; - } -} - -class WriteChar extends BaseCharacteritic { - constructor(bus, path, service) { - super(bus, path, service, "139fc003-a4ed-11ed-b9df-0242ac120003", [ - "write", - ]); - } - - write(value, options) { - console.log(value); + //this.addService(new HRService(bus, this.path + "/hrservice")); + //this.addService(new PWRService(bus, this.path + "/pwrservice")); + this.addService(new FTMSService(bus, this.path + "/ftmsservice")); } } async function main() { const bus = systemBus(); - const app = new Application(bus, "/eu/atoy"); - const adv = new Advertisement(bus, "/eu/atoy/advertising"); + const app = new BikeApplication(bus, "/eu/atoy"); + const adv = new Advertisement(bus, "/eu/atoy/advertising", [ + //"1818", + //"180d", + "1826", + ]); await bus.requestName("eu.atoy"); @@ -86,10 +31,17 @@ async function main() { const leamgr = obj.getInterface("org.bluez.LEAdvertisingManager1"); const gattmgr = obj.getInterface("org.bluez.GattManager1"); - /*leamgr.RegisterAdvertisement("/eu/atoy/advertising", {}); - console.log("BLE Advertisement registered"); - gattmgr.RegisterApplication("/eu/atoy", {}); - console.log("BLE GATT application registered");*/ + try { + await leamgr.RegisterAdvertisement("/eu/atoy/advertising", {}); + console.log("BLE Advertisement registered"); + } catch (e) { + console.log("Unable to register BLE Advertisement"); + } + + await gattmgr.RegisterApplication("/eu/atoy", {}); + console.log("BLE GATT application registered"); + + return; const bz = new Bluez(); const pafers = await bz.getDevice("PAFERS_53B9A3"); @@ -101,9 +53,9 @@ async function main() { const iscon = await pafers.isConnected(); console.log("connected: " + iscon); - bz.on("interfacesAdded", () => { + /*bz.on("interfacesAdded", () => { - }); + });*/ if (!iscon) { await pafers.connect(); diff --git a/src/power-service.js b/src/power-service.js new file mode 100644 index 0000000..38af999 --- /dev/null +++ b/src/power-service.js @@ -0,0 +1,45 @@ +import { BaseInterface } from "./base"; +import { BaseService, BaseCharacteritic, BaseApplication } from "./bluez-gatt"; + +export class PWRService extends BaseService { + UUID = "00001818-0000-1000-8000-00805f9b34fb"; + Primary = true; + + constructor(bus, path) { + super(bus, path); + this.addCharacteristic(new PWRMeasure(bus, this.path + "/char0", this)); + //this.addCharacteristic(new WriteChar(bus, "/eu/atoy/hrservice/char1", this)); + //this.addCharacteristic(new NotifChar(bus, "/eu/atoy/hrservice/char2", this)); + } +} + +class PWRMeasure extends BaseCharacteritic { + constructor(bus, path, service) { + super(bus, path, service, "00002a63-0000-1000-8000-00805f9b34fb", [ + "notify", + ]); + } + + startNotify() { + if (this.Notifying) { + return; + } + + this.Notifying = true; + + this.interval = setInterval(() => { + console.log("Power Notify"); + this.notify([0x00, 0x00, Math.round(170 + Math.random() * 30), 0x00]); + }, 1000); + } + + stopNotify() { + if (!this.Notifying) { + return; + } + + this.Notifying = false; + + clearInterval(this.interval); + } +}