Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,48 @@ It provides several grunt task:
}
```

- `sectv-deploy`: Deploys packaged (`.zip` or `.wgt`) Cordova apps to supported smart tv platforms through their relative deploy methods.

- Options for the task:

```js
'sectv-deploy': { // task
'sectv-orsay': { // target
dest: 'platforms/sectv-orsay/build/', // Path to use for host server root
id: 'app', // Widget Identifier
port: '80', // Port of the host server (default 80)
zipName: 'app.zip' // Name of the packaged app archive
/*
After the host server has started, boot up your TV in developer mode, point it at the correct IP address, and sync your apps.
*/
},
'sectv-tizen': {
wgtPath: 'app.wgt', // Path of the packaged widget
/*
Deployment is performed via: 'sdb install <buildfolder>/<wgtName>'
*/
},
'tv-webos': {
ipkPath: 'platform/webos/build/app.ipk', // Path of the packaged app archive
device: 'emulator', // Target device name
/*
Deployment is performed via: ares-install --device <device> <buildfolder>/<ipkName>
*/
}
}
```

When deploying to Samsung Orsay TVs via HTTP, the device expects a specific folder layout and manifest file:
<server-root>/
├── widgetlist.xml
└── Widget/
└── app.zip

- widgetlist.xml must reside in the server root and describe the available widget(s).
- The actual app package (app.zip) must be placed inside the Widget/ subdirectory.
- The <download> field in widgetlist.xml must point to http://<host-ip>/Widget/app.zip.
This structure allows Orsay TVs in developer mode to retrieve and install the app over the network without proprietary tools.

# Associated Projects

- [cordova-sectv-orsay](http://github.com/Samsung/cordova-sectv-orsay) is an application library that allows for [Cordova](http://cordova.apache.org)-based projects to be built for the Legacy Samsung Smart TV (A.K.A Orsay) Platform.
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "grunt-cordova-sectv",
"description": "Grunt task for build and package the cordova project with cordova-sectv-*** platforms",
"description": "Grunt task for build, package and deploy the cordova project with cordova-sectv-*** platforms",
"version": "1.4.1",
"author": {
"name": "Samsung Electronics",
Expand All @@ -18,11 +18,13 @@
"grunt-contrib-jshint": "^0.11.2",
"inquirer": "^5.2.0",
"js2xmlparser": "^1.0.0",
"xmlbuilder2": "^3.0.2",
"mkdirp": "^1.0.3",
"mustache": "^2.3.2",
"shelljs": "^0.5.3",
"xml2js": "^0.4.23",
"zip-dir": "^1.0.2"
"zip-dir": "^1.0.2",
"express": "^4.18.2"
},
"peerDependencies": {
"grunt": ">=0.4.0"
Expand Down
14 changes: 13 additions & 1 deletion sample/Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,17 @@ module.exports = function(grunt) {
www: 'platforms/tv-webos/www',
dest: 'platforms/tv-webos/build'
}
},
'sectv-deploy': {
'sectv-orsay': {
dest: 'platforms/sectv-orsay/build'
},
'sectv-tizen': {
dest: 'platforms/sectv-tizen/build'
},
'tv-webos': {
dest: 'platforms/tv-webos/build'
}
}
});

Expand All @@ -76,6 +87,7 @@ module.exports = function(grunt) {
'jshint',
'clean',
'sectv-prepare',
'sectv-build'
'sectv-build',
'sectv-deploy'
]);
};
178 changes: 171 additions & 7 deletions tasks/packager/sectv-orsay.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ var shelljs = require('shelljs');
var mustache = require('mustache');
var grunt = require('grunt');
var zipdir = require('zip-dir');
var js2xmlparser = require('js2xmlparser');
var xml2js = require('xml2js');
var os = require('os');
var express = require('express');

var revLen = 3;

Expand Down Expand Up @@ -316,14 +318,19 @@ function prepareDir(dir) {
mkdirp.sync(dir);
}

function getManualOrsayConfData(platformsData){
var i,
manualOrsayConfData = null;
function getManualOrsayConfData(platformsData) {
var manualOrsayConfData = null;
var builder = new xml2js.Builder({
headless: true,
renderOpts: { pretty: false }
});

if (platformsData) {
for (i=0; i < platformsData.length; i++) {
for (var i = 0; i < platformsData.length; i++) {
if (platformsData[i].$.name === 'sectv-orsay') {
delete platformsData[i].$;
manualOrsayConfData = utils.trim(js2xmlparser('platform',platformsData[i],{declaration : {include : false},attributeString : '$'}).replace(/<(\/?platform)>/igm,''));
var xml = builder.buildObject(platformsData[i]);
manualOrsayConfData = utils.trim(xml);
}
}
}
Expand Down Expand Up @@ -493,5 +500,162 @@ module.exports = {
successCallback && successCallback();
});
});
},
deploy: function (successCallback, errorCallback, data) { // HTTP host server + widgetlist.xml
console.log('\nStart deploying Legacy Samsung Smart TV Platform apps......');
var dest = data.dest || path.join('platforms', 'sectv-orsay', 'build');
dest = path.resolve(dest);
var serverRoot = data.serverRoot || '../serverRoot';
serverRoot = path.resolve(serverRoot)
var port = data.port || 80;

var userConfPath = path.join('platforms', 'userconf.json');

var projectName = 'package';

if(fs.existsSync(userConfPath)){
var userData = JSON.parse(fs.readFileSync(userConfPath));

if(userData.hasOwnProperty('orsay')){
projectName = userData.orsay.name;
}
}
else {
grunt.log.error('Prepare and Build the project first.');
}

var appId = data.id || projectName;
var zipName = data.zipName || projectName + '.zip';

arrangeFolders(serverRoot, function (err) {
if (err) {
grunt.log.error(err);
errorCallback && errorCallback(err);
return;
}

updateWidgetList(serverRoot, function (err) {
if (err) {
grunt.log.error(err);
errorCallback && errorCallback(err);
return;
}

startServer(serverRoot, port);
grunt.log.ok(`Host Server started at ${serverRoot}`);
successCallback && successCallback();
});
});

function getNetworkIp() {
var interfaces = os.networkInterfaces();
for (var name of Object.keys(interfaces)) {
for (var intf of interfaces[name]) {
if (intf.family === 'IPv4' && !intf.internal) {
return intf.address;
}
}
}
return '127.0.0.1';
}

function arrangeFolders(serverRoot, cb) {
fs.mkdir(path.join(serverRoot, 'Widget'), { recursive: true }, function (err) {
if (err) return cb(err);

var srcZip = path.join(dest, zipName);
var destZip = path.join(serverRoot, 'Widget', zipName);

fs.copyFile(srcZip, destZip, function (err) {
if (err) return cb(err);
grunt.log.ok(`Copied ${zipName} to ${destZip}`);
cb(null);
});
});
}

function updateWidgetList(serverRoot, cb) {
var xmlPath = path.resolve(path.join(serverRoot, 'widgetlist.xml'));
var builder = new xml2js.Builder({ headless: false, renderOpts: { pretty: true } });

if (fs.existsSync(xmlPath)) {
var existing = fs.readFileSync(xmlPath, 'utf8');
var parser = new xml2js.Parser({ explicitArray: false });

parser.parseString(existing, (err, obj) => {
if (err) return cb(err);

if (!obj.rsp) obj.rsp = { $: { stat: 'ok' }, list: { widget: [] } };
if (!obj.rsp.list) obj.rsp.list = { widget: [] };
if (!Array.isArray(obj.rsp.list.widget)) {
obj.rsp.list.widget = obj.rsp.list.widget ? [obj.rsp.list.widget] : [];
}

// Search if there is already a widget with the same id
var existingIndex = obj.rsp.list.widget.findIndex(w => w.$.id === appId);

var newWidget = {
$: { id: appId },
title: projectName,
compression: { $: { size: '12345', type: 'zip' } },
description: 'My TOAST App -- Description',
download: `http://${getNetworkIp()}/Widget/${zipName}`
};

