{"id":19362731,"url":"https://github.com/smartdevicelink/generic_hmi","last_synced_at":"2025-04-23T12:33:13.268Z","repository":{"id":10805525,"uuid":"65479904","full_name":"smartdevicelink/generic_hmi","owner":"smartdevicelink","description":"A sample HMI to use with sdl_core","archived":false,"fork":false,"pushed_at":"2022-10-26T16:00:43.000Z","size":33304,"stargazers_count":8,"open_issues_count":36,"forks_count":27,"subscribers_count":13,"default_branch":"master","last_synced_at":"2025-04-02T15:21:18.414Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/smartdevicelink.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":".github/CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-08-11T15:20:34.000Z","updated_at":"2023-08-25T17:29:39.000Z","dependencies_parsed_at":"2023-01-11T20:15:02.014Z","dependency_job_id":null,"html_url":"https://github.com/smartdevicelink/generic_hmi","commit_stats":null,"previous_names":[],"tags_count":25,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smartdevicelink%2Fgeneric_hmi","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smartdevicelink%2Fgeneric_hmi/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smartdevicelink%2Fgeneric_hmi/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/smartdevicelink%2Fgeneric_hmi/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/smartdevicelink","download_url":"https://codeload.github.com/smartdevicelink/generic_hmi/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250435276,"owners_count":21430252,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-10T07:30:04.988Z","updated_at":"2025-04-23T12:33:08.249Z","avatar_url":"https://github.com/smartdevicelink.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Getting Started \n\n## Get an instance of SDL Core running\n\nNote: This requires you to use Ubuntu 18.04 or 20.04.\n\nClone the [SDL Core repository](https://github.com/smartdevicelink/sdl_core) and follow the setup instructions for the project. After the project is built, run an instance of SDL Core in your terminal.\n\n### Dependencies\n\n- nvm\n- chromium-browser\n- python3 and pip\n- ffmpeg\n- ffmpeg-python\n- pyopenssl\n\n```\nsudo apt install chromium-browser ffmpeg python3 python3-pip -y\npython3 -m pip install ffmpeg-python pyopenssl\n```\n\nCheck out [nvm on github](https://github.com/nvm-sh/nvm#installing-and-updating) to learn how to install and use nvm!\n\n### Build and Run the HMI\n\nOnce SDL Core is running, follow these steps to set up the Generic HMI.\n\nFirst, clone this repository. Once cloned, you can initialize the git submodules in this project by running the following commands:\n```\ncd generic_hmi\ngit submodule init\ngit submodule update\n```\nAlternatively, you can clone this repository with the --recurse-submodules flag.\n\nThe build directory is not included in the github repository. In order to use the generic HMI you must build the application yourself.\n\nNote: These instructions are written for Node version 12\n\nInstall NVM and use node version 12\n```\nnvm use 12\n```\n\nInstall dependencies (you might need to clean the `node_modules` folder):\n```\nnpm install\n```\n\n\nBuild the project:\n```\nnpm run build\n```\nNote: This command must be run before launching the HMI in the browser.\n\nAfter running the build command, you can launch the Generic HMI in a web browser:\n```\nchromium-browser generic_hmi/build/index.html\n```\n**NOTE** Chromium is the only supported and tested browser. Browsers built on top of Chromium (Google Chrome) should work but are not officially supported.\n\n### HMI Backend\n\nThe generic_hmi includes an additional backend component that is required for some features, such as in-browser video streaming, policy table updates using the vehicle modem and accessing the webengine app store.\n\n1. Run `deploy_server.sh` in the root folder\n2. Start and run the HMI normally\n\n#### Connection status icon\n\nThe backend connection status is indicated by an icon in the hmi settings page (top-right). \n\n\u003cimg src= \"./src/img/static/0x30.svg\" width=50/\u003e\n \nThe icon will contain a check mark if the backend server is connected/cross if the backend server is disconnected.\n\nClicking on the icon will display a prompt allowing the user to set the url for the backend. Once the URL is set, the HMI will attempt to re-connect to the backend server.\n\n#### Features\n\nThe following features can be used in the hmi if the backend server is connected.\n\n##### HMI PTU\n\nSelect the `PTU using in-vehicle modem` checkbox to enable the feature\n\n##### Video streaming\n\nStart a video service from the SDL app. The video stream should start in the browser.\n\nVideo streaming also requires you to have all the [aforementioned dependencies](#dependencies) installed.\n\n##### Webengine app store\n\nThe app store can be accessed from the hmi settings page. Clicking on any of the listed webengine apps will allow you to download the webengine app.\n\n## Developing/Modifying the HMI\n\nThe main third-party technologies we use to develop this HMI are React, React-Redux, and React-Router. The HMI component of SDL is responsible for processing and responding to RPCs which are received from a connected SDL Core instance.\n\n### Key Files\n\n#### src/index.js\n\nThis is the main entry point for the entire application. It sets up the routes and highest level components in the React app. Once the application is loaded, it attempts to connect to an instance of SDL Core.\n\n#### Controllers/Controller.js\n\nThis is the main path to all things SDL related. The Controller routes RPCs coming from SDL to sub-controllers so that they can be handled, and responds to SDL. Sub-controllers all implement a `handleRPC()` function. The handleRPC function returns true if the Controller should respond with a generic success to SDL, return false for a generic false, return an object with a key of `rpc` to respond with a custom RPC, and return `null` if the Controller should not respond (such as in the case of incoming notifications from SDL). The Controller also implements a `sanitize` function which can be used to manipulate RPCs before they're sent off to a sub-controller to be handled.\n\n### Implementing an RPC\n\nImplementing an RPC is the main activity when developing this HMI as it related to communicating with SDL Core. There are three basic behaviors that can be implemented\n\n  1. An RPC comes in from SDL Core which changes some information displayed to the user in a view (Implementing Requests)\n  2. The user takes action on an element in the React Application which generates a message to SDL Core (Sending messages to SDL Core)\n  3. An RPC comes in from SDL Core which forces the current view in the React Application to change (Changing the router history)\n\n#### Implementing Requests\n\nFirst, add a case statement to the appropriate Sub-Controller. If the RPC is named `UI.something`, the appropriate sub-controller is the UIController. The case statement should dispatch a method to the store that you'll define shortly. Import that method name from actions at the top of the Controller. Head over to `actions.js`, add a new string to the `Actions` const and export a new method of the same name which returns an object containing the same parameters you passed and a `type` property which is the new `Action` you defined. In `reducers.js` you can now add a case statement for the Action name you created. Return a new state object based on the parameters passed into the action from the Controller. This state will be used in a container to send the appropriate information to a React Component. For more information about actions and reducers check out http://redux.js.org/docs/basics/index.html. Example of all this below.\n\n```js\n// UIController.js\nimport {\n    show // Importing the new action for use with store.dispatch\n} from '../actions'\n...\nhandleRPC(rpc) {\n    ...\n        case \"Show\":\n            store.dispatch(show( // dispatching the action with the needed info\n                rpc.params.appID,\n                rpc.params.showStrings,\n                rpc.params.graphic,\n                rpc.params.softButtons\n            ))\n            return true\n    ...\n\n// actions.js\nexport const Actions = {\n    SHOW: \"SHOW\" // Defining the new type\n}\n...\nexport const show = (appID, showStrings, graphic, softButtons) =\u003e { // exporting the show action\n    return {\n        type: Actions.SHOW, // Specifying the new type\n        appID: appID,\n        showStrings: showStrings,\n        graphic: graphic,\n        softButtons: softButtons\n    }\n}\n\n// reducers.js\nfunction ui(state = {}, action) {\n    switch (action.type) {\n        case Actions.SHOW: // implementing the reducer, you can do this in any of the functions that are to be reduced into state\n            var newState = { ...state } // Copy over the old state\n            var app = newState[action.appID] ? newState[action.appID] : newAppState() // Find the app specified by the action that we're changing state for or create a new one\n            newState[action.appID] = app // set it back in case we created a new one\n            if (action.showStrings \u0026\u0026 action.showStrings.length \u003e 0) {\n                app.showStrings = action.showStrings // Change show strings if they changed\n            }\n            if (action.graphic) { // Add the graphic to the state if it exists\n                app.graphic = action.graphic\n            }\n            if (action.softButtons \u0026\u0026 action.softButtons.length \u003e 0) { // Change soft buttons if they changed\n                app.softButtons = action.softButtons\n            }\n            return newState // self explanatory\n...\n\n```\n\nAt this point, you'll need to think about what component needs the information in the React application which you've just added to the state. In the example above, the information in the Show RPC is used in the MediaPlayerBody component as MetaData. So create a file for the container which will be hooked up directly to the React Component which needs the information about show. Below is a commented version of the Metadata container which parses out the useful information added to the state by the Show RPC for use in the React Component.\n\n```js\n// Metadata.js\nimport { connect } from 'react-redux' // so we can connect this container with the appropriate react component\nimport MediaPlayerBody from '../MediaPlayerBody' // this is the React component we're connecting it which will use the props we create off the state\n\nconst mapStateToProps = (state) =\u003e { // a function you always have to implement\n    var activeApp = state.activeApp // The active application in the react component\n    var metadata = state.ui[activeApp] // The UI metadata for that application (we created all this in reducers.js when we implemented Actions.SHOW)\n    if (metadata === undefined) return {} // Do nothing if there is no metadata yet\n    var props = { // Default mainfields for the react component\n        mainField1: null,\n        mainField2: null,\n        mainField3: null\n    }\n    metadata.showStrings.map ((textField) =\u003e { // Iterate all the strings added by the show\n        switch (textField.fieldName) { // Each textField has a fieldName which is its type\n            case \"mainField1\": // Map types to props that'll be used by the Component\n                props.mainField1 = textField.fieldText\n                break\n            case \"mainField2\":\n                props.mainField2 = textField.fieldText\n                break\n            case \"mainField3\":\n                props.mainField3 = textField.fieldText\n                break\n        }\n    })\n    // If there is a graphic, add it to the props\n    props.graphic = metadata.graphic ? metadata.graphic.value : \"http://www.unrecorded.mu/wp-content/uploads/2014/02/St.-Vincent-St.-Vincent1.jpg\"\n    return props // Return the props to the component\n}\n\n// This is where we would implement a way to communicate back to redux if there was some action the user can take to change our state. More on that later\nconst mapDispatchToProps = (dispatch) =\u003e {\n    return {}\n}\n\n// Connect this container with the component which will use it and export it\nexport const MediaMetadata = connect(\n    mapStateToProps,\n    mapDispatchToProps\n)(MediaPlayerBody)\n\nexport default MediaMetadata\n\n```\n\nThe last thing we need to do is make sure that in our react application we are now using our container instead of the original react component which is not connected, and that the react component is using the properly named props which were passed by the container in render.\nIn this example, this was done in MediaPlayer.js\n\n```js\n// MediaPlayer.js\n...\nimport { MediaMetadata } from './containers/Metadata';\n\nexport default class MediaPlayer extends React.Component {\n    constructor() {\n        super();\n    }\n\n    render() {\n        return ( // We created the MediaMetadata container in this tutorial\n            \u003cdiv\u003e\n                \u003cAppHeader backLink=\"/\" menuName=\"Apps\"/\u003e\n                \u003cMediaMetadata /\u003e\n                \u003cProgressBar /\u003e\n                \u003cButtons /\u003e\n            \u003c/div\u003e\n        )\n    }\n}\n\n```\n\nThe component we actually connected was the MediaPlayerBody, let's take a look to see how the props we created off the state in the container are used\n\n```js\nimport React from 'react';\n\nimport AlbumArt from './AlbumArt';\nimport MediaTrackInfo from './containers/MediaTrackInfo_c'\n\nexport default class MediaPlayerBody extends React.Component {\n    constructor(props) {\n        super(props);\n    }\n\n    render() {\n        return (\n            // mainFields and graphic - Perfect. Those exist because this component is connected to our redux state by the container.\n            // Any time SDL changes the state that is tied to this component, this component will re-render and update. \n            \u003cdiv className=\"media-player-body\"\u003e\n                \u003cAlbumArt image={this.props.graphic} /\u003e\n                \u003cdiv className=\"media-track\"\u003e\n                    \u003cp className=\"t-small t-medium th-f-color\"\u003e{this.props.mainField3}\u003c/p\u003e\n                    \u003cp className=\"t-large t-light th-f-color\"\u003e{this.props.mainField1}\u003c/p\u003e\n                    \u003cp className=\"t-large t-light th-f-color-secondary\"\u003e{this.props.mainField2}\u003c/p\u003e\n                    \u003cMediaTrackInfo /\u003e\n                \u003c/div\u003e\n            \u003c/div\u003e\n        )\n    }\n}\n```\n\n#### Sending Messages to SDL Core\n\nThere are many situations where a user's action in the React Application needs to trigger a message to be sent to SDL. For example, after an application uses the `AddCommand` RPC to add items to the App's in-HMI menu, and the user selects one of those items, we need to be able to tell SDL about that selection by sending the notification called `UI.OnCommand` to SDL Core so it can be relayed to the connected application. We do this by implementing the `mapDispatchToProps` function in our container. For the menu, this function does two things - changes state by called dispatch (in the same way we changed our state before in our sub-controller) and sending a message to a sub controller to notify SDL Core about the event.\n\n```js\nconst mapDispatchToProps = (dispatch) =\u003e {\n    return {\n        onSelection: (appID, cmdID, menuID) =\u003e { // Our function is called onSelection, so the component can use this.props.onSelection()\n            if (menuID) {\n                dispatch(activateSubMenu(appID, menuID)) // We can used the passed in dispatch to change state (don't forget to define and import the action activateSubMenu)\n            }\n            else if (cmdID) {\n                uiController.onSystemContext(\"MAIN\", appID) // We can call functions on uiController (again, don't forget to import) which send messages to SDL\n                uiController.onCommand(cmdID, appID)\n            }\n        }\n    }\n}\n```\n\nFrom here, the only thing left to do is implement the functions called on the sub controller. When the sub controllers imported by the main Controller, the main controller adds a function called `addListener`. The sub-controller can use the listener to send messages directly to SDL Core.\n\n```js\n// UIController.js\nonSystemContext(context, appID) {\n    this.listener.send(RpcFactory.OnSystemContextNotification(context, appID))\n}\nonCommand(cmdID, appID) {\n    this.listener.send(RpcFactory.OnCommandNotification(cmdID, appID))\n}\n```\n\nThe only thing left to do now is to make sure the connected React Component is properly using the method we defined in `mapDispatchToProps`. In this example, it's the `HScrollMenu` which passes the onSelection prop to an HScrollMenuItem which calls onSelection as we've defined\n\n```js\n// HScrollMenuItem.js\nonClick={() =\u003e this.props.onSelection(this.props.appID, this.props.cmdID, this.props.menuID)}\u003e\n```\n\n### Changing the router history\n\nThe last common activity required to implement an SDL HMI completely is the ability to change views based on messages received by SDL. Views in the React Application are defined by Routes. When a user selects an item that changes the view, a route is taken such as `/inapplist`. We can force a route to be taken using React Routers `withRouter`. Right now, since the AppHeader component is rendered in every single view, it is responsible for forcing a change to routing history (thereby changing the view) when it renders. So the flow is\n\n  1. Message comes into SDL\n  2. Dispatch to store\n  3. Implement Action and change app state in Reducer\n  4. AppHeader is rendered\n  5. AppHeader checks state to see if a change needs to be forced\n  6. If a change needs to be forced, AppHeader makes the change\n  7. Everything re-renders\n\nThis forced change is done in the React lifecycle method called `componentWillReceiveProps`, which gives the AppHeader access to the nextProps that will be used in the components render _before_ it's rendered and in time to make a change.\n\n```js\n// AppHeader.js\n// withRouter will give us access to router on this components props\nimport { withRouter } from 'react-router';\n...\n    componentWillReceiveProps (nextProps) {\n        if (nextProps.isDisconnected) {\n            this.props.history.push(\"/\") // The app got disconnected so we force a change back to the menu\n        }\n    }\n...\nexport default withRouter(AppHeader) // Hook this component up with router.\n```\n\n## Create React App\n\nThis project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).\n\n## Available Scripts\n\nIn the project directory, you can run:\n\n### `npm start`\n\nRuns the app in the development mode.\u003cbr /\u003e\nOpen [http://localhost:3000](http://localhost:3000) to view it in the browser.\n\nThe page will reload if you make edits.\u003cbr /\u003e\nYou will also see any lint errors in the console.\n\n### `npm test`\n\nLaunches the test runner in the interactive watch mode.\u003cbr /\u003e\nSee the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.\n\n### `npm run build`\n\nBuilds the app for production to the `build` folder.\u003cbr /\u003e\nIt correctly bundles React in production mode and optimizes the build for the best performance.\n\nThe build is minified and the filenames include the hashes.\u003cbr /\u003e\nYour app is ready to be deployed!\n\nSee the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.\n\n### `npm run eject`\n\n**Note: this is a one-way operation. Once you `eject`, you can’t go back!**\n\nIf you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.\n\nInstead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.\n\nYou don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmartdevicelink%2Fgeneric_hmi","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsmartdevicelink%2Fgeneric_hmi","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmartdevicelink%2Fgeneric_hmi/lists"}