diff --git a/0PLAN.org b/0PLAN.org deleted file mode 100644 index e3c583d..0000000 --- a/0PLAN.org +++ /dev/null @@ -1,90 +0,0 @@ -#+title: Frontend - -* Plan - -** 0.3 - -- [ ] versioned schema support - - [ ] barschema table - - [ ] function to check - - [ ] function to set - - [ ] initial creation of tables at version 1 assuming OK if tables exist -- [ ] users import from hackerbase - -** 0.4 - -- [ ] add inventory check table -- [ ] add inventory item check table -- [ ] create new check -- [ ] check item -- [ ] close check -- [ ] inventory check component - -** Later - -- [ ] add static token support - - [ ] hardwired token for the React.JS app? -- [ ] authentication - - [ ] login/logout - - [ ] allow API calls only with login token - -* Finished - -** 0.1 - -- [X] statically compiled binary HTTP server -- [X] include directory tree contents -- [X] handle frontend with mime type -- [X] move app to / again -- [X] load certificates -- [X] deployment preparations - - [X] crosscompilation - no, compile in qemu or on separate host - - [X] connect to postgres - - [X] use qr-scanner - no, qr-barcode-scanner is needed - - [X] rsync + build in qemu or host without react (filters or something) -- [X] API infrastructure - - [X] handle API calls - - [X] API registry syntax - - [X] lookup barcode in DB - - [X] separate module brmbar-data for queries - - [X] separate module api-servlets - -** 0.2 - -- [X] use localStorage to store user - - [X] add barcode scanner to get the user - - [X] allow editing - - [X] add UserSelect component -- [X] integrate script for starting build qemu system -- [X] add org file to the repository after cleanup - -* Qemu - -#+BEGIN_SRC -wget img zip # 2019-04-08-raspbian-stretch.zip -unzip 2019-04-08-raspbian-stretch.zip -sudo mkdir /mnt/image -sudo mount -o loop,offset=4194304 2019-04-08-raspbian-stretch.img /mnt/image/ -# In qemu-system-armhf terminal! -cp kernel, DT -umount /mnt/image -resize img -#+END_SRC - -sources.list - archive debian (remove rpi), archive raspberry -https://archive.raspberrypi.org/debian/ -https://archive.debian.org/debian/ -apt-get --no - -* React - Vite - -#+BEGIN_SRC sh - npm create vite@latest - # "frontend" - # React - # Javascript - cd frontend - npm install - npm run dev - npm run build -#+END_SRC diff --git a/backend/brminv.scm b/backend/brminv.scm index 48c5c28..808bf70 100644 --- a/backend/brminv.scm +++ b/backend/brminv.scm @@ -47,7 +47,6 @@ (define -db-user- (make-parameter #f)) (define -db-name- (make-parameter #f)) (define -db-pass- (make-parameter #f)) -(define -db-enabled- (make-parameter #t)) (command-line print-help @@ -83,8 +82,6 @@ (-db-user- dbuser)) (-dp (dbpass) "Database password" (-db-pass- dbpass)) - (-dd () "Disable database" - (-db-enabled- #f)) ) (define ssl? (and (-certificate-) (-key-) #t)) @@ -123,8 +120,7 @@ (print "current user id: " (current-user-id)) (print "current effective user id: " (current-effective-user-id)) -(when (-db-enabled-) - (bar-db-init! (-db-name-) (-db-host-) (-db-user-) (-db-pass-))) +(bar-db-init! (-db-name-) (-db-host-) (-db-user-) (-db-pass-)) (define (handle-api-calls) (define plst (cdr (uri-path (request-uri (current-request))))) diff --git a/backend/texts.scm b/backend/texts.scm index de6ac11..643e50e 100644 --- a/backend/texts.scm +++ b/backend/texts.scm @@ -38,7 +38,7 @@ (chicken format)) ;; Short banner - (define banner-line "BrmInv 0.2 (c) 2023-2025 Brmlab, z.s.") + (define banner-line "BrmInv 0.1 (c) 2023-2025 Brmlab, z.s.") ;; The license of this file and of the whole suite. (define license "ISC License diff --git a/frontend/src/App.css b/frontend/src/App.css index 6638b9a..467c548 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1,6 +1,6 @@ .App { background-color: var(--background-color); color: var(--primary-text-color) !important; - height: 100vh; - width: 10vh; + min-height: 100vh; + width: 100%; } diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 0f3b48f..2f4a741 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,88 +1,13 @@ -import { useState, useEffect } from 'react'; +import { useState } from 'react'; import BarcodeScannerComponent from 'react-qr-barcode-scanner'; -import Container from 'react-bootstrap/Container'; -import Row from 'react-bootstrap/Row'; -import Col from 'react-bootstrap/Col'; +import { Container, Row, Col } from 'react-bootstrap'; import Table from 'react-bootstrap/Table'; import Alert from 'react-bootstrap/Alert'; -import Form from 'react-bootstrap/Form'; -import Button from 'react-bootstrap/Button'; -import Card from 'react-bootstrap/Card'; import './App.css'; import 'bootstrap/dist/css/bootstrap.min.css'; function App() { - // Ensure we have persistent (informal) user information - const [user, setUser] = useState(""); - useEffect(() => { - const user = localStorage.getItem('user'); - if (user) { - setUser(user); - } - }, []); - useEffect(() => { - localStorage.setItem('user', user); - }, [user]); - - // If no user, must be scanned/set otherwise - return ( - <> - {user === "" ? - - : - } - - ); -} - -function NoUserView({setUser}) { - const [preUser, setPreUser] = useState(""); - return ( - - - BrmInv Anonymous - - User name: - setPreUser(v.target.value)} /> - Enter a user name or scan your barcode. - - - { - if (result) { - setPreUser(result.text); - }}} - delay={1500} - /> - - - - - - - ); -} - -function UserView({user, setUser}) { - return ( - - - {user} - - - - - - - ); -} - -function BarItemScanner() { const [reqAccount, setReqAccount] = useState(''); const [actAccount, setActAccount] = useState(''); const [balance, setBalance] = useState(-1); @@ -112,7 +37,7 @@ function BarItemScanner() { }; return ( - <> + {statusMsg} - + ); } diff --git a/install-eggs-arm.sh b/install-eggs-arm.sh new file mode 100644 index 0000000..fbd5da6 --- /dev/null +++ b/install-eggs-arm.sh @@ -0,0 +1,64 @@ +#!/bin/sh +# +# install-eggs.sh +# +# Local installer of CHICKEN eggs required for building. +# +# ISC License +# +# Copyright 2023 Brmlab, z.s. +# Dominik Pantůček +# +# Permission to use, copy, modify, and/or distribute this software +# for any purpose with or without fee is hereby granted, provided +# that the above copyright notice and this permission notice appear +# in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# + +# Source root directory +owd=$(pwd) +cd $(dirname "$0") +SRCDIR=$(pwd) +cd "$owd" + +# Make temporary prefix directory (eggs shared throwaway files) +TMPDIR=$(mktemp -d) + +# Installs given egg locally +chicken_install() { + echo "Installing $1 ..." + # CHICKEN_INSTALL_PREFIX="$TMPDIR" \ + # CHICKEN_REPOSITORY_PATH="$SRCDIR/eggs-arm":`./cross-chicken-arm/bin/arm-chicken-install -repository` \ + # CHICKEN_INSTALL_REPOSITORY="$SRCDIR/eggs-arm" \ + # ./cross-chicken-arm/bin/arm-chicken-install "$1" 2>&1 | \ + # sed -u 's/^/ /' +# CHICKEN_INSTALL_PREFIX="$TMPDIR" \ + ./cross-chicken-arm/bin/arm-chicken-install "$1" 2>&1 | \ + sed -u 's/^/ /' +} + +# Removes throwaway files +chicken_cleanup() { + echo "Cleaning up ..." + rm -fr ${TMPDIR} +} + +# Always cleanup +trap chicken_cleanup INT QUIT + +# Install required eggs +chicken_install spiffy +chicken_install openssl +chicken_install postgresql + +# Normal termination cleanup +chicken_cleanup diff --git a/install-eggs.sh b/install-eggs.sh index 08c098c..2bcb95c 100644 --- a/install-eggs.sh +++ b/install-eggs.sh @@ -57,7 +57,6 @@ chicken_install openssl chicken_install spiffy chicken_install postgresql chicken_install json -chicken_install posix-groups # Normal termination cleanup chicken_cleanup diff --git a/schema/0000-init.sql b/schema/0000-init.sql deleted file mode 100644 index 52afd7e..0000000 --- a/schema/0000-init.sql +++ /dev/null @@ -1,313 +0,0 @@ --- --- 0000-init.sql --- --- Initial SQL schema construction as of 2025-04-20 (or so) --- --- ISC License --- --- Copyright 2023-2025 Brmlab, z.s. --- Dominik Pantůček --- --- Permission to use, copy, modify, and/or distribute this software --- for any purpose with or without fee is hereby granted, provided --- that the above copyright notice and this permission notice appear --- in all copies. --- --- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL --- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED --- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE --- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR --- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS --- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, --- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN --- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. --- - --- To require fully-qualified names -SELECT pg_catalog.set_config('search_path', '', false); - --- Privileged schema with protected data -CREATE SCHEMA IF NOT EXISTS brmbar_privileged; - --- Initial versioning -CREATE TABLE IF NOT EXISTS brmbar_privileged.brmbar_schema( - ver INTEGER NOT NULL -); - --- ---------------------------------------------------------------- --- Legacy Schema Initialization --- ---------------------------------------------------------------- - -DO $$ -DECLARE v INTEGER; -BEGIN - SELECT ver FROM brmbar_privileged.brmbar_schema INTO v; - IF v IS NULL THEN - -- -------------------------------- - -- Legacy Types - - SELECT COUNT(*) INTO v - FROM pg_catalog.pg_type typ - INNER JOIN pg_catalog.pg_namespace nsp - ON nsp.oid = typ.typnamespace - WHERE nsp.nspname = 'public' - AND typ.typname='exchange_rate_direction'; - IF v=0 THEN - RAISE NOTICE 'Creating type exchange_rate_direction'; - CREATE TYPE public.exchange_rate_direction - AS ENUM ('source_to_target', 'target_to_source'); - ELSE - RAISE NOTICE 'Type exchange_rate_direction already exists'; - END IF; - - SELECT COUNT(*) INTO v - FROM pg_catalog.pg_type typ - INNER JOIN pg_catalog.pg_namespace nsp - ON nsp.oid = typ.typnamespace - WHERE nsp.nspname = 'public' - AND typ.typname='account_type'; - IF v=0 THEN - RAISE NOTICE 'Creating type account_type'; - CREATE TYPE public.account_type - AS ENUM ('cash', 'debt', 'inventory', 'income', 'expense', - 'starting_balance', 'ending_balance'); - ELSE - RAISE NOTICE 'Type account_type already exists'; - END IF; - - SELECT COUNT(*) INTO v - FROM pg_catalog.pg_type typ - INNER JOIN pg_catalog.pg_namespace nsp - ON nsp.oid = typ.typnamespace - WHERE nsp.nspname = 'public' - AND typ.typname='transaction_split_side'; - IF v=0 THEN - RAISE NOTICE 'Creating type transaction_split_side'; - CREATE TYPE public.transaction_split_side - AS ENUM ('credit', 'debit'); - ELSE - RAISE NOTICE 'Type transaction_split_side already exists'; - END IF; - - -- -------------------------------- - -- Currencies sequence, table and potential initial data - - CREATE SEQUENCE IF NOT EXISTS public.currencies_id_seq - START WITH 2 INCREMENT BY 1; - CREATE TABLE IF NOT EXISTS public.currencies ( - id INTEGER PRIMARY KEY NOT NULL DEFAULT NEXTVAL('public.currencies_id_seq'::regclass), - name VARCHAR(128) NOT NULL, - UNIQUE(name) - ); - INSERT INTO public.currencies (id, name) VALUES (1, 'Kč') - ON CONFLICT DO NOTHING; - - -- -------------------------------- - -- Exchange rates table - no initial data required - - CREATE TABLE IF NOT EXISTS public.exchange_rates ( - valid_since TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, - - target INTEGER NOT NULL, - FOREIGN KEY (target) REFERENCES public.currencies (id), - - source INTEGER NOT NULL, - FOREIGN KEY (source) REFERENCES public.currencies (id), - - rate DECIMAL(12,2) NOT NULL, - rate_dir public.exchange_rate_direction NOT NULL - ); - - -- -------------------------------- - -- Accounts sequence and table and 4 initial accounts - - CREATE SEQUENCE IF NOT EXISTS public.accounts_id_seq - START WITH 2 INCREMENT BY 1; - CREATE TABLE IF NOT EXISTS public.accounts ( - id INTEGER PRIMARY KEY NOT NULL DEFAULT NEXTVAL('public.accounts_id_seq'::regclass), - - name VARCHAR(128) NOT NULL, - UNIQUE (name), - - currency INTEGER NOT NULL, - FOREIGN KEY (currency) REFERENCES public.currencies (id), - - acctype public.account_type NOT NULL, - - active BOOLEAN NOT NULL DEFAULT TRUE - ); - INSERT INTO public.accounts (id, name, currency, acctype) - VALUES (1, 'BrmBar Cash', (SELECT id FROM public.currencies WHERE name='Kč'), 'cash') - ON CONFLICT DO NOTHING; - INSERT INTO public.accounts (name, currency, acctype) - VALUES ('BrmBar Profits', (SELECT id FROM public.currencies WHERE name='Kč'), 'income') - ON CONFLICT DO NOTHING; - INSERT INTO public.accounts (name, currency, acctype) - VALUES ('BrmBar Excess', (SELECT id FROM public.currencies WHERE name='Kč'), 'income') - ON CONFLICT DO NOTHING; - INSERT INTO public.accounts (name, currency, acctype) - VALUES ('BrmBar Deficit', (SELECT id FROM public.currencies WHERE name='Kč'), 'expense') - ON CONFLICT DO NOTHING; - - -- -------------------------------- - -- Barcodes - - CREATE TABLE IF NOT EXISTS public.barcodes ( - barcode VARCHAR(128) PRIMARY KEY NOT NULL, - - account INTEGER NOT NULL, - FOREIGN KEY (account) REFERENCES public.accounts (id) - ); - INSERT INTO public.barcodes (barcode, account) - VALUES ('_cash_', (SELECT id FROM public.accounts WHERE acctype = 'cash')) - ON CONFLICT DO NOTHING; - - -- -------------------------------- - -- Transactions - - CREATE SEQUENCE IF NOT EXISTS public.transactions_id_seq - START WITH 1 INCREMENT BY 1; - CREATE TABLE IF NOT EXISTS public.transactions ( - id INTEGER PRIMARY KEY NOT NULL DEFAULT NEXTVAL('public.transactions_id_seq'::regclass), - time TIMESTAMP DEFAULT NOW() NOT NULL, - - responsible INTEGER, - FOREIGN KEY (responsible) REFERENCES public.accounts (id), - - description TEXT - ); - - -- -------------------------------- - -- Transaction splits - - CREATE SEQUENCE IF NOT EXISTS public.transaction_splits_id_seq - START WITH 1 INCREMENT BY 1; - CREATE TABLE IF NOT EXISTS public.transaction_splits ( - id INTEGER PRIMARY KEY NOT NULL DEFAULT NEXTVAL('public.transaction_splits_id_seq'::regclass), - - transaction INTEGER NOT NULL, - FOREIGN KEY (transaction) REFERENCES public.transactions (id), - - side public.transaction_split_side NOT NULL, - - account INTEGER NOT NULL, - FOREIGN KEY (account) REFERENCES public.accounts (id), - amount DECIMAL(12,2) NOT NULL, - - memo TEXT - ); - - -- -------------------------------- - -- Account balances view - - CREATE OR REPLACE VIEW public.account_balances AS - SELECT ts.account AS id, - accounts.name, - accounts.acctype, - - sum( - CASE - WHEN ts.side = 'credit'::public.transaction_split_side THEN - ts.amount - ELSE ts.amount - END) AS crbalance - FROM public.transaction_splits ts - LEFT JOIN public.accounts ON accounts.id = ts.account - GROUP BY ts.account, accounts.name, accounts.acctype - ORDER BY (- sum( - CASE - WHEN ts.side = 'credit'::public.transaction_split_side THEN - ts.amount - ELSE ts.amount - END)); - - -- -------------------------------- - -- Transaction nice splits view - - CREATE OR REPLACE VIEW public.transaction_nicesplits AS - SELECT ts.id, - ts.transaction, - ts.account, - CASE - WHEN ts.side = 'credit'::public.transaction_split_side THEN - ts.amount - ELSE ts.amount - END AS amount, - a.currency, - ts.memo - FROM public.transaction_splits ts - LEFT JOIN public.accounts a ON a.id = ts.account - ORDER BY ts.id; - - -- -------------------------------- - -- Transaction cash sums view - - CREATE OR REPLACE VIEW public.transaction_cashsums AS - SELECT t.id, - t."time", - sum(credit.credit_cash) AS cash_credit, - sum(debit.debit_cash) AS cash_debit, - a.name AS responsible, - t.description - FROM public.transactions t - LEFT JOIN ( SELECT cts.amount AS credit_cash, - cts.transaction AS cts_t - FROM public.transaction_nicesplits cts - LEFT JOIN public.accounts a_1 ON a_1.id = cts.account OR a_1.id = cts.account - WHERE a_1.currency = (( SELECT accounts.currency - FROM public.accounts - WHERE accounts.name::text = 'BrmBar Cash'::text)) - AND (a_1.acctype = ANY (ARRAY['cash'::public.account_type, 'debt'::public.account_type])) - AND cts.amount < 0::numeric) credit ON credit.cts_t = t.id - LEFT JOIN ( SELECT dts.amount AS debit_cash, - dts.transaction AS dts_t - FROM public.transaction_nicesplits dts - LEFT JOIN public.accounts a_1 ON a_1.id = dts.account OR a_1.id = dts.account - WHERE a_1.currency = (( SELECT accounts.currency - FROM public.accounts - WHERE accounts.name::text = 'BrmBar Cash'::text)) - AND (a_1.acctype = ANY (ARRAY['cash'::public.account_type, 'debt'::public.account_type])) - AND dts.amount > 0::numeric) debit ON debit.dts_t = t.id - LEFT JOIN public.accounts a ON a.id = t.responsible - GROUP BY t.id, a.name - ORDER BY t.id DESC; - - -- -------------------------------- - -- Function to check schema version (used in migrations) - - CREATE OR REPLACE FUNCTION brmbar_privileged.has_exact_schema_version( - IN i_ver INTEGER - ) RETURNS INTEGER - VOLATILE NOT LEAKPROOF LANGUAGE plpgsql AS $x$ - DECLARE - v_ver INTEGER; - BEGIN - SELECT ver INTO v_ver FROM brmbar_privileged.brmbar_schema; - IF v_ver is NULL THEN - RETURN false; - ELSE - RETURN v_ver = i_ver; - END IF; - END; - $x$; - - -- -------------------------------- - -- - - CREATE OR REPLACE FUNCTION brmbar_privileged.upgrade_schema_version_to( - IN i_ver INTEGER - ) RETURNS INTEGER - VOLATILE NOT LEAKPROOF LANGUAGE plpgsql AS $x$ - DECLARE - v_ver INTEGER; - BEGIN - SELECT ver FROM brmbar_privileged.brmbar_schema INTO v_ver; - IF v_ver=(i_ver-1) THEN - UPDATE brmbar_privileged.brmbar_schema SET ver = i_ver; - ELSE - RAISE EXCEPTION 'Invalid brmbar schema version transition (% -> %)', v_ver, i_ver; - END IF; - END; - $x$; - - -- Initialize version 1 - INSERT INTO brmbar_privileged.brmbar_schema(ver) VALUES(1); - END IF; -END; -$$; diff --git a/tools/build-in-qemu.sh b/tools/build-in-qemu.sh index ebd2093..a14913f 100644 --- a/tools/build-in-qemu.sh +++ b/tools/build-in-qemu.sh @@ -1,28 +1,4 @@ #!/bin/sh -# -# build-in-qemu.sh -# -# Expects running armhf qemu system, builds the binary inside. -# -# ISC License -# -# Copyright 2025 Brmlab, z.s. -# Dominik Pantůček -# -# Permission to use, copy, modify, and/or distribute this software -# for any purpose with or without fee is hereby granted, provided -# that the above copyright notice and this permission notice appear -# in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL -# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE -# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR -# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS -# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, -# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -# if [ -z "$1" ] ; then echo "Usage: $0 password" diff --git a/tools/run-build-qemu-system.sh b/tools/run-build-qemu-system.sh deleted file mode 100644 index c76d8b8..0000000 --- a/tools/run-build-qemu-system.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/sh -# -# run-build-qemu-system.sh -# -# Runs the emulated armhf system for building the application. -# -# ISC License -# -# Copyright 2025 Brmlab, z.s. -# Dominik Pantůček -# -# Permission to use, copy, modify, and/or distribute this software -# for any purpose with or without fee is hereby granted, provided -# that the above copyright notice and this permission notice appear -# in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL -# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE -# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR -# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS -# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, -# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN -# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -# - -qemu-system-armhf \ - -machine raspi2b \ - -nographic \ - -dtb bcm2710-rpi-3-b-plus.dtb \ - -m 1G \ - -smp 4 \ - -kernel kernel7.img \ - -sd 2019-04-08-raspbian-stretch.img \ - -append "rw earlyprintk loglevel=8 console=ttyAMA0,115200 root=/dev/mmcblk0p2 rootdelay=1 dwc_otg.fiq_fsm_enable=0" \ - -usb \ - -device usb-net,netdev=net0 \ - -netdev user,id=net0,hostfwd=tcp::2222-:22