if (existingIndex >= 0) {
// Update existing
obj.rsp.list.widget[existingIndex] = newWidget;
grunt.log.ok(`Updated existing widget ${appId}`);
} else {
// Add new
obj.rsp.list.widget.push(newWidget);
grunt.log.ok(`Added new widget ${appId}`);
}

var xml = builder.buildObject(obj);
fs.writeFile(xmlPath, xml, err => {
if (err) return cb(err);
grunt.log.ok(`Manifest written at ${xmlPath}`);
cb(null);
});
});
} else {
var obj = {
rsp: {
$: { stat: 'ok' },
list: {
widget: [{
$: { id: appId },
title: projectName,
compression: { $: { size: '12345', type: 'zip' } },
description: 'My Samsung Application -- Description',
download: `http://${getNetworkIp()}/Widget/${zipName}`
}]
}
}
};

var xml = builder.buildObject(obj);
fs.writeFile(xmlPath, xml, err => {
if (err) return cb(err);
grunt.log.ok(`Created new manifest at ${xmlPath}`);
cb(null);
});
}
}

function startServer(serverRoot, port) {
var app = express();
app.all('*', (req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'X-Requested-With');
next();
});
app.use(express.static(serverRoot));
app.listen(port, () => {
grunt.log.ok(`Server root at ${serverRoot}/`);
grunt.log.ok(`Server running at http://${getNetworkIp()}:${port}/`);
});
}
}
};
};
35 changes: 35 additions & 0 deletions tasks/packager/sectv-tizen.js
Original file line number Diff line number Diff line change
Expand Up @@ -368,5 +368,40 @@ module.exports = {
}
}
});
},
deploy: function (successCallback, errorCallback, data) { // Smart Developement Bridge
console.log('\nStart deploying Tizen Samsung Smart TV Platform apps......');

var { spawn } = require('child_process');
var wgtPath = data.wgtPath || './platform/tizen/build/app.wgt';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

the default dest path should be ./platform/sectv-tizen/build/app.wgt

It's sorry for saying that,
There are some parts of this PR that are questionable as to whether it works properly, so It's hard to be accepted for now.
Please test all platforms and make sure that they all work well.
Please consider to attach the working well screenshots for each platforms,
Then I will accept this PR.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

You're right.
As reported, the only tested platform that works so far is Orsay.
I'd say to wait for someone to test the other platforms first.
Thanks for your attention.


var userConfPath = path.join('platforms', 'userconf.json');

var projectName = 'package';

if(fs.existsSync(userConfPath)){
var userData = JSON.parse(fs.readFileSync(userConfPath));

if(userData.hasOwnProperty('tizen')){
projectName = userData.tizen.name;
}
}
else {
grunt.log.error('Prepare and Build the project first.');
}

var proc = spawn('sdb', ['install', wgtPath]);

proc.stdout.on('data', d => grunt.log.write(d.toString()));
proc.stderr.on('data', d => grunt.log.error(d.toString()));
proc.on('close', code => {
if (code === 0) {
grunt.log.ok('Tizen app deployed successfully.');
successCallback && successCallback();
} else {
grunt.log.error('Tizen app deployment failed.');
errorCallback && errorCallback();
}
});
}
};
36 changes: 36 additions & 0 deletions tasks/packager/tv-webos.js
Original file line number Diff line number Diff line change
Expand Up @@ -421,5 +421,41 @@ module.exports = {
}
}
});
},
deploy: function (successCallback, errorCallback, data) { // WebOS CLI
console.log('\nStart deploying WebOS Smart TV Platform apps......');

var { spawn } = require('child_process');
var ipkPath = data.ipkPath || './platform/webos/build/app.ipk';
var device = data.device || 'emulator';

var userConfPath = path.join('platforms', 'userconf.json');

var projectName = 'package';

if(fs.existsSync(userConfPath)){
var userData = JSON.parse(fs.readFileSync(userConfPath));

if(userData.hasOwnProperty('tizen')){
projectName = userData.tizen.name;
}
}
else {
grunt.log.error('Prepare and Build the project first.');
}

var proc = spawn('ares-install', ['--device', device, ipkPath]);

proc.stdout.on('data', d => grunt.log.write(d.toString()));
proc.stderr.on('data', d => grunt.log.error(d.toString()));
proc.on('close', code => {
if (code === 0) {
grunt.log.ok('WebOS app deployed successfully.');
successCallback && successCallback();
} else {
grunt.log.error('WebOS app deployment failed.');
errorCallback && errorCallback();
}
});
}
};
Loading