Raspberri PI: прием и передача сигналов на частоте 433mhz
На даче у меня управление батареями построено на реле Sonoff R3 DIY, которые включаются-выключаются при помощи WiFi. Это было моей ошибкой — управление получилось не очень стабильным. Реле имеют свойство периодически «отваливаться», из-за того что сигнал от роутера слабоват и весь дом покрывает с трудом. Решил освоить управление реле при помощи приёмопередатчиков 433mhz. Приём-передача по слухам гораздо стабильнее. Да и пультики-брелки можно будет прикрутить таким образом для управления уличным освещением. Приятно будет — подъезжаешь вечером на машине и шарахаться в темноте не нужно — с пульта включил свет. Плюс бонусом хочу на первый этаж поставить arduino с приёмником, и часть функционала с raspberry перекинуть на неё, т.к. тупо пинов уже не хватает к малинке.
» Читать далееDart: секунды в строку вида hh:mm:ss
Задача: преобразовать и отобразить секунды в строку часы-минуты-секунды
Решение:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
String TimeLeft(diff){ var timestamp = diff.floor(); var hours = (timestamp / 60 / 60).floor(); var minutes = ((timestamp / 60) - (hours * 60)).floor(); var seconds = timestamp % 60; if (seconds<0){ seconds=0;hours=0;minutes=0; } String hs=hours.toString(); hs=hs.length>1?hs:"0"+hs; String ms=minutes.toString(); ms=ms.length>1?ms:"0"+hs; String ss=seconds.toString(); ss=ss.length>1?ss:"0"+ss; return "$hs:$ms:$ss"; } |
Бонусом тоже самое на javascript:
1 2 3 4 5 6 7 8 9 10 |
function TimeLeft(diff){ var timestamp = Math.floor(diff); var hours = Math.floor(timestamp / 60 / 60); var minutes = Math.floor(timestamp / 60) - (hours * 60); var seconds = timestamp % 60; if (seconds<0){seconds=0;hours=0;minutes=0;} h=("0"+hours).slice(-2); m=("0"+minutes).slice(-2); s=("0"+seconds).slice(-2); }; |
Flutter: использование радиокнопок
Обычно радиокнопки во Flutter обертываю виджетом ListTile. Однако в этом случае получаются слишком большие отступы между кнопками, которые обычными средствами не убираются. В моем варианте, кнопки размещаются просто в Row строке:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
enum ChangingTime {t30a,t30,t60,t90,t120,t150,t180} ... ChangingTime? _chtime = ChangingTime.t30a; ... Container RadioLine(String title,ChangingTime rvalue){ return Container( padding: const EdgeInsets.only(bottom: 0,left: 0,right: 0,top: 0), height: 20, child: Row( children: [ Expanded( child: Padding( padding: const EdgeInsets.only(bottom: 0,left: 8,right: 8,top: 0), child: Text(title), ) ), Radio<ChangingTime>( value: rvalue, groupValue: _chtime, onChanged: (ChangingTime? value) { setState(() { _chtime = value; print(value); }); }, ), ], ) ); } ... ... RadioLine("30 минут с автопродлением",ChangingTime.t30a), new Divider(), RadioLine("30 минут",ChangingTime.t30), new Divider(), RadioLine("1 час",ChangingTime.t60), |
Получается вполне приемлимое:
Одно но. В таком случае выбор кнопки будет только по щелчку по переключателю, а не по тексту. Как вариант можно попробовать RadioListTile, но с ним отступы до желаемого минимума мне убрать не удалось.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
RadioListTile RadioLine2(String title,ChangingTime rvalue){ return RadioListTile<ChangingTime>( title:Text( title, ), dense: true, value: rvalue, groupValue: _chtime, visualDensity: VisualDensity( horizontal: VisualDensity.minimumDensity, vertical: VisualDensity.minimumDensity, ), controlAffinity: ListTileControlAffinity.trailing, onChanged: (ChangingTime? value) { setState(() { _chtime = value; print(value); }); }, ); } |
Flutter: древовидный список с анимацией
Задача: показать двухуровневый список с анимацией отображения выбора второго уровня.
Что сделаем? Отображением займутся два вложенных друг в друга ListView.builder. Отлавливает выбор модели при помощи GestureDetector и при отрисовке второго ListView.builder со списокм автомобилей, показываем только совпадающие с моделью. Также отображение моделей обертываем в AnimatedOpacity для плавного показа.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
class _AddAutoState extends State<AddAuto> { final AddAuto_scaffoldKey = GlobalKey<ScaffoldState>(); bool auto_list_is_loading=false; List ListAutoes=[]; List FilteredListAutoes=[]; int cur_model_select=0; void LoadListAutoes(context){ TRequests req=new TRequests(); req.request("GetListVendorsAndModels", jsonEncode({}), (List result){ setState(() { ListAutoes=result; FilteredListAutoes=result; auto_list_is_loading=true; }); }, (String error){ EasyLoading.showToast(error); }); } @override void initState() { WidgetsBinding.instance.addPostFrameCallback((_) => LoadListAutoes(context)); // эвент после того как страница отобразилась - обновим данные по пользователю } @override Widget build(BuildContext context) { return Scaffold( key: AddAuto_scaffoldKey, appBar: AppBar( iconTheme: IconThemeData(color: Colors.black), backgroundColor: Colors.white, //automaticallyImplyLeading: false, // убрать кнопку "назад" title: Text('Добавление автомобиля', style: TextStyle(fontFamily: 'Poppins', color: Colors.black, fontSize: 22,),), actions: [], centerTitle: false, elevation: 2, ), body: SingleChildScrollView( child: Column( children: [ Visibility( visible: auto_list_is_loading, child: Container( padding: const EdgeInsets.all(8), child: ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: const EdgeInsets.all(0), itemCount: FilteredListAutoes.length, itemBuilder: (BuildContext context, int index) { return GestureDetector( onTap: () { setState(() { print("тыкнули по модели автомобиля.."); cur_model_select=index; }); }, child: Column( children: [ Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.start, children: [ Container( height: 16, width: 16, child: Transform.rotate( angle: index==cur_model_select?90 * 3.14/180:0, child: IconButton ( padding: const EdgeInsets.all(0), icon: SvgPicture.asset('lib/images/arrow-right-small.svg',height: 16,width: 16,), onPressed: () {print('IconButton pressed ...');}, ), ) ), Text( FilteredListAutoes[index]["name"]+" ("+FilteredListAutoes[index]["cars"].length.toString()+")", style: TextStyle( fontWeight: index==cur_model_select?FontWeight.bold:FontWeight.w100, ) ) ], ), // рисуем список автомобилей AnimatedOpacity( opacity: index==cur_model_select ? 1.0 : 0.0, duration: const Duration(milliseconds: 1500), child: ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), padding: const EdgeInsets.all(0), itemCount: FilteredListAutoes[index]["cars"].length, itemBuilder: (BuildContext context, int index2) { return Visibility( visible: index==cur_model_select?true:false, child: Padding( padding: const EdgeInsets.only(left: 20,top: 4), child: GestureDetector( onTap: () { print("тыкнули по автомобилю.."+index2.toString()); }, child: Text(FilteredListAutoes[index]["cars"][index2]["name"]), ) ) ); } ), ), new Divider() ] ) ); } ), ) ), Visibility( visible: !auto_list_is_loading, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: const [ CardLoading( height: 30, borderRadius: BorderRadius.all(Radius.circular(15)), width: 100, margin: EdgeInsets.only(bottom: 10), ), CardLoading( height: 100, borderRadius: BorderRadius.all(Radius.circular(15)), margin: EdgeInsets.only(bottom: 10), ), ] ) ), ], ) ) ); } } |