Is it me, or is learning to develop a LuCI module strangely difficult?

I've got an idea for a my NanoPi R1 – One of the online communities I'm involved in has a need for a networking appliance that members can use for helping to find other members for online multiplayer games. I'd like to make a firmware for the NanoPi so that members can get one and then download my firmware image. Since most members won't want to deal with the intricacies of setting it up, I want to make a "setup wizard" that will show when the web gui is first accessed – all the commonly needed functionality to get the device set up and running on the network. I've got my build environment working pretty well now (on Mac OS no less!), so the next step is developing my "Setup Wizard" for LuCI.

I certainly don't want to be walked through how to do all of this, but figured I'd describe what I'm doing on the off chance anyone can provide some resources or point me in the right direction for developing such functionality, using the most accepted practices.

I'd like to make this as a module that has "pages", like you'd imagine a setup wizard would use. Only one main menu item in the LuCI menu that links to the "master" or "template" wizard page with next and back buttons that will load in the "child" pages, possibly as an array of includes? Can't find any good examples of how to do this though.

I would like to have the child pages include:

A welcome page that describes what will need to be done to get the device set up.
I can't find any good examples of how to just format simple text on a page. I know it sounds silly, but ever since this change from Lua to JS, just putting a string of text as a page title seems to involve like 40 lines of javascript and probably 500,000 clock cycles on the client CPU... all to offload 100 clock cycles of Lua on the router?

A "Connect to your network" page (device will operate as Wifi client or Ethernet). I've been able to "include" the wireless.js module and have it work just fine, but that's with no changes at all and I fully realize that as soon as I change something I'm going to break it because these modules seem woefully complex. For example, I'd like to have a radio button that selects between using ethernet or wifi, and then conditionally load either Wifi setup or Ethernet setup page in the wizard as the next step. I'm not sure how to link to/load/append a page without it showing in the menu (since the only way to have a "path" for a page that I know of is to have it in the menu json, which adds it to the menu). I found a few places where "show different things based on a drop-down" functionality is implemented in the OpenWrt web gui so will be studying that, but still feel like I'll struggle getting this into a "wizard" interface given how complex the javascript seems to be in LuCI.

The reason I ask here, is because I'm having difficulty finding good tutorials and learning material for LuCI. For example, even the official developer guide shows writing a module in Lua, which as I understand, is old and deprecated and we're supposed to do everything with billions of lines of javascript now right? :crazy_face:
Can I still write it in Lua? Should I? Why does so much of the documentation seem unaware of such a drastic fundamental change to how LuCI modules are written?

Thanks!

from someone who also found themselves in your position ( non-offical take )

  • imho... for someone with general coding experience ( not fluent in lua or js ) it is challenging to say the least if you have a background in js ( less so lua these days ) then the level of difficulty comes down accordingly

  • luci is a constantly moving target which makes using source samples ( of which there are quite a few ... clone the github luci repo and take a look through ) as a guide the best 'learning' method for developers and new-developers alike if you struggle too much with this... it's likely an indicator that your fundamental coding skills are perhaps not up to the task and would need improvement.

  • starting on a router... rather than worrying about the package/git structure later makes getting started much much easier...

so fundamentally I think it comes down a few questions;

  • do you have lua / js experience?
  • have you cloned the luci git repo or selected a package to use for reference?

if you keep an eye on luci@github you'll gain much of what you need to know there...

  • lua is becoming deprecated so coding keeping most of the stuff in JS is advisable if you wish to share/have your code supported for a longer duration
  • the current tcpdump PR should give you an interesting window into this process and ways to implement wizard like behavior ( general luci no password is set functionality provides mechanisms to detect and redirect for a setup process )
2 Likes

Thank you so much for your insight. I do have a modest enterprise background in C#, Java and JS. Unfortunately, "enterprise" just means a "git-r-done" attitude with not much emphasis on best practices or even embracing emerging development paradigms, so there's that... I readily admit that my js skills are not nearly up to open-source-developer level - certainly not for something like LuCI JS which seems to be quite "convoluted" to my admittedly small brain... although my Lua skills are completely nonexistent, so I should probably be happy to see LuCI moving away from Lua and towards JS lol.

What I have now is a build environment with a cloned repo of 19.07.6, on a Mac OS build environment (kind of a hurdle in itself lol). I've got my own "feed" with a LuCI module (I duplicated one of the built-in modules, I think it was status) added to the menuconfig and have it built into the image. I've configured and built what I think is a good image to start with, and the NanoPi is running it.
For developing my module, I have been connecting to the NanoPi over SFTP and editing the js files so I can see changes and debug quickly without having to re-build the image. I've definitely been looking over many of the built-in LuCI modules and apps, seeing how they work, seeing what UI elements and rpc functions they implement and trying to build an uderstanding of how I should approach my module. I just wish I could find a developer guide/tutorial/wiki that went over the basics, but for JS not Lua.

As an example, the status module has the following in index.js:

