Inky Dash is an interface for Inky pHAT, an e-paper display for the Raspberry Pi, that can be accessed from a browser via a local web server. I wanted a fast and easy way to upload images to the e-paper display that met the following goals:

Screenshot of the inky dash display settings page
Inky Dash interface

Tech Stack

It may have been more suitable to have created Inky Dash using a Python web framework such as Flask but I took this project as an opportunity to broaden my experience with the Express framework and the React/Redux libraries.

Development

The project is structured as a monorepo with the server and UI code bundled together. By treating the application as a whole it meant fewer steps were needed to deploy and run it.

Validation

For server side validation Inky Dash uses the schema validation built into express-validator, "[s]chemas are a special, object-based way of defining validations or sanitizations on requests" 1. In the example below the requests value for the image property must be base64 encoded and the value of the palette_colour property has to be either 'red' or 'yellow'.

'image': {
    in: ['body'],
    errorMessage: 'Error uploading image',
    isBase64: {
        errorMessage: 'Image must be base64'
    },
...
'palette_colour': {
    in: ['body'],
    errorMessage: 'Error setting palette colour',
    custom: {
    errorMessage: 'Palette colour must be either \'red\' or \'yellow\'',
      options: value => {
        return ['red', 'yellow'].includes(value)
...
                
Example of the validation schema 2

Checks against the schema can then be made in the middleware, like in the below example for the upload endpoint:

const { imageUpload } = require('../validation/display-schema');
...
router.post('/upload',
    jsonParser
    checkSchema(imageUpload),
    validationErrorHandler,
...
                
Using the schema in middleware 3

After checking the request against the schema the following function "validationErrorHandler" formats any validation errors, returning an error to the client with a 400 status code if an error occurred, else a call to "next()" is made and we move onto the next step in the middleware.

State management

As previously mentioned Redux has been used in this project for state management. Being my first time using Redux there was a little bit of learning curve to begin with but once I had a structure things became more intuitive. As the time of writing I'm using Redux to maintain the state of the display and any errors, both have related actions, reducers, enums and interfaces which as a collective I'm calling a service.

-src
    ...
    -services
        -display
            -display.actions.ts
            -display.enum.ts
            -display.interface.ts
            -display.reducers.ts
        -errors
            -errors.actions.ts
            -errors.enum.ts
            -errors.interface.ts
            -errors.reducers.ts
    ...
                
File structure for services in the UI

As an example lets follow how the state for the display preview is managed:

  1. Starting with the upload form control, when an image is selected on the client a call is made to the setPreview function, passing the base64 encoded image.
  2. base64 => setPreview(dispatch, base64)
    Calling the setPreview function 4
  3. The setPreview function is part of the display service it dispatches a SET_PREVIEW action with the base64 encoded preview as the payload.
  4. export function setPreview(dispatch: Dispatch, base64: string | null): void {
        dispatch({
            type: DisplayActions.SET_PREVIEW,
            payload: { preview: base64 }
        });
    }
    Dispatching the set preview action 5
  5. The displays reducer then handles the action and sets the state for the preview.
  6. ...
    case DisplayActions.SET_PREVIEW: {
        return Object.assign( {}, state, {
            preview: action.payload.preview
        });
    }
    ...
    Reducer for the set preview action 6
  7. Using the Redux useSelector hook the display settings page is kept updated with the display state and can refresh the preview image when it changes.
  8. const imageState: DisplayState = useSelector((state: RootState) => {
        return state.displaySlice;
    })
    ...
    <img src={ `data:image/png;base64,${ imageState.preview }` }
    useSelector hook 7
  9. Finally to ensure the preview is cleared every time the form component is destroyed the useEffect hook is used to set the preview to null.
  10. React.useEffect(() => {
        return function cleanup() {
            setPreview(dispatch, null)
        }
    }, [dispatch]);
    useEffect hook for resetting the preview 8
Mentions

Other points of interest is a middleware async error handler that catches any errors thrown by async processes such as image manipulation or external calls to Python scripts. And a function that handles the execution a given Python script.

Inky Dash in action

Uploading an image

Notes

  1. Schema Validation: https://express-validator.github.io/docs/schema-validation.html
  2. display-schema.js: https://github.com/End-S/inky_dash/blob/497937240224dab274f64897059f7e4159117e2b/src/validation/display-schema.js
  3. display.js: https://github.com/End-S/inky_dash/blob/4e1f47f2783bb6bb18bfcb6d1dba6736b2e9a110/src/routes/display.js
  4. display-settings-form.tsx line 73: https://github.com/End-S/inky_dash/blob/497937240224dab274f64897059f7e4159117e2b/src/ui/src/pages/display-settings/components/display-settings-form.tsx#L73
  5. display.actions.ts line 42: https://github.com/End-S/inky_dash/blob/497937240224dab274f64897059f7e4159117e2b/src/ui/src/services/display/display.actions.ts#L42
  6. display.reducers.ts line 48: https://github.com/End-S/inky_dash/blob/497937240224dab274f64897059f7e4159117e2b/src/ui/src/services/display/display.reducers.ts#L48
  7. display-settings.tsx line 15: https://github.com/End-S/inky_dash/blob/844b3ae0ba93d574d46392407488add6a4c15215/src/ui/src/pages/display-settings/display-settings.tsx#L15
  8. display-settings-form.tsx line 33: https://github.com/End-S/inky_dash/blob/497937240224dab274f64897059f7e4159117e2b/src/ui/src/pages/display-settings/components/display-settings-form.tsx#L33