rwaldron / johnny-five

JavaScript Robotics and IoT programming framework, developed at Bocoup.
http://johnny-five.io
Other
13.26k stars 1.76k forks source link

How to close connection to a board? #617

Closed chellem closed 8 years ago

chellem commented 9 years ago

Hello,

Is it possible to dynamically close a connection to a board ?

if yes, can you explain me how?

Thanks and Regards.

henricavalcante commented 9 years ago

I wanna know too, how to close a connection and reconnect.

rwaldron commented 9 years ago

While it's possible to programmatically disconnect and reconnect a serialport, the resulting program state would be nonsensical and effectively "corrupt". Johnny-Five is coordinating the physical state of the hardware with an in memory object representation—if the physical state is completely reset, the program can only go so far to synchronize and resolve that. Consider the following program, with the hypothetical connect and reconnect methods:

var five = require("johnny-five");
var board = new five.Board();
var servos = [];

board.on("ready", function() { 
  //  "ready" emits upon confirmation of board capabilities and receipt of initial state

  var servo = new five.Servo(9);
  servos.push(servo);

  this.disconnect();
  this.connect();
});

I'd be interested in learning more about the use cases that lead to this request.

divanvisagie commented 9 years ago

@rwaldron I think the idea here is to open a connection , do stuff and then close them(like db connections), which may be practical if you have some sort of web server that has multiple controllers like if you are using express-enrouten

scottgonzalez commented 9 years ago

So it sounds like all you want is the method to disconnect, not to reconnect. When reconnecting, you would just create a new board instance. Is that correct?

If so, can't we have the boards track their components (perhaps though Board.mount()) and do some cleanup on disconnect? This seems like it's technically possible, but the question is does the cost of extra logic for every component outweigh the benefit of being able to disconnect? Speaking from experience in jQuery UI, I can say that providing the ability to destroy a widget adds a fair amount of additional logic, testing, and diligence. I'm not familiar enough with this code base to know how much it would add here, but it may not be worth the effort.

I suppose it's possible that it will be almost as simple as just removing the component data from some maps, or potentially just switching to weak maps.

rwaldron commented 9 years ago

It's not that simple at all: all references to any component instance that holds any reference to any aspect of a board instance, or any of its properties, that exist in user code (way outside of Johnny-Five's control) would have to become unreachable, delete'ed or set to null.

rwaldron commented 9 years ago

Also, consider that "connect" and "disconnect" don't make any sense for code that runs on the board. What does it mean to call board.disconnect() on a beagle bone?

scottgonzalez commented 9 years ago

It's not that simple at all: all references to any component instance that holds any reference to any aspect of a board instance, or any of its properties, that exist in user code (way outside of Johnny-Five's control) would have to become unreachable, delete'ed or set to null.

My impression was that this would actually be very likely to happen, given the example of express-enrouten. I assumed the use case was instantiating the board, performing some action, and disconnecting all within the scope of a single request. Once the request is complete, everything gets garbage collected (assuming Johnny-Five is cleaning up internally).

scottgonzalez commented 9 years ago

Also, consider that "connect" and "disconnect" don't make any sense for code that runs on the board. What does it mean to call board.disconnect() on a beagle bone?

I honestly have no idea, having no experience with that, but I would assume it just means that the board instance is no longer usable. So you wouldn't actually break the connection to the board, but all the references would be destroyed.

boneskull commented 9 years ago

@rwaldron

I also have this use case. I want to "hot-plug" physical boards:

  1. I'm running a web server.
  2. I'll probably leverage Serial.detect() or something to find a new device
  3. When the device disconnects, it will need to be destroyed in memory.

My server knows which "component" objects belong to which Board instances, and it should be trivial to simply zap the Board and everything with it. At least, in theory.

(Tangentially, does something like an Arduino have a unique identifier which the serialport module can read?)

JpEncausse commented 9 years ago

I have the same issue. I'd like to close the Board to free COM port and create a new one.

rwaldron commented 9 years ago

Copy this and run it in your console please:

function disconnect() {
  memory.length = 0;
  reference = null;
}

var memory = [];
var reference = {
  write: function(value) {
    console.log(value);
  }
};

memory.push(reference);

function Component() {
  this.reference = reference;
  this.value = 0;

  setInterval(function() {
    this.reference.write(this.value ^= 1);
  }.bind(this), 1000);
}

var a = new Component();
var b = new Component();

var components = [a, b];

reference.cache = components;

disconnect();

console.log(reference);

The result will illustrate the problem I've been trying to explain.

boneskull commented 9 years ago

Implement some sort of "destroy" event. Call Board#disconnect() and it will broadcast the event to all of its components. Components would then be responsible for listening for this event, and clean up after themselves. (this is how AngularJS does it)

rwaldron commented 9 years ago

That's a huge amount of work for a feature that's only useful to one of the many platforms that Johnny-Five runs on. Furthermore, the Angular comparison is bogus, because not even Angular can go and find all references, including alias bindings, to eliminate them.

boneskull commented 9 years ago

(re: AngularJS, it's just a part of it--think using jQuery/jqLite to bind an event to a DOM node--when that DOM node's Scope is destroyed, you will want to unbind the event b/c of memory leaks. You subscribe to a $destroy event and unbind within its handler.)

I'm not sure I understand why this is only applicable to one platform? How is a BBB any different (and I don't mean semantics)?

I'm not going to argue that it may be a lot of work, but I'm not seeing a way around it if anyone wants to embed J5 in another application (like a web app). The alternative is eschewing J5 for its lower-level IO libraries.

nebrius commented 9 years ago

One possible solution for hot-plugging, etc that doesn't require any change to J5: I think you should be able to use the child_process or cluster modules to isolate the board to its own process, thus allowing you to easily destroy the board by destroying the process. Disclaimer: I have not actually tried doing this, but I would be surprised if it didn't work.

scottgonzalez commented 9 years ago

Perhaps the way to move forward is for one of the interested parties to send a PR with a proof of concept that implements the destroy behavior at the high level, plus one or two components. Then provide an example web app that repeatedly connects to a board, initializes some components, then disconnects in response to a request.

scottgonzalez commented 9 years ago

@bryan-m-hughes's suggestion also sounds reasonable. I wonder if you could even get away with just using a new VM context instead of a new process.

nebrius commented 9 years ago

@scottgonzalez do you know if Node.js provides nice, easy to use APIs for spinning up new VM contexts? I'm not familiar with any, but then again I've never gone looking for them (I use cluster personally for this stuff)

rwaldron commented 9 years ago

@scottgonzalez I agree.

@boneskull the semantics of "disconnect" don't make any sense if the code is running directly on the board that it's controlling. What is it disconnecting?

boneskull commented 9 years ago

@rwaldron

I'm not sure I understand why this is only applicable to one platform? How is a BBB any different (and I don't mean semantics)?

:smile: Perhaps destroy more appropriate?

nebrius commented 9 years ago

Perhaps destroy more appropriate?

I dunno, I feel like even that doesn't really make sense in the Node.js world. It's not a common idiom unless you're dealing with something that has a connection/stream, which BBB, RPi, Yun, etc don't have.

rwaldron commented 9 years ago

What is it destroying?

boneskull commented 9 years ago

objects

nebrius commented 9 years ago

to clarify my point above: it's not The Node Way (tm) to have a manual method you call to destroy/disconnect/etc, unless it's a stream. BBB, RPi, etc aren't streams, therefore any method like that just isn't The Node Way (tm).

This isn't to say we must religiously follow the usual way of doing so, of course, but deviating from it requires compelling arguments. I don't think we're at that point, especially since I'm about 99% certain that destroy/disconnect/etc isn't even necessary for things where there is a Firmata connection since we can use cluster to achieve the same result.

scottgonzalez commented 9 years ago

@scottgonzalez do you know if Node.js provides nice, easy to use APIs for spinning up new VM contexts?

https://nodejs.org/api/vm.html

nebrius commented 9 years ago

@scottgonzalez Nice! I'm actually not familiar with that API (ooo new Node API to learn, that doesn't happen often :))

Maybe it would be a good idea to test this process/context isolation approach, and if it works, add some docs on best practices for this use case.

boneskull commented 9 years ago

@bryan-m-hughes I think I understand how this would work with child_process, but I don't know jack shit about cluster. Same idea, basically?

It's very likely I have an XY problem here. I really just want to hot-plug stuff and not suffer memory leaks when removing. I don't really care how we solve that problem.

nebrius commented 9 years ago

@boneskull yeah, cluster is similar, just higher level and awesome when you want to manage a group of these things.