return view.extend({
	load: function() {
		return L.resolveDefault(fs.list('/www' + L.resource('view/status/include')), []).then(function(entries) {
			return Promise.all(entries.filter(function(e) {
				return (e.type == 'file' && e.name.match(/\.js$/));
			}).map(function(e) {
				return 'view.status.include.' + e.name.replace(/\.js$/, '');
			}).sort().map(function(n) {
				return L.require(n);
			}));
		});
	},

	render: function(includes) {
		var rv = E([]), containers = [];

		for (var i = 0; i < includes.length; i++) {
			var title = null;

			if (includes[i].title != null)
				title = includes[i].title;
			else
				title = String(includes[i]).replace(/^\[ViewStatusInclude\d+_(.+)Class\]$/,
					function(m, n) { return n.replace(/(^|_)(.)/g,
						function(m, s, c) { return (s ? ' ' : '') + c.toUpperCase() })
					});

			var container = E('div');

			rv.appendChild(E('div', { 'class': 'cbi-section', 'style': 'display:none' }, [
				title != '' ? E('h3', title) : '',
				container
			]));

			containers.push(container);
		}

		return startPolling(includes, containers).then(function() {
			return rv;
		});
	},

	handleSaveApply: null,
	handleSave: null,
	handleReset: null
});

Nothing about this is easy to follow as far as what's writing to the page. I get that load: is finding the files in the includes folder, and render: is iterating through the includes passed to it and appending a div with each include... but how do the includes get from load to render? How do they get out of load? Like I said, I don't claim to be any kind of expert but there appears to be some kind of API or unspoken class interface that's being followed but I can't seem to find documentation on how it works. Sure, I can just copy and paste already working stuff but I'd really like to learn how I'm supposed to be doing things from some documentation instead of crashing through it lol.

Either way though, I'm sure I'll eventually figure it out. Just be nice to find some up-to-date documentation to peruse.

1 Like

Development should always be done against the current master (development) branch, even if you don't expect to merge your changes. the openwrt-19.07 branch is almost two years old by now and a lot has happened in luci development since then, by the time your prototype is ready, the 19.07.x releases are dusted and forgotten.

1 Like

Thanks for the insight. I was under the impression that 19.07.6 was the latest stable release? If I remember correctly, when I initially started on the master branch, the patches/support for the NanoPi R1 were not included.

alright then... seems you are around the right place technically where you should be...

I suck at JS... but surprisingly I find the example you posted fairly clear / readable... ( good idea to post and discuss the sample )

you make a very good observation though... re: polling/acls/etc and knowing where something may be reliant on non-obvious code backend requirements... ( fgrep and opkg/PKGNAME.list saved my day here )

again... the source is your best guide... if something is not clear ... either;

  • break it down into smaller pieces ( you should be making use of the browser debugger )...
  • find another example to dissect ( fgrep is your friend )

I think breaking stuff down into elements is what you need at this stage... trying to develop an 'app' that makes use of file-calls/acls, setup-redirect logic, js-output constructions... is way to large a bite to swallow...

create a separate page that does each task... you'll then be tooled to proceed...

1 Like

Sure.
But it is not for development...

All new stuff goes to master.

1 Like

Thanks for your insights.

I also agree about approaching things in small bites, and that's why I'm wanting to make my setup wizard pages as separate js files.

Right now I'd like to make two pages - a welcome page and a network select page. I'm looking at the "Status" module to see how they've implemented includes and what's got me slightly confused is what's calling the included file's render function.

The "load" function:

load: function() {
		
		return L.resolveDefault(fs.list('/www' + L.resource('view/wizard/pages')), []).then(function(entries) {
			return Promise.all(entries.filter(function(e) {
				return (e.type == 'file' && e.name.match(/\.js$/));
			}).map(function(e) {
				pages.push(e);
				return 'view.wizard.pages.' + e.name.replace(/\.js$/, '');
			}).sort().map(function(n) {
				return L.require(n);
			}));
		});
		
	},

...where L.require(n) seems to cause the items passed into it to be rendered even if return rv; near the end of render: is omitted:

render: function(includes) {
		var rv = E([]), containers = [];

		for (var i = 0; i < includes.length; i++) {
			var title = null;

			if (includes[i].title != null)
				title = includes[i].title;
			else
				title = String(includes[i]).replace(/^\[ViewWizardInclude\d+_(.+)Class\]$/,
					function(m, n) { return n.replace(/(^|_)(.)/g,
						function(m, s, c) { return (s ? ' ' : '') + c.toUpperCase() })
					});

			var container = E('div');

			rv.appendChild(E('div', { 'class': 'cbi-section', 'style': 'display:none' }, [
				title != '' ? E('h3', title) : '',
				container
			]));

			containers.push(container);
		}

		return startPolling(includes, containers).then(function() {
			return rv;
		});
	},

So what's doing the rendering? Why return the view in render:? Is there a better way for me to look for my "pages" without immediately rendering them, but rather store them in an array of paths (or even an array of objects) that I can call render (or some other function name if render is the issue here) when I want to move to the next page etc?

1 Like

I don't really plan to keep a release schedule with this module that corresponds to OpenWrt. It's a simple project for some friends to use, and once I get a setup wizard module written I'd want to distribute this with the a stable release of OpenWrt... my understanding is if I wanted to do that (send out a firmware based on a stable release) on master, I'd have to wait for the next stable release or merge my project into 19.07.6 anyway right?

Also, just noticed your avatar... do you use Mac OS as a build environment for OpenWrt?

No.
Why? Due to the old symbol used in Mac keyboards?

The ancient symbol has nothing to do with Macs.

2 Likes

I see. I certainly was aware that the symbol has other meanings, and I see that in the context of your avatar it has nothing to do with Macs, but I'm sure you can imagine that on a development forum it's use as the Command modifier key on Mac is arguably the most relevant initial thought. Even Wikipedia implies that it only gained "international recognition" after it's use on the Mac.

I'm only trying to find other people to commiserate with. :joy:

Sure, but I happen to be from Finland (mentioned in that Wiki quote above), and that symbol directly relates to my name :wink:

2 Likes