Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New paradigm for handling attached devices #61

Merged
merged 65 commits into from
Feb 10, 2020
Merged

Conversation

nathankellenicki
Copy link
Owner

@nathankellenicki nathankellenicki commented Dec 7, 2019

This is a work in progress PR to implement some of the changes discussed in #49.

Currently, large swathes of code are commented out. The approach I'm taking for this is a bottom up approach - starting with minimal functionality and adding upon it, until feature parity is nearly reached.

Currently implemented:

  • Attach/detach events
  • BasicMotor setSpeed and brake (to demonstrate devices sending data to the hub)
  • Basic hierarchy of Device/BasicMotor/ControlPlusLargeMotor etc.

In addition, this starts to implement an auto-subscribe mechanism (originally suggested by @nutki in #60). Currently it is a Big Ugly Switch Statement(TM), however my hope is that this can be reduced and automated with some of the logic implemented by @aileo in #60.

I've added a temporary example at examples/new_device_test.js to demonstrate how the new paradigm would work.

this.on("newListener", (event) => {
switch (event) {
case "color":
this.subscribe(0x00);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should avoid using hex notation as all const are in decimal and I see modes as almost const.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case I don't mind either way. But for Consts.DeviceType I would actually prefer to use hex literals as this is how they are listed in LWP doc.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a good question. I think for the moment having the values hardcoded is a placeholder, I'd like to put all consts in a file somewhere. So - hex or decimal? Perhaps hex as @nutki suggests?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, like @nutki wrote hex make more sense regarding the doc. Even more if we had some const with all message headers and all.

Plus, the end users can use whatever format they want when they call methods.

Copy link
Owner Author

@nathankellenicki nathankellenicki Dec 17, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aileo I've started getting rid of the hex notation (and decimals too) and implementing modes as enums. And as suggested by @nutki somewhere else, subscriptions are automatically made when attaching the associated event handler. It's much cleaner, thanks!

* @param {number} speed For forward, a value between 1 - 100 should be set. For reverse, a value between -1 to -100. Stop is 0.
* @returns {Promise} Resolved upon successful completion of command.
*/
public setSpeed (speed: number) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since basic motors don't have a tachometer, this is actually setPower. I guess this makes it easier for the API, but would you then have setSpeed and setPower for tacho motors or just setSpeed?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I guess I haven't liked power as I think for most users speed is a more appropriate term for what they want, even if it isn't exactly the same. I don't this library should attempt to exactly emulate LWP functionally, instead adding a user friendly layer on top of it.

Perhaps we could have setSpeed(50) set power by default, however with an options object (such as setSpeed(50, { maxPower: 50 }) to switch to maintaining the speed with power?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nutki I've gone with setPower and done a 180 on this. :) Will implement setSpeed appropriately later.

src/device.ts Outdated
this._portId = portId;
this._type = type;
this.hub.on("detach", (device) => {
if (device.portId === this.portId) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't device === this be better? This way old disconnected devices would not generate detach events.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Will change this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a more straightforward approach would be to remove this listener on detach.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nutki Listener is now removed on detach.

src/device.ts Outdated
}

public send (data: Buffer, characteristic: string = Consts.BLECharacteristic.LPF2_ALL, callback?: () => void) {
this.hub.send(data, characteristic, callback);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this send LPF2_ALL characteristic messages to WeDo2 hub? As a side note, I am not sure if continued support of the WeDo hub makes sense. The new devices will not be supported on it anyway since the FW is not updatable (according to a Lego designer) and it has limited availability (which also makes it expensive).

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now, yes, eventually I want to have each device method (ie. setSpeed) switch characteristics and message based on hub type.

Regarding the WeDo 2.0 support - I've also been thinking about this recently, but I think I want to keep support personally, as I actually use a few WeDo 2.0 hubs myself. :) They are currently the only hub type that offers an AC adapter so it can run forever.

On the topic of firmware support, that's correct - however most newer devices can be supported on it in some way. See the compatibility matrix in the readme of this project.

Eventually I might drop it though - especially if Lego releases a hub with an AC adapter.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I understand that WeDo support is mostly for people who already own one. Iteresting about the AC adapter though, I have not heard about it. Is this stand alone or the one with rechargeable battery allows to bypass the battery? Long time ago I especially for that purpose bought Lego RCX with an AC plug, even though I already had 2 without :)

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I understand that WeDo support is mostly for people who already own one. Iteresting about the AC adapter though, I have not heard about it. Is this stand alone or the one with rechargeable battery allows to bypass the battery? Long time ago I especially for that purpose bought Lego RCX with an AC plug, even though I already had 2 without :)

It's the rechargeable battery pack for the WeDo 2.0 hub - it has a built in port to plug in an AC adapter. :)

https://education.lego.com/en-us/products/wedo-2-0-add-on-power-pack-by-lego-education/5004838

That will allow you to keep a WeDo 2.0 hub running forever.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've implemented some WeDo2 support into some devices - it's actually on par with master feature wise, but I may implement some more modes, once I test to see what works and doesn't!

It's much simpler to implement WeDo2 support with each motor/sensor having its own device class.

src/hub.ts Outdated

protected _ports: {[port: string]: Port} = {};
protected _virtualPorts: {[port: string]: Port} = {};
protected _attachedDevices: Device[] = [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the current use I would say a map of portId to Device would make more sense than a flat array for _attachedDevices.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so too. But object literals don't accept numbers as key type, and I was hoping not to use ES6 maps. I guess we could though ("Just shut up and do it, Nathan!")

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm totally wrong - they do support numbers as keys/property names, just not in initializers. I'll change to use this. :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean initializes as in: ({0:'Test'})[0]? It does work for me. What's wrong with ES6 Maps, btw? I strongly prefer them to using POJOs for maps, especially in Typescript.

src/pupremote.ts Outdated
Comment on lines 42 to 45
this.type = Consts.HubType.POWERED_UP_REMOTE;
this._ports = {
"LEFT": new Port("LEFT", 0),
"RIGHT": new Port("RIGHT", 1)
this._type = Consts.HubType.POWERED_UP_REMOTE;
this._portNames = {
"LEFT": 0,
"RIGHT": 1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks repetitive over hubs, does it make sense to use the same approach you have on devices ?

super(device, autosubscribe, Consts.HubType.POWERED_UP_REMOTE, {
    "LEFT": 0,
    "RIGHT": 1
});

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, perhaps so. To be honest I’ve barely touched the hub setup, I’ve been focused on the device abstraction. But they will get a revamp too.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also now done - #61 (comment)

Comment on lines 34 to 40
sensor = hub;

sensor.on("distance", (_, distance) => {
if (distance < 5 && !ramping) {
await stopTrain();
}
});
Copy link
Contributor

@aileo aileo Dec 9, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This listen a device (sensor) event on the hub, does it mean we support device events at the hub level ?

If it does not call the same method on the device, it does not subscribe to the correct mode.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, I shouldn’t have committed that! :) It’s for my house - designed to run on the old code (master). But I haven’t though more about supporting that as of now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes a nice example !

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the new waitForDeviceByType should allow you to migrate this to the new paradigm

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah - it was an idea that only occurred to me last night while thinking about other stuff, but it solves a problem that many people have, ie. not knowing when to call getDeviceByType without watching for an attach event, by which point it's provided to you anyway. It just makes it easier to maintain a more sequential coding pattern, which is easier for a lot of people.

Copy link
Owner Author

@nathankellenicki nathankellenicki Dec 18, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's also how I was thinking this could support internal motors/functions on a hub rather on device. ie. If a boost internal motor has a method setSpeed(speed), I can create a lightweight wrapper on the hub called setSpeed(port, speed), which simply passes through the values to the method on the device after using waitForDeviceByPort.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome

Comment on lines 39 to 44
this._portNames = {
"A": 0,
"B": 1,
"C": 2,
"D": 3,
"TILT": 58
Copy link
Contributor

@aileo aileo Dec 12, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think port names/id should appear in consts as they are constant and the same over same type hubs. It would also help developers like @ractive and me determine which port we should look at in our projects.

this comes from here and here

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've now created a const (exported under the hub namespace) for each hub, accessible via PoweredUP.WeDo2SmartHub.PortMap, etc. This is also passed to the root hub object at constructor time. It's also available at runtime via a ports getter, ie. hub.ports = ["A", "B", ...].

Comment on lines +43 to +56
angle = normalizeAngle(angle);
if (angle < -135) {
return -180;
}
if (angle < -45) {
return -90;
}
if (angle < 45) {
return 0;
}
if (angle < 135) {
return 90;
}
return -180;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would go for a more mathematical approach :

export const roundAngleToNearest90 = (angle: number) => {
    return normalizeAngle(Math.round(angle/90)*90);
}

@nathankellenicki
Copy link
Owner Author

I think at this point this PR has reached feature parity with what was there before. I'd love some help testing it before I end up approving it and merging it to master and pushing out a new release.

I'm hoping to do this soon - especially as my copy of Spike Prime is scheduled to be delivered today, so I'll have some new devices to add support for shortly! :)

@aileo
Copy link
Contributor

aileo commented Jan 30, 2020

I'd love some help testing it before I end up approving it and merging it to master and pushing out a new release.

Do you have any testing plan ? I have no idea where to start.

Aslo, I can only test the web-bluetooth part with my current setup.

@nathankellenicki
Copy link
Owner Author

I'd love some help testing it before I end up approving it and merging it to master and pushing out a new release.

Do you have any testing plan ? I have no idea where to start.

Aslo, I can only test the web-bluetooth part with my current setup.

My testing plan up until now has been quite simple - for every hub, try every sensor in every mode, and every motors output. :D It's not especially quick.

Thus far I'm happy with this PR, though more testing (both similar to the plan and real-life application testing) I'm always thankful for!

I think the main things I need to do now are update the documentation and JSDoc comments, and update the samples.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants