fabmo / fabmo-engine Goto Github PK
View Code? Open in Web Editor NEWThe FabMo Engine - A software and apps ecosystem for digital fabrication.
Home Page: http://gofabmo.org/
License: Apache License 2.0
The FabMo Engine - A software and apps ecosystem for digital fabrication.
Home Page: http://gofabmo.org/
License: Apache License 2.0
Currently, when the homing routine is initiated, the units change to the default system units of millimeters.
To reproduce: Home the tool, watch the units change in the position feedback.
The units change back once homing is complete, but the jump is a little unsettling.
in routes.js :
server.get(/.*/, restify.serveStatic({
directory: './static'
}));
Shouldn't we separate static server from the API ?
I think that it's two different things...
I tried the G01 command and it doesn't work, but the G00 is working.
I tried to run the file for cutting a circle of radius 1 center in x2y4 and the tool is moving too far.
Could it be a problem with the configuration file ?
I got a lot of G2_ERR in the console running the Engine...
G2_ERR: -1,JSON_PARSE_ERROR,Could not parse response: {"r":{"key":0},"f":[1,40,10,3777]}(TypeError: Cannot read property '0' of undefined)
G2_ERR: -1,JSON_PARSE_ERROR,Could not parse response: {"r":{"key":0},"f":[1,40,10,3777]}(TypeError: Cannot read property '0' of undefined)
G2_ERR: -1,JSON_PARSE_ERROR,Could not parse response: {"r":{"key":1},"f":[1,40,10,9520]}(TypeError: Cannot read property '0' of undefined)
G2_ERR: -1,JSON_PARSE_ERROR,Could not parse response: {"r":{"key":2},"f":[1,40,10,2100]}(TypeError: Cannot read property '0' of undefined)
G2_ERR: -1,JSON_PARSE_ERROR,Could not parse response: {"r":{"key":2},"f":[1,40,12,2102]}(TypeError: Cannot read property '0' of undefined)
G2_ERR: -1,JSON_PARSE_ERROR,Could not parse response: {"r":{"key":2},"f":[1,40,12,2102]}(TypeError: Cannot read property '0' of undefined)
G2_ERR: -1,JSON_PARSE_ERROR,Could not parse response: {"r":{"key":2},"f":[1,40,12,2102]}(TypeError: Cannot read property '0' of undefined)
G2_ERR: -1,JSON_PARSE_ERROR,Could not parse response: {"r":{"key":0},"f":[1,40,25,3813]}(TypeError: Cannot read property '0' of undefined)
G2_ERR: -1,JSON_PARSE_ERROR,Could not parse response: {"r":{"key":0},"f":[1,40,25,3813]}(TypeError: Cannot read property '0' of undefined)
G2_ERR: -1,JSON_PARSE_ERROR,Could not parse response: {"r":{"key":0},"f":[1,40,25,3813]}(TypeError: Cannot read property '0' of undefined)
G2_ERR: -1,JSON_PARSE_ERROR,Could not parse response: {"r":{"key":10},"f":[1,40,11,3117]}(TypeError: Cannot read property '0' of undefined)
G2_ERR: -1,JSON_PARSE_ERROR,Could not parse response: {"r":{"key":10},"f":[1,40,11,3117]}(TypeError: Cannot read property '0' of undefined)
G2_ERR: -1,JSON_PARSE_ERROR,Could not parse response: {"r":{"key":10},"f":[1,40,11,3117]}(TypeError: Cannot read property '0' of undefined)
G2_ERR: -1,JSON_PARSE_ERROR,Could not parse response: {"r":{"key":360},"f":[1,40,12,7544]}(TypeError: Cannot read property '0' of undefined)
G2_ERR: -1,JSON_PARSE_ERROR,Could not parse response: {"r":{"key":360},"f":[1,40,12,7544]}(TypeError: Cannot read property '0' of undefined)
G2_ERR: -1,JSON_PARSE_ERROR,Could not parse response: {"r":{"key":360},"f":[1,40,12,7544]}(TypeError: Cannot read property '0' of undefined)
G2_ERR: -1,JSON_PARSE_ERROR,Could not parse response: {"r":{"key":720},"f":[1,40,12,4981]}(TypeError: Cannot read property '0' of undefined)
G2_ERR: -1,JSON_PARSE_ERROR,Could not parse response: {"r":{"key":720},"f":[1,40,12,4981]}(TypeError: Cannot read property '0' of undefined)
G2_ERR: -1,JSON_PARSE_ERROR,Could not parse response: {"r":{"key":720},"f":[1,40,12,4981]}(TypeError: Cannot read property '0' of undefined)
G2_ERR: -1,JSON_PARSE_ERROR,Could not parse response: {"r":{"key":5000000},"f":[1,40,16,5927]}(TypeError: Cannot read property '0' of undefined)
G2_ERR: -1,JSON_PARSE_ERROR,Could not parse response: {"r":{"key":5000000},"f":[1,40,16,5927]}(TypeError: Cannot read property '0' of undefined)
G2_ERR: -1,JSON_PARSE_ERROR,Could not parse response: {"r":{"key":5000000},"f":[1,40,16,5927]}(TypeError: Cannot read property '0' of undefined)
G2_ERR: -1,JSON_PARSE_ERROR,Could not parse response: {"r":{"key":6},"f":[1,40,10,5581]}(TypeError: Cannot read property '0' of undefined)
G2_ERR: -1,JSON_PARSE_ERROR,Could not parse response: {"r":{"key":8},"f":[1,40,10,3904]}(TypeError: Cannot read property '0' of undefined)
G2_ERR: -1,JSON_PARSE_ERROR,Could not parse response: {"r":{"key":3},"f":[1,40,13,4682]}(TypeError: Cannot read property '0' of undefined)
G2_ERR: -1,JSON_PARSE_ERROR,Could not parse response: {"r":{"key":{},"f":[1,40,105,1952]}(SyntaxError: Unexpected end of input)
Saving: temp.gc
Running file
Deleting file
G2_ERR: -1,JSON_PARSE_ERROR,Could not parse response: {"r":{},"f":[1,64,22,9242]}(TypeError: Cannot read property '0' of undefined)
G2_ERR: -1,JSON_PARSE_ERROR,Could not parse response: {"r":{},"f":[1,64,26,9246]}(TypeError: Cannot read property '0' of undefined)
G2_ERR: -1,JSON_PARSE_ERROR,Could not parse response: {"r":{},"f":[1,64,11,9210]}(TypeError: Cannot read property '0' of undefined)
G2_ERR: -1,JSON_PARSE_ERROR,Could not parse response: {"r":{},"f":[1,64,4,6370]}(TypeError: Cannot read property '0' of undefined)
G2_ERR: -1,JSON_PARSE_ERROR,Could not parse response: {"r":{},"f":[1,64,10,9209]}(TypeError: Cannot read property '0' of undefined)
G2_ERR: -1,JSON_PARSE_ERROR,Could not parse response: {"r":{},"f":[1,64,4,6370]}(TypeError: Cannot read property '0' of undefined)
G2_ERR: -1,JSON_PARSE_ERROR,Could not parse response: {"r":{},"f":[1,64,10,9209]}(TypeError: Cannot read property '0' of undefined)
G2_ERR: -1,JSON_PARSE_ERROR,Could not parse response: {"r":{},"f":[1,64,4,6370]}(TypeError: Cannot read property '0' of undefined)
And the move keypad doesn't work for me : reach a limit state and cannot resume it.
problem with the install-server script on the beaglebone
add the following for close any httpd server before launching nginx :
systemctl stop httpd
OpenSBP should be supported as a cutting file format. The implementation wouldn't be all that hard for basic files. support for MX,MY,MZ,M2,M3,CC and CG would put the system 80% of the way there.
No provision for user feedback/pause is currently supported. We want this feature eventually:
http://shopbotwiki.com/index.php?title=Getting_Operator_Inputs
These are the sorts of situations we're talking about.
Now that I can input a simple G2 command I can report that the circles still behave in the same manner as in the g-code and sbp code files ... that is, they seem jerky and segmented. I have mostly tested 1" and 2" circles. They also do not seem to respond to a ShopBot speed change, even when M2 moves do. Given this is happening with g-code that I believe worked previously ... seems G2 related?
Some users may want to talk directly to G2.
What about using websocket for this ?
If a g-code program (or direct string) is issued that does not cause the tool to change 'stat' - the tool never leaves the 'running' state, which causes it to misbehave (requiring a /quit from the API) to be back in a "good" state.
There is a fundamental difference between the way G-code and G2 take speed settings and the way ShopBot does. It may make more sense to add some additional commands to the VS command for the sake of G2 for setting the maximum settings. G2 uses maximum feed and rapids, then takes an F command to set the current speed. ShopBot jog speeds are used in the same way as the maximum rapid, but maximum feedrate is used differently. Anyone have any thoughts on this?
I just wait for the example app to refresh after a reboot ... that is, not long after the wifi is blinking, the app updates location and is ready to run ... compared to 3+ min to get in via PUTTY ...
Jobs need to be searchable, by title, description, filename, etc.
The openSBP parser doesn't like the '\r\n' that Windows put in its file instead of '\n'.
These fall into a few categories:
The configuration process should have some measure in place to make sure that G2 actually accepts all the configuration parameters as valid inputs. Currently the system runs "open loop" and can power up without changing the G2 default configuration, which is dangerous to the motion system, which may run much faster or farther than expected if configuration properties didn't take.
The application should have a (generous) watchdog, and fail on the absence of a response from G2 after a certain period of time. It would be extremely unusual not to hear from G2 within 10 seconds of sending the most recent message to it, and in that case, the application should quit, so that a daemon process that is running the application could detect the failure and restart it, making the app more robust to connection failures.
Manual mode tends to be slower, but needs to stop and start quicker than coordinated motion in a file. A separate jerk rate should be supplied for manual mode so as to allow for quick keyboard/joystick response as appropriate.
Still reporting idle when sbp file running ...
There is a problem when executing a POST request on the server, it respond "success", but nothing happen. (the tool is not moving and the state stay "idle").
When running files of any significant size, the g2 driver appears to stop feeding g-codes to g2, even when there are some left in the queue. The queue can be printed out on the changeover to idle state to confirm that the data is still there. This is maybe a mishandled status report? (or a misconfigured status verbosity)
Engine should support dual USB operation.
We need to split the http (json mostly) replies from the modules functions.
It'll be more modular, and more clear.
You need to stop the Engine and restart it to load the configuration.
This should be caused by sending the configuration commands before the G2 core reply that it's ready.
Perhaps make the first wait for a G2 response synchronous is a good idea.
The program should return an error message when you try to run a second instance of the server.
Currently the program say nothing, except it doesn't start the web server.
it shall be a problem when the program try to connect to the serial port. it seems to wait the other instance close the port before using it.
for example :
G1X1.0797Y2.4746F239.8
Y2.1699
X1.2990
will escape the 2 last lines.
It's due to the fact that lines that doesn't start by 'M', 'G' or 'N' are ignored.
Homing is a fundamental operation in G2, but without some massaging, out of the box, it doesn't work quite in the way that the FabMo API might wish to present it. Previously, our intent was to simply expose homing through the gcode/runstring function, but I think there's a significant advantages to having an API resource dedicated to homing.
The g-code command is G28.2 X0 (for homing the X axis for instance)
This seems simple enough, but the system draws homing speed from the JSON configuration, so to home at a different speed, the homing speed must be set with a JSON command first and then the G-Code command executed.
This isn't the worst thing in the world, because I think the expectation that the homing speed needs to change frequently is misguided. The homing speed is likely set once for a hardware configuration, and never touched again,.
In the case of the Z axis, however, there are typically two types of homing. There's homing to the prox switch, which is at the travel MAXIMUM, and then there's homing to the work surface, which is in the direction of the travel MINIMUM. In order to switch between these different homing tactics, you have to re-configure G2 with a small collection of JSON commands that set the homing direciton and limit switch configuration.
In addition, when homing is complete, in the case of using a z-zero plate, or something similar, it's comming to want to set the final location to something other than zero. This functionality could be wrapped up in the home command so that it appears to the caller to be a single operation, even though it's really a home, followed by a configuration of the final tool location.
The reconfiguration via JSON is something i think falls outside of what we want to live on the client side of the API. Better to have fabmo.js POST to /home and provide information such as axis, perhaps an optional speed, homing direction, backoff distance, and final position, offset from the location that triggered the limit during the home routine.
The engine should report the current system units to the client somehow.
add the file progress report, so we should be able to display a progress bar for the users.
The scaling of the accessory axes is strange. A move of G1 A1 moves the accessory axis to 288. This is most probably a G2 issue.
I suspect that they same is true for the CR command and any others that pass characters for things like the offset of the bit to get an inside or outside offset.
Even though a g-code exists for this (G28.3) I think an API function to set an axis location manually to a specific coordinate should exist, and convenience helper functions should exist for it in fabmo.js
The command position to used to keep track of the expected position of the tool based on the commands in a file or movement command. Some of these values need a good starting point before sending a command, such as, sending a CG command. Since the start position is an important calculation in the circle command.
To reproduce:
Upload a file with the same name twice.
Two entries are generated in the database, but only one file is used, the file is presumably overwritten.
It is worth discussing the intended behavior here. Options are:
The system never enters the "homing" state - even when homing, although one is defined.
The wifi module say it loads correctly :
info: Loading routes... [server]
info: wifi manager enable ! [wifi]
info: FabMo Engine listening at http://0.0.0.0:9876 [server]
info: Starting detection daemon... [server]
But when I try to access the routes throw my web browser It says that the routes don't exist :
http://192.168.10.1:9876/wifi_manager/profiles
{"code":"ResourceNotFound","message":"/wifi_manager/profiles"}
I tried to debug it and it sounds that the server loads the routes correctly :
{ domain: null,
_events: { uncaughtException: [Function] },
_maxListeners: 10,
before: [],
chain:
[ [Function: crossOrigin],
[Function: readBody],
[Function: parseBody] ],
log:
{ domain: null,
_events: {},
_maxListeners: 10,
_level: 20,
streams: [ [Object], [Object] ],
serializers:
{ err: [Function: err],
req: [Function: req],
res: [Function: res],
client_req: [Function: clientReq],
client_res: [Function: clientRes] },
src: false,
fields: { name: 'FabMo Engine', hostname: 'FabMo_dev_004', pid: 1124 } },
name: 'FabMo Engine',
router:
{ domain: null,
_events: { mount: [Function] },
_maxListeners: 10,
cache:
{ _max: 100,
_lengthCalculator: [Function: naiveLength],
_allowStale: false,
_maxAge: null,
_dispose: undefined,
_cache: {},
_lruList: {},
_mru: 0,
_lru: 0,
_length: 0,
_itemCount: 0 },
contentType: [],
log:
{ domain: null,
_events: {},
_maxListeners: 10,
_level: 20,
streams: [Object],
serializers: [Object],
src: false,
fields: [Object] },
mounts:
{ getstatus: [Object],
getconfig: [Object],
postconfig: [Object],
getinfo: [Object],
postdirectsbp: [Object],
postdirectgcode: [Object],
postdirectmove: [Object],
postdirectjog: [Object],
postdirectgoto: [Object],
getrunid: [Object],
getquit: [Object],
getpause: [Object],
getresume: [Object],
getfile: [Object],
postfile: [Object],
deletefileid: [Object],
getfileid: [Object],
getfileidview: [Object],
postjob: [Object],
getjobs: [Object],
getjobid: [Object],
getjobsqueue: [Object],
deletejobsqueue: [Object],
postjobsqueuerun: [Object],
getpassthrough: [Object],
get: [Object],
getwifi_managerdetection: [Object],
getwifi_managerprofiles: [Object],
postwifi_managerprofile: [Object],
deletewifi_managerprofilessid: [Object] },
name: 'RestifyRouter',
routes:
{ DELETE: [Object],
GET: [Object],
HEAD: [],
OPTIONS: [],
PATCH: [],
POST: [Object],
PUT: [] },
reverse:
{ '^\\/+status$': [Object],
'^\\/+config$': [Object],
'^\\/+info$': [Object],
'^\\/+direct\\/+sbp$': [Object],
'^\\/+direct\\/+gcode$': [Object],
'^\\/+direct\\/+move$': [Object],
'^\\/+direct\\/+jog$': [Object],
'^\\/+direct\\/+goto$': [Object],
'^\\/+run\\/+([^/]*)$': [Object],
'^\\/+quit$': [Object],
'^\\/+pause$': [Object],
'^\\/+resume$': [Object],
'^\\/+file$': [Object],
'^\\/+file\\/+([^/]*)$': [Object],
'^\\/+file\\/+([^/]*)\\/+view$': [Object],
'^\\/+job$': [Object],
'^\\/+jobs$': [Object],
'^\\/+job\\/+([^/]*)$': [Object],
'^\\/+jobs\\/+queue$': [Object],
'^\\/+jobs\\/+queue\\/+run$': [Object],
'^\\/+passthrough$': [Object],
'.*': [Object],
'^\\/+wifi_manager\\/+detection$': [Object],
'^\\/+wifi_manager\\/+profiles$': [Object],
'^\\/+wifi_manager\\/+profile$': [Object],
'^\\/+wifi_manager\\/+profile\\/+([^/]*)$': [Object] },
versions: [] },
routes:
{ getstatus:
[ [Function: crossOrigin],
[Function: readBody],
[Function: parseBody],
[Function] ],
getconfig:
[ [Function: crossOrigin],
[Function: readBody],
[Function: parseBody],
[Function] ],
postconfig:
[ [Function: crossOrigin],
[Function: readBody],
[Function: parseBody],
[Function] ],
getinfo:
[ [Function: crossOrigin],
[Function: readBody],
[Function: parseBody],
[Function] ],
postdirectsbp:
[ [Function: crossOrigin],
[Function: readBody],
[Function: parseBody],
[Function] ],
postdirectgcode:
[ [Function: crossOrigin],
[Function: readBody],
[Function: parseBody],
[Function] ],
postdirectmove:
[ [Function: crossOrigin],
[Function: readBody],
[Function: parseBody],
[Function] ],
postdirectjog:
[ [Function: crossOrigin],
[Function: readBody],
[Function: parseBody],
[Function] ],
postdirectgoto:
[ [Function: crossOrigin],
[Function: readBody],
[Function: parseBody],
[Function] ],
getrunid:
[ [Function: crossOrigin],
[Function: readBody],
[Function: parseBody],
[Function] ],
getquit:
[ [Function: crossOrigin],
[Function: readBody],
[Function: parseBody],
[Function] ],
getpause:
[ [Function: crossOrigin],
[Function: readBody],
[Function: parseBody],
[Function] ],
getresume:
[ [Function: crossOrigin],
[Function: readBody],
[Function: parseBody],
[Function] ],
getfile:
[ [Function: crossOrigin],
[Function: readBody],
[Function: parseBody],
[Function] ],
postfile:
[ [Function: crossOrigin],
[Function: readBody],
[Function: parseBody],
[Function] ],
deletefileid:
[ [Function: crossOrigin],
[Function: readBody],
[Function: parseBody],
[Function] ],
getfileid:
[ [Function: crossOrigin],
[Function: readBody],
[Function: parseBody],
[Function] ],
getfileidview:
[ [Function: crossOrigin],
[Function: readBody],
[Function: parseBody],
[Function] ],
postjob:
[ [Function: crossOrigin],
[Function: readBody],
[Function: parseBody],
[Function] ],
getjobs:
[ [Function: crossOrigin],
[Function: readBody],
[Function: parseBody],
[Function] ],
getjobid:
[ [Function: crossOrigin],
[Function: readBody],
[Function: parseBody],
[Function] ],
getjobsqueue:
[ [Function: crossOrigin],
[Function: readBody],
[Function: parseBody],
[Function] ],
deletejobsqueue:
[ [Function: crossOrigin],
[Function: readBody],
[Function: parseBody],
[Function] ],
postjobsqueuerun:
[ [Function: crossOrigin],
[Function: readBody],
[Function: parseBody],
[Function] ],
getpassthrough:
[ [Function: crossOrigin],
[Function: readBody],
[Function: parseBody],
[Function] ],
get:
[ [Function: crossOrigin],
[Function: readBody],
[Function: parseBody],
[Function: serve] ],
getwifi_managerdetection:
[ [Function: crossOrigin],
[Function: readBody],
[Function: parseBody],
[Function] ],
getwifi_managerprofiles:
[ [Function: crossOrigin],
[Function: readBody],
[Function: parseBody],
[Function] ],
postwifi_managerprofile:
[ [Function: crossOrigin],
[Function: readBody],
[Function: parseBody],
[Function] ],
deletewifi_managerprofilessid:
[ [Function: crossOrigin],
[Function: readBody],
[Function: parseBody],
[Function] ] },
secure: false,
versions: [],
acceptable:
[ 'application/json',
'text/plain',
'application/octet-stream',
'application/javascript' ],
formatters:
{ 'application/javascript': [Function: formatJSONP],
'application/json': [Function: formatJSON],
'text/plain': [Function: formatText],
'application/octet-stream': [Function: formatBinary] },
server:
{ domain: null,
_events:
{ connection: [Object],
clientError: [Object],
close: [Object],
error: [Function],
listening: [Function],
secureConnection: [Function],
upgrade: [Object],
checkContinue: [Function: onCheckContinue],
request: [Function] },
_maxListeners: 10,
_connections: 0,
connections: [Getter/Setter],
_handle: null,
_usingSlaves: false,
_slaves: [],
allowHalfOpen: true,
httpAllowHalfOpen: false,
timeout: 120000 },
maxHeadersCount: [Getter/Setter],
url: [Getter] }
@ryansturmer can you look at it ? can it be a problem with restify ? or my routes are not correct ?
G2 appears not to respond to immediate feedhold, ie feedhold issued during rampup. As a result, a simple "tap" on the manual mode (a jog followed by an immediate stopJog) does not result in the expected traversal. Instead, the 500ms planned motion for a jog must time-out (instead of the immediate stop) which results in a much larger move than anticipated for a nudge.
If I send an MS command from the example app command line, e.g. "ms, 3" the communication with G2 locks up and "run" is displayed (with the stop button having no effect). Yet on reboot (full system), the new speed seems to have been accepted and is in effect ...
So I think we've settled on the idea of an "app" being a packaged bundle which gets hosted (either on the tool or on the client machine) Here is a short list of properties of an "app"
So, questions are:
Just kicking off the discussion here.
When calling the gcode function of the driver from the direct commands in the API, the tool execute the gcode once, and then enter in hold mode. work if I use runString instead...
We start thinking about the structure of the information file that is on the SBC (for giving information about the tool to the web developers).This configuration file will be accessible from the API.
Here is the first idea of the structure :
{
"serial_number":"0123456789",
"vendor":"ShopBotTools",
"product":"handibot",
"surname":"my handibot",
"type":
[
"router",
"3dprinter",
"lasercutter"
],
"hw_version" : "1.1",
"core_version":"1.2",
"sw_version":"0.1.1",
"conf_unit":"in",
"nb_axis" :
[
3,
4,
5
],
"x_size":"6",
"y_size":"8",
"z_size":"3.25"
}
So I've got three main question about this :
With the move to the job system, part filenames should have the following attributes:
The G2 driver code should be separated from the machine model. The model of the machine includes higher level methods and data that are outside of the scope of the G2 driver (for example, management of files and progress)
In the process of testing the VU command changes and I can't get the engine to start without hanging. It gets to the info: Connecting to G2... most of the time and made it to info: Configuring G2 once. An error message is returned if I wait long enough:
events.js:72
throw er; // Unhandled 'error' event
But, I can't locate the events.js to try and fix the issue.
The engine should either be able to reconnect to a bombed serial port, or it should exit entirely, allowing the service manager to mark it as failed and re-start it.
The state should pass from running to idle, but it doesn't.
This is due to this line.
It return just a "undefined" in the log. I suppose that the job attribute just doesn't exist ...
Jobs are currently "all or nothing" - some pagination scheme is needed for requests so a job manager can deal appropriately with a database that is full of jobs.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.