EDIT: to be clear, it's similar to child_process.fork, which is different than exec/spawn.

nebrius commented 9 years ago

The basic approach would be (assuming cluster), for the master process to just be a controller/manager. No J5 code in it. It would spawn off one or many child processes where the J5 code goes. Whenever it's time to disconnect, you can send messages back and forth between the master and J5 process to coordinate a shutdown of the J5 process and spawn up a new one once it's done.

boneskull commented 9 years ago

@bryan-m-hughes I should be able to test this soon. I should be able to figure it out on my own, but if you can provide me some direction or a starting point, that may speed things along.

nebrius commented 9 years ago

For an example of cluster being used in this fashion, check out https://github.com/bryan-m-hughes/aquarium-control/tree/master/server. It's ancient (way predates raspi-io, so no J5), but still "in production" controlling the lights of my aquarium after 2 years, so obviously the approach works well. Just pretend the src/controller.js file is filled with J5 code :D

boneskull commented 9 years ago

@bryan-m-hughes :+1:

kenken64 commented 8 years ago

I tried off and on the board

rwaldron commented 8 years ago

@gbaumgart

I still can't find board.disconnect.

There is no such thing as "disconnect" for the boards that Johnny-Five runs directly on—how can a program "disconnect" from something that itself controls execution?

Is that on purpose or are the developers incapable to implement proper lifecycle methods? I can't believe it.

I don't appreciate the tone you've brought to the discussion, please have a look at the Code Of Conduct

nebrius commented 8 years ago

@gbaumgart Johnny-five does not have a close/disconnect/destroy method for the reasons presented above.

As @rwaldron mentioned, please take a look at the Code of Conduct and moderate your posts. If not, we may need to take moderation actions ourselves.

rwaldron commented 8 years ago

so how to disconnect from the board? I can't believe the developers build only a one-way road.

There has been a lot of discussion, in this thread, as to why this is.

Just not johnny-five, there must be something I missed, even after reading the whole source code ;-)

You would never find that answer in the source code, because it's not there. The answer is in this thread.

rwaldron commented 8 years ago

@gbaumgart perhaps you can explain your use case, detailing why you need to disconnect while the program is running?

nebrius commented 8 years ago

The reason we don't have a disconnect/reset is because doing a software disconnect leaves the Arduino (or other firmata device) in an unknown state. If you kill the process, it's analogous to killing an ssh process to a remote server. Whatever you were running will still be running on the Arduino, only now we don't have control or even really know what it's doing.

To implement a proper disconnect/reset/etc, we would first have to add a "reset" command in firmata, which is a different project. Once that's done, then we could begin to figure out how to implement it in Johnny-Five.

rwaldron commented 8 years ago

Based on what I see in that UI, it's not clear to me what Disconnect and Connect are supposed to do? If I were building this app, I would make "Start" and "Stop", because that would work for all platforms (I see that you have Raspberry Pi there on the left). For Arduino, "Start" would open the serialport and execute the program "on ready"; "Stop" would sigint the program, implicitly closing the serialport. For Raspberry Pi, if the code is running directly on the board, "Start" would be an alias to node program.js and "Stop" is a sigint.

rwaldron commented 8 years ago

To implement a proper disconnect/reset/etc, we would first have to add a "reset" command in firmata, which is a different project.

There is already a SYSTEM_RESET command, but all it does is reset the internal state on the remote device. That's not really the problem, because we could already just do that by looping through all pins and setting everything to low and then send a SYSTEM_RESET instruction; the problem is that the local JavaScript process has objects in memory that think they know about and have control over state that it thinks exists on the board, but no longer does. I'm fundamentally opposed to the introduction of a potentially dangerous operation that would leave a program and remote device in a state of complete chaos. I refuse to be the one that make it possible for someone to get electrocuted by a "reset" relay that had some object reference floating around changing its state.

Edit: that description was not quite accurate, so stricken.

nebrius commented 8 years ago

Ah, my mistake.

rwaldron commented 8 years ago

Don't sweat it, there's a lot of moving parts here :)

reconbot commented 8 years ago

Looks like we haven an answer here. In any case I'm going to close this due to age.

rwaldron commented 7 years ago

@gbaumgart from my comment above

