Freewrite SDK
As of May 2017 and firmware version v1.4
, the Freewrite SDK is in limited beta. Developers seeking an invitation to use the SDK should email mailto:developer@astrohaus.com.
The full SDK will be launched publicly with firmware version v1.5
.
Features expected to land in the next firmware version v1.5
:
- Click-to-install, online Extensions portal for end-users
- Custom user data captured during click-to-install for use in extensions
- Better
preflight
support
Getting Started
Adding a Freewrite USB ethernet device on Linux:
# /etc/network/interfaces
allow-hotplug usb0
auto usb0
iface usb0 inet static
address 192.168.99.44
netmask 255.255.255.0
To develop on your Freewrite, you will need:
- A registered Developer account on Postbox
- A USB-C cable and your Freewrite device
- A static USB Ethernet connection to your Freewrite
- An example is provided at right for Linux developers.
- An sFTP client like WinSCP or Cyberduck
The following credentials allow sFTP access to the extensions/
folder on your Freewrite for local extension development:
sdk:sdk@192.168.99.190
Creating an Extension package
Here is an example
package.json
:
{
"name": "offline-folder",
"description": "This extension disables uploading documents for a single folder."
"sdk_versions": [
"1.0"
]
}
To register a file path as an extension:
// index.js
function(sdk_version, register){
register('preflights/openpgp.js', {
type: 'preflight',
folder: null, // optional
});
register('components/hint-modal.js', {
type: 'component',
triggers: {
hotkey: 'z'
},
assets: [
'stylesheets/style.css' // not implemented yet. use fetch API.
]
});
}
An extension comprises at least two files:
- A
package.json
which describes the extension package - An
index.js
which programatically registers the extension(s)
package.json
To create a package.json
, install npm package manager and perform this command in your terminal: npm init
The following fields are mandatory:
name
: A globally unique name for your packagesdk_versions: ["1.0"]
index.js
The index.js
file is used to register files as extensions. You can register more than one extension in a package (e.g. both a Preflight extension and a Console extension).
The index.js
file is invoked as an anonymous function and receives two arguments:
Parameter | Type | Description |
---|---|---|
sdk_version | String | '1.0' |
register | Function | This is a callable function used to register file paths as extensions. |
Developers can register files within their package as extensions by invoking the register()
callable function in their index.js
.
The first argument is a relative path to the extension’s source code file. Different extension types require different values included in the second options
argument.
Option | Required | Description |
---|---|---|
type | Yes | String. Must be one of preflight , component , console , or canvas |
folder | No | String. Limits the extension to being active in the given folder a , b , or c . |
triggers | No | Dictionary. Register hotkeys for applicable extensions using a dictionary-like data structure. |
There are four extension types available:
- Preflight
- Console
- Canvas
- Component
A Preflight extension modifies the document before uploading it to Postbox. This can be useful for operations like encryption.
A Console extension adds a new status bar component to the console at the bottom of your Freewrite display.
A Canvas extension replaces the editor at the top of your Freewrite display.
Finally, a Component extension is a custom UI element that is displayed when triggered by the user. Triggers often include custom hotkeys like NEW
+ [Hotkey]
.
Preflight Extensions
Register a Preflight in
index.js
:
register('preflights/openpgp.js', {
type: 'preflight',
folder: 'a' // only use this Preflight for folder A
});
Return the new Document object, a Promise, or null:
function(document, user, modules){
let openpgp = modules.opengpg;
openpgp.initWorker();
return
fetch('extension://my-extension-name/public_key.txt')
.then((response) => {
return response.text();
})
.then((public_key) => {
let options = {
data: document.body,
publicKeys: openpgp.key.readArmored(pubkey).keys
}
return openpgp.encrypt(options)
})
.then(function(ciphertext) {
return ciphertext.data;
});
}
A Preflight extension performs an operation on a document just before uploading the document to Postbox.
The Preflight extension receives these arguments:
Position | Argument | Type | Description |
---|---|---|---|
1 | document | object | A document object |
2 | user | object | A user object |
3 | modules | object | An object exporting several third-party libraries for use in extensions |
A Preflight extension must return one of the following values:
Type | Example | Description |
---|---|---|
String | ‘new text’ | A complete text buffer of the document body |
Object | {body: 'new text', data: {}} |
A Document object |
Promise | fetch().then((obj) => {return obj}) |
A Promise object |
Null | null |
A null return value will be ignored; the document will not be uploaded. |
Components
Register a component in
index.js
:
register('components/character-generator.js', {
type: 'console'
});
register('components/canvas.js', {
type: 'canvas',
folder: 'a'
});
register('components/file-manager.js', {
type: 'component'
});
Example:
function(modules) {
return class MyComponent extends modules.React.Component {
constructor() {
super()
this.state = {
visible: false
}
this.onHotkey_bound = this.onHotkey.bind(this);
}
onHotkey(event){
if(event.key === 'm'){
this.setState({
visible: !this.state.visible
});
}
}
componentWillUnmount() {
this.props.emitter.off('hotkey', this.onHotkey_bound);
}
componentDidMount() {
this.props.emitter.on('hotkey', this.onHotkey_bound);
}
render() {
if(!this.state.visible){
return modules.React.createElement('span');
}
return modules.React.createElement('div', {
style: {
position: 'fixed',
width: '25%',
right: '0px',
top: '0px',
height: '100%',
border: '1px solid black',
backgroundColor: 'white'
}
}, 'My Text');
}
};
}
The Console, Canvas, and Component extensions are user interface elements that are added to the Freewrite application.
A Console extension adds a new status bar to the lower console on a Freewrite.
A Canvas extension replaces the text editor for a provided folder.
A Component extension is injected to application and can respond to certain events to make itself visible or hidden.
Properties
Components are written using React. (JSX support may be available in the future.)
Certain properties are passed to the component and can be accessed through this.props
:
Property | Description |
---|---|
emitter |
An event emitter used to bind to Freewrite events like hotkey |
data |
An object containing extension data |
Actions
Actions are also passed to components and can be accessed through this.props.actions
:
Action | Description |
---|---|
patchExtensionData({...}) |
Updates this.props.data available to all extension components |
putExtensionData({...}) |
Overwrites this.props.data available to all extension components |
These two patch/put
actions can update data across multiple components registered in the same extension.
Event Emitter
An event emitter is provided for listening to and emitting events.
Subscriber Events | Arguments | Description |
---|---|---|
hotkey |
event |
Emits an event object containing event.key which represents a NEW+{key} hotkey press. |
document.load |
document |
Publisher Events | Arguments | Description |
---|---|---|
document.select |
||
document.new |
||
document.prev |
||
document.next |
||
block.create |
||
block.update |
||
block.delete |
||
language.cycle |
||
toggle-frontlight |
Upcoming Events (not implemented yet)
- Add
emitter.promise(...)
convenience method for emitting event with promised callback document.list
Assets & Data
Register a JSON data fixture in
index.js
:
register('component.json', {
type: 'component'
});
register('names.json', {
type: 'json'
});
The data fixture
names.json
:
{
"names": [
"chrono",
"lucca",
"marle",
"frog",
"magus",
"ayla",
"robo"
]
}
Accessing the data in a component:
function(modules) {
class GeneratorComponent extends modules.React.Component {
getRandomName(){
// The data fixture is keyed by its filename
let names = this.props.data['names.json']['names'];
let index = Math.floor(Math.random() * names.length);
return names[index];
}
}
}
Accessing any Extension asset using
fetch
:
fetch_secret = function(){
fetch('extension://secretSauce/secret.txt')
.then((response) => {
console.log(response.text());
});
}
Data fixtures can be loaded by registering a file path as a json
extension type. JSON data loaded this way will be available to all components included in the extension and accessible through this.props.data
.
Alternatively, any asset can be loaded asynchronously by using the fetch API (example at right).