Skip to content

Commit

Permalink
add a levels screen for minidsp use
Browse files Browse the repository at this point in the history
  • Loading branch information
3ll3d00d committed Jun 19, 2021
1 parent 74fade5 commit 195fcb5
Show file tree
Hide file tree
Showing 21 changed files with 572 additions and 251 deletions.
24 changes: 21 additions & 3 deletions ezbeq/apis/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ def load_filter(catalogue: List[CatalogueEntry], bridge: DeviceRepository, devic
:return: current state after load, 200 if loaded, 400 if no such entry, 500 if unable to load
'''
logger.info(f"Sending {entry_id} to Slot {slot}")
match: CatalogueEntry = next((c for c in catalogue if c.idx == entry_id), None)
if not match:
matching_entry: CatalogueEntry = next((c for c in catalogue if c.idx == entry_id), None)
if not matching_entry:
logger.warning(f"No title with ID {entry_id} in catalogue")
return {'message': 'Title not found, please refresh.'}, 404
try:
bridge.load_filter(device_name, slot, match)
bridge.load_filter(device_name, slot, matching_entry)
return bridge.state(device_name).serialise(), 200
except InvalidRequestError as e:
logger.exception(f"Invalid request {entry_id} to Slot {slot}")
Expand Down Expand Up @@ -184,6 +184,24 @@ def patch(self, device_name: str):
return self.__bridge.state(device_name).serialise()


@v1_api.route('/<string:device_name>/levels')
class DeviceLevels(Resource):

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__bridge: DeviceRepository = kwargs['device_bridge']

def get(self, device_name: str) -> Tuple[dict, int]:
try:
return self.__bridge.levels(device_name), 200
except InvalidRequestError as e:
logger.exception(f"Invalid device {device_name}")
return {}, 400
except Exception as e:
logger.exception(f"Failed to get levels for {device_name}")
return {}, 500


@v1_api.route('/<string:device_name>/config/<string:slot>/active')
@v1_api.doc(params={
'device_name': 'The dsp device name',
Expand Down
7 changes: 7 additions & 0 deletions ezbeq/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ def set_gain(self, slot: Optional[str], channel: Optional[int], gain: float) ->
def update(self, params: dict) -> bool:
pass

@abstractmethod
def levels(self) -> dict:
pass


class DeviceRepository:

Expand Down Expand Up @@ -148,6 +152,9 @@ def set_gain(self, name: str, slot: Optional[str], channel: Optional[int], gain:
def update(self, device_name: str, params: dict) -> bool:
return self.__get_device(device_name).update(params)

def levels(self, device_name: str) -> dict:
return self.__get_device(device_name).levels()


def create_devices(cfg: Config, ws_server: WsServer, catalogue: CatalogueProvider) -> List[Device]:
devices = []
Expand Down
4 changes: 4 additions & 0 deletions ezbeq/htp1.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ def unmute(self, slot: Optional[str], channel: Optional[int]) -> None:
def set_gain(self, slot: Optional[str], channel: Optional[int], gain: float) -> None:
raise NotImplementedError()

def levels(self) -> dict:
# TODO implement
return {}

def on_mso(self, mso: dict):
logger.info(f"Received {mso}")
version = mso['versions']['swVer']
Expand Down
4 changes: 4 additions & 0 deletions ezbeq/jriver.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,10 @@ def unmute(self, slot: Optional[str], channel: Optional[int]) -> None:
def set_gain(self, slot: Optional[str], channel: Optional[int], gain: float) -> None:
raise NotImplementedError()

def levels(self) -> dict:
# TODO implement
return {}


def convert_filter_to_mc_dsp(filt: dict, target_channels: str) -> List[Dict[str, str]]:
'''
Expand Down
38 changes: 24 additions & 14 deletions ezbeq/minidsp.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import logging
import os
from concurrent.futures.thread import ThreadPoolExecutor
Expand Down Expand Up @@ -193,23 +194,15 @@ def __load_state(self) -> MinidspState:
return result if result else MinidspState(self.name)

def __read_state_from_device(self) -> Optional[MinidspState]:
values = {}
lines = None
try:
kwargs = {'retcode': None} if self.__ignore_retcode else {}
lines = self.__runner(timeout=self.__cmd_timeout, **kwargs)
for line in lines.split('\n'):
if line.startswith('MasterStatus'):
idx = line.find('{ ')
if idx > -1:
vals = line[idx + 2:-2].split(', ')
for v in vals:
if v.startswith('preset: '):
values['active_slot'] = str(int(v[8:]) + 1)
elif v.startswith('mute: '):
values['mute'] = v[6:] == 'true'
elif v.startswith('volume: Gain('):
values['mv'] = float(v[13:-1])
status = json.loads(self.__runner['-o', 'jsonline'](timeout=self.__cmd_timeout, **kwargs))
values = {
'active_slot': status['master']['preset'],
'mute': status['master']['mute'],
'mv': status['master']['volume']
}
return MinidspState(self.name, **values)
except:
logger.exception(f"Unable to parse device state {lines}")
Expand Down Expand Up @@ -376,6 +369,23 @@ def __update_slot(self, slot: dict) -> bool:
any_update = True
return any_update

def levels(self) -> dict:
return self.__executor.submit(self.__read_levels_from_device).result(timeout=self.__cmd_timeout)

def __read_levels_from_device(self) -> dict:
lines = None
try:
kwargs = {'retcode': None} if self.__ignore_retcode else {}
lines = self.__runner['-o', 'jsonline'](timeout=self.__cmd_timeout, **kwargs)
levels = json.loads(lines)
return {
'input': levels['input_levels'],
'output': levels['output_levels']
}
except:
logger.exception(f"Unable to load levels {lines}")
return {}


class MinidspBeqCommandGenerator:

Expand Down
2 changes: 2 additions & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-scripts": "4.0.3",
"uplot": "^1.6.12",
"uplot-react": "^1.1.0",
"web-vitals": "^0.2.4"
},
"scripts": {
Expand Down
184 changes: 35 additions & 149 deletions ui/src/App.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import React, {useCallback, useEffect, useState} from 'react';
import React, {useEffect, useState} from 'react';
import {createMuiTheme, makeStyles, ThemeProvider} from '@material-ui/core/styles';
import ezbeq from './services/ezbeq';
import useMediaQuery from '@material-ui/core/useMediaQuery';
import CssBaseline from '@material-ui/core/CssBaseline';
import Header from "./components/Header";
import {pushData} from "./services/util";
import Footer from "./components/Footer";
import Catalogue from "./components/Catalogue";
import Slots from "./components/Slots";
import Filter from "./components/Filter";
import Entry from "./components/Entry";
import {Grid} from "@material-ui/core";
import {BottomNavigation, BottomNavigationAction} from "@material-ui/core";
import LocalLibraryIcon from '@material-ui/icons/LocalLibrary';
import EqualizerIcon from '@material-ui/icons/Equalizer';
import ErrorSnack from "./components/ErrorSnack";
import MainView from "./components/main";
import Levels from "./components/levels";

const useStyles = makeStyles((theme) => ({
root: {
Expand Down Expand Up @@ -44,47 +43,16 @@ const App = () => {
};

const classes = useStyles();
const [showBottomNav, setShowBottomNav] = useState(false);
// errors
const [err, setErr] = useState(null);
// catalogue data
const [entries, setEntries] = useState([]);
const [filteredEntries, setFilteredEntries] = useState([]);
// device state
const [availableDevices, setAvailableDevices] = useState({});
// view selection
const [selectedDeviceName, setSelectedDeviceName] = useState('');
// user selections
const [selectedAuthors, setSelectedAuthors] = useState([]);
const [selectedYears, setSelectedYears] = useState([]);
const [selectedAudioTypes, setSelectedAudioTypes] = useState([]);
const [selectedContentTypes, setSelectedContentTypes] = useState([]);
const [selectedFreshness, setSelectedFreshness] = useState([]);
const [txtFilter, setTxtFilter] = useState('');
const [showFilters, setShowFilters] = useState(false);
const [selectedEntryId, setSelectedEntryId] = useState(-1);
const [selectedSlotId, setSelectedSlotId] = useState(null);
const [userDriven, setUserDriven] = useState(false);

const toggleShowFilters = () => {
setShowFilters((prev) => !prev);
};

const getSelectedDevice = useCallback(() => {
if (selectedDeviceName && availableDevices.hasOwnProperty(selectedDeviceName)) {
return availableDevices[selectedDeviceName];
}
return {};
}, [selectedDeviceName, availableDevices]
);

useEffect(() => {
const d = getSelectedDevice();
if (d && d.hasOwnProperty('slots')) {
const slot = d.slots.find(s => s.active === true);
if (slot) {
setSelectedSlotId(slot.id);
}
}
}, [getSelectedDevice, selectedDeviceName, availableDevices]);
const [selectedNav, setSelectedNav] = useState('catalogue');

// initial data load
useEffect(() => {
Expand All @@ -96,123 +64,41 @@ const App = () => {
}, []);

useEffect(() => {
if (availableDevices && !selectedDeviceName) {
const deviceNames = Object.keys(availableDevices);
if (deviceNames.length > 0) {
setSelectedDeviceName(deviceNames[0]);
}
}
}, [availableDevices, selectedDeviceName]);

useEffect(() => {
const txtMatch = e => {
const matchOn = txtFilter.toLowerCase()
if (e.title.toLowerCase().includes(matchOn)) {
return true;
} else if (e.hasOwnProperty('altTitle')) {
if (e.altTitle.toLowerCase().includes(matchOn)) {
return true;
}
}
return false;
}

// catalogue filter
const isMatch = (entry) => {
if (!selectedAuthors.length || selectedAuthors.indexOf(entry.author) > -1) {
if (!selectedYears.length || selectedYears.indexOf(entry.year) > -1) {
if (!selectedAudioTypes.length || entry.audioTypes.some(at => selectedAudioTypes.indexOf(at) > -1)) {
if (!selectedContentTypes.length || selectedContentTypes.indexOf(entry.contentType) > -1) {
if (!selectedFreshness.length || selectedFreshness.indexOf(entry.freshness) > -1) {
if (!txtFilter || txtMatch(entry)) {
return true;
}
}
}
}
}
}
return false;
}
pushData(setFilteredEntries, () => entries.filter(isMatch), setErr);
}, [entries, selectedAudioTypes, selectedYears, selectedAuthors, selectedContentTypes, selectedFreshness, txtFilter]);

useEffect(() => {
const d = getSelectedDevice();
if (d && userDriven && d.hasOwnProperty('slots')) {
const slot = d.slots.find(s => s.id === selectedSlotId);
if (slot && slot.last && slot.last !== "ERROR" && slot.last !== "Empty") {
setTxtFilter(slot.last);
}
}
}, [getSelectedDevice, selectedSlotId, setTxtFilter, userDriven]);
setShowBottomNav([...Object.keys(availableDevices)].find(k => availableDevices[k].hasOwnProperty('masterVolume')));
}, [availableDevices, setShowBottomNav]);

const useWide = useMediaQuery('(orientation: landscape) and (min-height: 580px)');
const devices = <Slots selectedDeviceName={selectedDeviceName}
selectedEntryId={selectedEntryId}
selectedSlotId={selectedSlotId}
useWide={useWide}
setSelectedSlotId={setSelectedSlotId}
setUserDriven={setUserDriven}
device={getSelectedDevice()}
setDevice={d => replaceDevice(d)}
setError={setErr}/>;
const catalogue = <Catalogue entries={filteredEntries}
setSelectedEntryId={setSelectedEntryId}
selectedEntryId={selectedEntryId}
useWide={useWide}/>;
const entry = <Entry selectedDeviceName={selectedDeviceName}
selectedEntry={selectedEntryId ? entries.find(e => e.id === selectedEntryId) : null}
useWide={useWide}
setDevice={d => replaceDevice(d)}
selectedSlotId={selectedSlotId}
device={getSelectedDevice()}
setError={setErr}/>;
return (
<ThemeProvider theme={theme}>
<CssBaseline/>
<div className={classes.root}>
<ErrorSnack err={err} setErr={setErr}/>
<Header txtFilter={txtFilter}
setTxtFilter={setTxtFilter}
availableDeviceNames={Object.keys(availableDevices)}
setSelectedDeviceName={setSelectedDeviceName}
selectedDeviceName={selectedDeviceName}
showFilters={showFilters}
toggleShowFilters={toggleShowFilters}/>
<Filter visible={showFilters}
selectedAudioTypes={selectedAudioTypes}
setSelectedAudioTypes={setSelectedAudioTypes}
selectedFreshness={selectedFreshness}
setSelectedFreshness={setSelectedFreshness}
selectedYears={selectedYears}
setSelectedYears={setSelectedYears}
selectedAuthors={selectedAuthors}
setSelectedAuthors={setSelectedAuthors}
selectedContentTypes={selectedContentTypes}
setSelectedContentTypes={setSelectedContentTypes}
filteredEntries={filteredEntries}
setError={setErr}/>
{
useWide
selectedNav === 'catalogue'
?
<Grid container>
<Grid item xs={6} md={6}>
{devices}
<Grid container>
{catalogue}
</Grid>
</Grid>
<Grid item xs={6} md={6}>
{entry}
</Grid>
</Grid>
<MainView entries={entries}
setErr={setErr}
replaceDevice={replaceDevice}
availableDevices={availableDevices}
selectedDeviceName={selectedDeviceName}
setSelectedDeviceName={setSelectedDeviceName}
showBottomNav={showBottomNav}/>
:
<>
{devices}
{catalogue}
{entry}
</>
<Levels availableDevices={availableDevices}
selectedDeviceName={selectedDeviceName}
setSelectedDeviceName={setSelectedDeviceName}
setErr={setErr}/>
}
{
showBottomNav
?
<BottomNavigation value={selectedNav}
onChange={(event, newValue) => {
setSelectedNav(newValue);
}}>
<BottomNavigationAction label="Catalogue" value="catalogue" icon={<LocalLibraryIcon/>}/>
<BottomNavigationAction label="Levels" value="levels" icon={<EqualizerIcon/>}/>
</BottomNavigation>
: null
}
<Footer/>
</div>
Expand Down

0 comments on commit 195fcb5

Please sign in to comment.