the problem is that the local JavaScript process has objects in memory that think they know about and have control over state that it thinks exists on the board, but no longer does. I'm fundamentally opposed to the introduction of a potentially dangerous operation that would leave a program and remote device in a state of complete chaos. I refuse to be the one that make it possible for someone to get electrocuted by a "reset" relay that had some object reference floating around changing its state.

If you don't care about that, then just call board.io.reset()

mdingena commented 6 years ago

At risk of necromancing a thread, I'm answering @rwaldron 's question

perhaps you can explain your use case, detailing why you need to disconnect while the program is running?

I'm trying to use Johnny-Five to control boards for home automation. These boards are connected via WiFi to a Raspberry Pi running a Node instance. Because some of these boards will disappear into ceilings and walls, I'd like them to reconnect whenever they lose WiFi signal or power.

When the board momentarily loses power, upon restarting it will re-run the Firmata code and connect to the Node instance as per usual. However, Node now creates a second new five.board, and both the new and old instances do not seem to control the board at all.

Below is some pseudo-code from my use case.

// Node.js
mqtt.on( 'published', function( packet, client ) {
    switch( packet.topic ) {
        case 'hello':
            const json = JSON.parse( packet.payload.toString() );
            let board = new five.Board({
                port : new EtherPortClient({
                    host : json.ipAddress,
                    port : json.port
                }),
                timeout: 1e5,
                repl: false
            });
            board.on( 'ready', function() {
                console.log( "READY!" );
                var relay = new five.Relay( 5 );
                this.loop(2000, () => {
                    relay.toggle();
                    console.log( relay.isOn );
                });
            });
            break;
    }
});

// StandardFirmataWifi + these functions below
#include <PubSubClient.h>
#define DOMUS_HEARTBEAT_INTERVAL 6000
WiFiClient espClient;
PubSubClient client(espClient);
IPAddress domusMqttServerIp(192, 168, 178, 201);
unsigned long domusHeartbeatMillis = millis();

void helloDomus() {
  IPAddress ip = WiFi.localIP();
  DEBUG_PRINTLN("Sending my connection details to Domus over MQTT");
  String json = "{\"ipAddress\":\"" + ip.toString() + "\",\"port\":\"" + SERVER_PORT + "\",\"type\":\"board\"}";
  int json_len = json.length() + 1; 
  char char_array[json_len];
  json.toCharArray(char_array, json_len);
  client.publish( "hello", char_array );
}

void initMqtt() {
  client.setServer( domusMqttServerIp, 3002 );
  while( !client.connected() ) {
    DEBUG_PRINT( "Connecting to Domus MQTT..." );
    if( client.connect( "Board: Wemos D1 Mini" ) ) {
      DEBUG_PRINTLN( " connected" );
    } else {
      DEBUG_PRINT( " failed, rc=" );
      DEBUG_PRINT( client.state() );
      DEBUG_PRINTLN( " try again in 5 seconds" );
      delay( 5000 );
    }
  }
  helloDomus();
}

void domusHeartbeat( unsigned long currentMillis ) {
  if( currentMillis - domusHeartbeatMillis > DOMUS_HEARTBEAT_INTERVAL ) {
    if( client.state() != 0 ) {
      client.disconnect();
      initMqtt();
    }
    domusHeartbeatMillis += DOMUS_HEARTBEAT_INTERVAL;
    client.publish( "HB", "1" );
  }
  client.loop();
}

The Node console correctly outputs true/false every 2 seconds when the board connects for the first time. The relay which is connected to it happily click/clacks accordingly. Then, while the Node program is still running, I press the reset button on my board (or unplug the USB powering it, same effect). While the board is unpowered, Node console continues to incorrectly output the relay.isOn state. Then, when the board powers up and connects to Node, a new five.board is created and now I have two sets of true/false in my Node console. Also, the relay is no longer toggling, indicating that both five.board instances are not actually doing what they are supposed to do.

For me, it makes sense to use my heartbeats over MQTT to keep such a board instance alive. When a heartbeat times out (due to board losing power and not sending its heartbeat), the Node program could destroy that board instance. When the board reconnects, a new five.board instance will be created for it, and it will be the only instance.

That is the theory at least, because I've been unable to test it. I can't seem to get rid of the five.board instance correctly.

Instead of using a heartbeat, I've tried to add the board to the program's memory by its IP address. When the board reconnects and uses the same IP address, I first remove the first five.board instance before creating the new one, like so:

export default class {
    constructor( db ) {
        this._db = db;
        this._boards = {};
    }

    remove( ip ) {
        console.log( "[Boards] Removing board: " + ip );
        return delete this._boards[ ip ];
    }

    add( ip, port, type ) {
        this.remove( ip );
        console.log( "[Boards] Adding board: " + ip );
        const board = this._board( ip, port );
        board.on( 'ready', function() {
            console.log( "READY!" );
            var relay = new five.Relay( 5 );

            this.loop(2000, () => {
                relay.toggle();
                console.log( relay.isOn );
            });
        });
        return this._boards[ ip ] = board;
    }

    _board( ip, port ) {
        return new five.Board({
            port : new EtherPortClient({
                host : ip,
                port : port
            }),
            timeout: 1e5,
            repl: false
        });
    }
}

But this doesn't seem to work. Here is a snapshot of my Node console.

  1. Board connects to Node for first time. IP address gets added to _boards object with IP address as key.
  2. Johnny-Five board ready event fires. Relay is being toggled correctly.
  3. I unplug the board, yet true/false is still being printed by console.log(relay.isOn);.
  4. Board reconnects. My code delete _boards[ ip ] before saving the new five.board instance.
  5. The output from the first board is still shown, even after delete.
  6. Johnny-Five board ready event fires for second instance. Now two sets of true/false are output.

image

I was hoping this is either a good use case for having this feature, OR I missed something else that will fix this. Thanks so much for your work on Johnny-Five. I'm enjoying programming with this for a while now :)

Watchdogx7 commented 6 years ago

Greetings to all

I venture to look for this solution for the same asundo.

I'm starting a project. Start using the Universal Serial port, and now go to the point of using bluetooth. Soon I will deploy a node using ethernet or Wifi shield. The problem with the USB connection is the distance, in bluetooth, the interference. Ethernet and WiFi would solve this problem and allow proper synchronization. Even rf transmitters can be used if the problem is distance.

The problem with radio frequency signals is that they end up being a health problem for man. For what I consider that a constant synchronization in a scalable project can produce severe damages in the health of those who are close to this technology.

Ergo, I think that ethernet would be, for me, the solution of distance and reduction of harmful signals for greeting

However, in the test environment it is good to handle everything. And that's why I'm testing using a BT HC-05 module. The signals in bluetooth are very bulnerable in terms of interference and distance, so keeping the connection constant is difficult. So look for the way to disconnect the plate I use (arduino), first so it does not transmit all the time and second because it can get disconnected the same. (by the way ... the board.disconnect ( ); command does not work at the moment ... I could not use it)

I ask them. If this library uses serial port to function and create the instances. Would not it be good practice to keep the current instance always running, separate the instance Board of the instance synchronized by serial port?

There should be a means by which the Board instance is always running and using the USB port or the network IP, and port synchronization will be carried out serial port. Serial port could use a kind of relationship that allows assigning the same board to the Board instance.

What do you think? this can be possible?

scottgonzalez commented 1 year ago

So it turns out this is already possible to do. While Johnny-Five always tracks the serial ports being used, it only checks those ports if automatic port detection is being used. So if you want to be able to "disconnect" from a board, you can provide your own SerialPort instance when instantiating the board. Then you can control the opening and closing the port.

Here's a code snippet showing that we can connect, disconnect, then connect again. It displays the firmware information of the board to show that it has actually connected and is working.


function connectBoard(port) {
  return new Promise((resolve) => {
    const serialport = new SerialPort({
      baudRate: 57600,
      highWaterMark: 256,
      path: port,
    });
    const board = new five.Board({ port: serialport });
    board.on('ready', () => {
        console.log(board.io.firmware);
        serialport.close();
        resolve();
    });
  });
}

(async () => {
    const port = (await SerialPort.list())
        .map(({ path }) => path)
        .filter((port) => {
            return /usb|acm|^com/i.test(port);
        })[0];

    await connectBoard(port);
    await connectBoard(port);
})();

When the serial port is closed, the board is basically going to be frozen in its current state. It is your responsibility to do all clean up and turn off any components that should be turned off prior to closing the port.

FWIW, I'm only using this logic to detect which specific devices are connected to the host. I'm uploading custom firmata instances so I can identify the devices based on the firmware. Then the user can choose which device to start and I will already know which port to use and what configuration options to show them.