Я дауншифтер. Так получилось, что последние три года мы с женой и
младшим ребёнком наслаждаемся сельским пейзажем за окном, свежим
воздухом и пением птиц. Удобства в доме, оптический интернет от
местного провайдера, мощный бесперебойник и нагрянувший ковид
неожиданно сделали идею переезда из мегаполиса не такой уж
странной.
Пока я увлеченно занимался веб разработкой, где-то на фоне жена
периодически жаловалась на проблемы выбора школы для ребёнка. И тут
(вдруг) ребёнок подрос и школьный вопрос встал ребром. Ладно,
значит, время пришло. Давайте вместе разберёмся, что же все-таки не
так с системой образования в бывшей 1/6 части суши, и что мы с вами
можем с этим сделать?
Традиционные методы очного обучения я оставлю за рамками этой
статьи. Скажу только, что у обычных школ есть как неоспоримые
преимущества, так и серьезные недостатки, к которым, кстати, в
последнее время добавилась вынужденная самоизоляция. Здесь мы
рассмотрим варианты дистанционного и семейного образования,
которые, по целому ряду причин, в последнее время привлекают все
больше родителей.
Внесу ясность: дистанционное обучение подразумевает занятия в
обычной школе с помощью дистанционных образовательных технологий
(ДОТ), а семейное означает добровольный уход из школы и обучение
только силами семьи (по сути, это старый добрый экстернат).
Впрочем, в любом случае ребёнка нужно прикрепить к какой-либо из
доступных школ, как минимум, для сдачи промежуточных
аттестаций.
А теперь немного наблюдений из жизни. С вынужденным переводом на
дистанционку детей, уже учившихся в обычной школе, все грустно.
Школьники воспринимают этот подарок судьбы как своего рода
каникулы, родители не привыкли следить за дисциплиной во время
занятий и в результате общая успеваемость неизбежно падает.
С первоклашками, особенно в случае семейной формы, у родителей,
пожалуй, появляется шанс поставить ребёнка на рельсы, используя
естественный интерес и эффект новизны. Лично для меня добиться
самостоятельности главная задача. Сидеть и делать с ребёнком
домашку я считаю верхом глупости. Конечно, если вы хотите, чтобы
ваши дети чего-то добились в жизни и не висели у вас на шее. Я
хочу, поэтому моя цель научить ребёнка учиться, правильно задавать
вопросы и вообще, думать своей головой.
Ближе к делу. Выбираем государственную школу
Пожалуй, семейное образование мне нравится больше из-за возможности
выбрать программу и график обучения. Да и физически посещать школу
можно реже. Но выбрать государственную школу, поговорить с
директором о прикреплении ребёнка и получить приказ о зачислении в
первый класс нужно уже в конце зимы, чтобы в сентябре не было
сюрпризов. Хотя, с юридической точки зрения, закон об образовании
вроде бы не требует ежегодных аттестаций, дедлайны, по моему опыту,
отлично мотивируют, поэтому пусть будут аттестации. Вряд ли любая
школа примет нас с распростертыми объятьями, но найти достойный
вариант в ближайшем городе мы сможем, я уверен.
Выбираем учебную программу
Именно выбираем. Пытаться составить программу самостоятельно, не
имея профильного образования, не разумно. Хотя существуют
государственные образовательные ресурсы, такие как Российская
Электронная Школа (
РЭШ)
и Московская Электронная Школа (
МЭШ), которых в теории могло было бы хватить, но Оба
варианта предоставляют планы уроков, видеозаписи, тесты и учебные
пособия. Вот чего мне не удалось найти, так это самих учебников,
даже по обязательной программе.
И тут нет самого главного: общения. Обучить ребёнка, показывая ему
бесконечные видеоролики и заставляя ставить галочки в тестах, не
получится. Значит, нужно либо проводить уроки полностью
самостоятельно, либо выбрать одну из онлайн школ.
Выбираем онлайн школу
Мы почти вернулись к тому, с чего начали. Дистанционка? Ладно,
присмотримся к ней повнимательней. Как вообще можно организовать
учебный процесс удаленно? Тут возникает много вопросов, я подниму
только ключевые:
* Живое общение. Что предлагают школы? Скайп, в лучшем случае Тимс.
Уроки по Скайпу? Серьёзно? Если я не ошибаюсь, на дворе 2020-й.
Открыть перед первоклашкой несколько окон с красивыми разноцветными
кнопочками и ждать, что он на них не нажмет, а будет пол-дня
послушно слушать скучного дядю или тетю? Ни разу таких детей не
видел. А вы?
* Домашка. Точнее, как она попадает к учителю на проверку? На самом
деле, это действительно сложный вопрос, возможно, даже не решаемый
в принципе. Существующие варианты:
- Написать в тетрадке, сфоткать и отправить учителю. Бр-р-р, не
хочу заставлять учителей ломать глаза в попытках прочесть мутные
фотки с мобильников, сделанные, как правило, по какому-то
неписанному закону в темноте.
- Отправить скан. Полумера, в общем случае невозможная из-за
отсутствия у родителей нужного оборудования.
- Оцифровать рукописный ввод с помощью дигитайзера или планшета.
Так себе вариант, но об этом чуть позже.
- Напечатать текст. В принципе, допустимо, но вот как ребёнок
введёт с клавиатуры, например, математическую или химическую
формулу? Никак. Плюс, для более продвинутых деток, проблема с
плагиатом.
- Выполнить онлайн тест. Это, безусловно, самый популярный
вариант. Полагаю, большинство школ, включая РЭШ и МЭШ,
ориентируются на него. На практике это означает скорее дрессировку,
чем обучение. Дети учатся ставить галочки в правильном месте. За
бортом остаются предметы, требующие любой формы творчества,
например, сочинения, а также диктанты и непопулярное теперь по
неведомой мне причине чистописание. Сюда же можно отнести умение
отстаивать своё мнение.
* Оценки. Очевидно, выставленные на уроке и при проверке домашних
заданий оценки должны попадать в электронный дневник, доступный
родителям. И они туда попадают. Вот только не сразу. Я
поинтересовался у старших детей, закончивших один из престижных
лицеев златоглавой (по иронии судьбы, с информационным уклоном),
почему так? Ответ, честно сказать, меня удивил. Оказывается,
учителя записывают оценки на бумажку, а после уроков вбивают их в
этот самый электронный дневник на государственном портале. И это в
то время, как Теслы Илона Маска бороздят просторы космоса
Ладно, пора провести небольшое техническое исследование и
проверить, может существуют объективные причины такого положения
дел?
Давайте определим требования к гипотетической идеальной платформе
для обучения. На самом деле, все просто: дети должны оставаться на
уроке, сосредоточившись на том, что говорит и показывает учитель,
при необходимости отвечая на вопросы и при желании поднимая руку.
По сути, нам нужно окно на полный экран с потоком с учительской
камеры, презентацией или интерактивной доской. Самый простой способ
добиться этого использовать технологию
WebRTC (real-time communications, коммуникации в
реальном времени). Эта штука работает в любом более-менее
современном браузере, не требует покупки дополнительного
оборудования и, к тому же, обеспечивает хорошее качество связи. И
да, этот стандарт требует асинхронного программирования как минимум
потому, что необходимый JS метод
navigator.mediaDevices.getUserMedia() возвращает
промис. Вроде все понятно, приступаю к
реализации.
Лирическое отступление о выборе фреймворка
В последнее время я все чаше слышу мнение
о том, что чистый JavaScript шагнул далеко вперед, проблем с
кроссбраузерностью уже нет и фреймворки на фронтэнде не нужны
вообще. Особенно часто критике подвергается
jQuery. Хорошо, стряхнем пыль со старого доброго JS
и сравним:
// Выбрать элементelement = $(selector);element = document.querySelector(selector);// Выбрать элемент внутри элементаelement2 = element.find(selector2);element2 = element.querySelector(selector2);// Скрыть элементelement.hide(); // добавляет стиль display: noneelement.classList.add('hidden');
Тут нужно пояснить, что CSS классу hidden, при желании, можно
прописать свойства opacity и transition, что даст эффект
fadeIn/fadeOut на чистом CSS. Отлично, давно хотел отказаться от JS
анимации!
// Слушать событие onClickelement.click(e => { ... });element.onclick = (e) => { ... }// Переключить классelement.toggleClass(class_name);element.classList.toggle(class_name);// Создать divdiv = $("<div>");div = document.createElement("div");// Вставить созданный div в element// (это не опечатка, можно писать одинаково)element.append(div);element.append(div);
И т.д и т.п. В целом, код на чистом JS получается немного более
многословным, но такова цена повышения производительности и
снижения трафика. Нет, я никого не агитирую, но для эксперимента
использую нативный JS с удовольствием!
WebRTC предназначен для связи между браузерами напрямую, по
технологии точка-точка (p2p). Однако, чтобы установить эту связь,
браузеры должны сообщить друг другу о своем намерении общаться. Для
этого понадобится сервер
сигнализации.
Пример базовой реализации простого видеочата,
использующего топологию full mesh
'use strict';(function () { const selfView = document.querySelector('#self-view'), remoteMaster = document.querySelector('#remote-master'), remoteSlaves = document.querySelector('#remote-slaves'); let localStream, selfStream = null, socket = null, selfId = null, connections = {}; // *********************** // UserMedia & DOM methods // *********************** const init = async () => { try { let stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: { width: { max: 640 }, height: { max: 480 } } }); localStream = stream; selfStream = new MediaStream(); stream.getVideoTracks().forEach(track => { selfStream.addTrack(track, stream); // track.kind == 'video' }); selfView.querySelector('video').srcObject = selfStream; } catch (e) { document.querySelector('#self-view').innerHTML = '<i>Веб камера и микрофон не найдены</i>'; console.error('Local stream not found: ', e); } wsInit(); } const createRemoteView = (id, username) => { let iDiv = document.querySelector('#pc' + id); if (!iDiv) { iDiv = document.createElement('div'); iDiv.className = 'remote-view'; iDiv.id = 'pc' + id; let iVideo = document.createElement('video'); iVideo.setAttribute('autoplay', 'true'); iVideo.setAttribute('playsinline', 'true'); let iLabel = document.createElement('span'); iDiv.append(iVideo); iDiv.append(iLabel); if (!remoteMaster.querySelector('video')) { remoteMaster.append(iDiv); iLabel.textContent = 'Ведущий'; } else { remoteSlaves.append(iDiv); iLabel.textContent = username; } remoteMaster.style.removeProperty('display'); } } // ******************************* // Signaling (Web Socket) methods // ******************************* const wsInit = () => { socket = new WebSocket(SIGNALING_SERVER_URL); socket.onopen = function (e) { log('[socket open] Соединение установлено'); } socket.onmessage = function (event) { log('[socket message] Данные получены с сервера', event); wsHandle(event.data); } socket.onclose = function (event) { if (event.wasClean) { log('[close] Соединение закрыто чисто, ' + `код=${event.code} причина=${event.reason}`); } else { log('[socket close] Соединение прервано', event); } clearInterval(socket.timer); } socket.onerror = function (error) { logError('[socket error]', error); } socket.timer = setInterval(() => { socket.send('heartbeat'); }, 10000); } const wsHandle = async (data) => { if (!data) { return; } try { data = JSON.parse(data); } catch (e) { return; } switch (data.type) { case 'handshake': selfId = data.uid; if (!Object.keys(data.users).length) { createRemoteView(selfId, 'Ведущий'); remoteMaster.querySelector('video').srcObject = selfStream; selfView.remove(); break; } else { selfView.style.removeProperty('display'); } for (let id in data.users) { await pcCreate(id, data.users[id]); } break; case 'offer': await wsHandleOffer(data); break; case 'answer': await wsHandleAnswer(data) break; case 'candidate': await wsHandleICECandidate(data); break; default: break; } } const wsHandleOffer = async (data) => { let pc = null; if (!connections[data.src]) { await pcCreate(data.src, data.username); } pc = connections[data.src].pc; // We need to set the remote description to the received SDP offer // so that our local WebRTC layer knows how to talk to the caller. let desc = new RTCSessionDescription(data.sdp); pc.setRemoteDescription(desc).catch(error => { logError('handleOffer', error); }); await pc.setLocalDescription(await pc.createAnswer()); wsSend({ type: 'answer', target: data.src, sdp: pc.localDescription }); connections[data.src].pc = pc; // ??? } const wsHandleAnswer = async (data) => { log('*** Call recipient has accepted our call, answer:', data); let pc = connections[data.src].pc; // Configure the remote description, // which is the SDP payload in our 'answer' message. let desc = new RTCSessionDescription(data.sdp); await pc.setRemoteDescription(desc).catch((error) => { logError('handleAnswer', error); }); } const wsHandleICECandidate = async (data) => { let pc = connections[data.src].pc; let candidate = new RTCIceCandidate(data.candidate); log('*** Adding received ICE candidate', candidate); pc.addIceCandidate(candidate).catch(error => { logError('handleICECandidate', error); }); } const wsSend = (data) => { if (socket.readyState !== WebSocket.OPEN) { return; } socket.send(JSON.stringify(data)); } // *********************** // Peer Connection methods // *********************** const pcCreate = async (id, username) => { if (connections[id]) { return; } try { let pc = new RTCPeerConnection(PC_CONFIG); pc.onicecandidate = (event) => pcOnIceCandidate(event, id); pc.oniceconnectionstatechange = (event) => pcOnIceConnectionStateChange(event, id); pc.onsignalingstatechange = (event) => pcOnSignalingStateChangeEvent(event, id); pc.onnegotiationneeded = (event) => pcOnNegotiationNeeded(event, id); pc.ontrack = (event) => pcOnTrack(event, id); connections[id] = { pc: pc, username: username } if (localStream) { try { localStream.getTracks().forEach( (track) => connections[id].pc.addTransceiver(track, { streams: [localStream] }) ); } catch (err) { logError(err); } } else { // Start negotiation to listen remote stream only pcOnNegotiationNeeded(null, id); } createRemoteView(id, username); } catch (error) { logError('Peer: Connection failed', error); } } const pcOnTrack = (event, id) => { let iVideo = document.querySelector('#pc' + id + ' video'); iVideo.srcObject = event.streams[0]; } const pcOnIceCandidate = (event, id) => { let pc = connections[id].pc; if (event.candidate && pc.remoteDescription) { log('*** Outgoing ICE candidate: ' + event.candidate); wsSend({ type: 'candidate', target: id, candidate: event.candidate }); } } const pcOnNegotiationNeeded = async (event, id) => { let pc = connections[id].pc; try { const offer = await pc.createOffer(); // If the connection hasn't yet achieved the "stable" state, // return to the caller. Another negotiationneeded event // will be fired when the state stabilizes. if (pc.signalingState != 'stable') { return; } // Establish the offer as the local peer's current // description. await pc.setLocalDescription(offer); // Send the offer to the remote peer. wsSend({ type: 'offer', target: id, sdp: pc.localDescription }); } catch(err) { logError('*** The following error occurred while handling' + ' the negotiationneeded event:', err); }; } const pcOnIceConnectionStateChange = (event, id) => { let pc = connections[id].pc; switch (pc.iceConnectionState) { case 'closed': case 'failed': case 'disconnected': pcClose(id); break; } } const pcOnSignalingStateChangeEvent = (event, id) => { let pc = connections[id].pc; log('*** WebRTC signaling state changed to: ' + pc.signalingState); switch (pc.signalingState) { case 'closed': pcClose(id); break; } } const pcClose = (id) => { let remoteView = document.querySelector('#pc' + id); if (connections[id]) { let pc = connections[id].pc; pc.close(); delete connections[id]; } if (remoteView) { remoteView.remove(); } } // ******* // Helpers // ******* const log = (msg, data) => { if (!data) { data = '' } console.log(msg, data); } const logError = (msg, data) => { if (!data) { data = '' } console.error(msg, data); } init();})();
Сервер сигнализации выполнен на Python фреймвоке aiohttp и
представляет собой простую вьюху, тривиально проксирующую запросы
WebRTC. Соединение с сервером в этом примере выполнено на
веб сокетах. Ну и, в дополнение, через канал
сигнализации передаются данные простого текстового чата.
Пример реализации сервера сигнализации
import jsonfrom aiohttp.web import WebSocketResponse, Responsefrom aiohttp import WSMsgTypefrom uuid import uuid1from lib.views import BaseViewclass WebSocket(BaseView): """ Process WS connections """ async def get(self): username = self.request['current_user'].firstname or 'Аноним' room_id = self.request.match_info.get('room_id') if room_id != 'test_room' and self.request['current_user'].is_anonymous: self.raise_error('forbidden') # @TODO: send 4000 if (self.request.headers.get('connection', '').lower() != 'upgrade' or self.request.headers.get('upgrade', '').lower() != 'websocket'): return Response(text=self.request.path) # ??? self.ws = WebSocketResponse() await self.ws.prepare(self.request) self.uid = str(uuid1()) if room_id not in self.request.app['web_sockets']: self.request.app['web_sockets'][room_id] = {} self.room = self.request.app['web_sockets'][room_id] users = {} for id, data in self.room.items(): users[id] = data['name'] ip = self.request.headers.get( 'X-FORWARDED-FOR', self.request.headers.get('X-REAL-IP', self.request.remote)) msg = { 'type': 'handshake', 'uid': str(self.uid), 'users': users, 'ip': ip} await self.ws.send_str(json.dumps(msg, ensure_ascii=False)) self.room[self.uid] = {'name': username, 'ws': self.ws} try: async for msg in self.ws: if msg.type == WSMsgType.TEXT: if msg.data == 'heartbeat': print('---heartbeat---') continue try: msg_data = json.loads(msg.data) if 'target' not in msg_data or msg_data['target'] not in self.room: continue msg_data['src'] = self.uid if 'type' in msg_data and 'target' in msg_data: if msg_data['type'] == 'offer': msg_data['username'] = username else: print('INVALID DATA:', msg_data) except Exception as e: print('INVALID JSON', e, msg) try: await self.room[msg_data['target']]['ws'].send_json( msg_data); except Exception as e: if 'target' in msg_data: self.room.pop(msg_data['target']) finally: self.room.pop(self.uid) return self.ws
Технология WebRTC, кроме видеосвязи, позволяет предоставить
браузеру разрешение на захват
содержимого дисплея или отдельного приложения,
что может оказаться незаменимым при проведении онлайн уроков,
вебинаров или презентаций. Отлично, используем.
Я так увлекся современными возможностями видеосвязи, что чуть не
забыл о самом главном предмете в классе интерактивной доске.
Впрочем, базовая реализация настолько тривиальна, что я не стану
загромождать ею эту статью. Просто добавляем
canvas, слушаем события перемещения мыши
onmousemove (ontouchmove для планшетов) и отправляем полученные
координаты всем подключенным точкам через тот же сервер
сигнализации.
Тестируем интерактивную доску
Тут понадобится планшет, дигитайзер и живой ребёнок. Заодно
проверим возможность оцифровки рукописного ввода.
Для начала я взял старенький планшет Galaxy Tab на андроиде 4.4,
самодельный стилус и первые попавшиеся прописи в качестве фона для
canvas. Дополнительные программы не устанавливал. Результат меня
обескуражил: мой планшет абсолютно не пригоден для письма! То есть
водить по нему пальцем без проблем, а вот попасть стилусом в контур
буквы, даже такой огромной, как на картинке ниже, уже проблема.
Плюс гаджет начинает тупить в процессе рисования, в результате чего
линии становятся ломанными. Плюс мне не удалось заставить ребёнка
не опирать запястье на экран, отчего под рукой остается
дополнительная мазня, а сам планшет начинает тормозить еще больше.
Итог: обычный планшет для письма на доске не подходит. Максимум его
возможностей двигать пальцем по экрану достаточно крупные фигуры.
Но предлагать это школьникам поздновато.
Ладно, у нас ведь чисто теоретическое исследование, верно? Тогда
берем дигитайзер (он же графический планшет) Wacom Bamboo формата
A8, и наблюдаем за ребёнком.
Замечу, что мой подопытный шести лет от роду получил ноутбук, да
еще с графическим пером первый раз в жизни. На получение базовых
навыков обращения с пером у нас ушло минут десять, а уже на втором
уроке ребёнок пользовался планшетом вполне уверенно, самостоятельно
стирал с доски, рисовал рожицы, цветочки, нашу собаку и даже начал
тыкать доступные в эпсилон окрестности кнопки, попутно задавая
вопросы типа А зачем в школе поднимают руку?. Вот только результат
неизменно оставлял желать лучшего. Дело в том, что дизайнеры и
художники для прорисовки элемента максимально увеличивают фрагмент
изображения, что и делает линии точными. Здесь же мы должны видеть
доску целиком, в масштабе 1:1. Тут и взрослый не попадет в линию.
Вот что получилось у нас:
Итоговый вердикт: ни о каком рукописном вводе не может быть и речи.
А если мы хотим поставить руку нашим детям, нужно добиваться этого
самостоятельно, на бумаге, школа в этом никак не поможет.
Надо сказать, что ребёнок воспринял все мои эксперименты с
восторгом и, более того, с тех пор ходит за мной хвостиком и просит
включить прописи. Уже хорошо, полученный навык ему пригодится,
только совсем для других целей.
Так или иначе, в результате экспериментов я фактически получил MVP
минимально жизнеспособный продукт (minimum viable product),
почти пригодный для проведения онлайн уроков, с видео/аудио
конференцией, общим экраном, интерактивной доской, простым
текстовым чатом и кнопкой поднять руку. Это на случай, если у
ребёнка вдруг не окажется микрофона. Да, такое бывает, особенно у
детей, не выучивших уроки.
Но в этой бочке меда, к сожалению, есть пара ложек дёгтя.
Тестируем WebRTC
Ложка 1. Поскольку наша видеосвязь использует прямые
подключения между клиентами, нужно первым делом проверить
масштабируемость такого решения. Для теста я взял старенький
ноутбук с двухядерным i5-3230M на борту, и начал подключать к нему
клиентов с отключенными веб камерами, то есть эмулируя режим
один-ко-многим:
Как видите, подопытный ноут в состоянии более-менее комфортно
вещать пяти клиентам (при загрузке CPU в пределах 60%). И это при
условии снижения разрешения исходящего видеопотока до 720p
(640x480px) и frame rate до 15 fps. В принципе, не так уж и плохо,
но при подключении класса из нескольких десятков учеников от фулл
меша придется отказаться в пользу каскадирования, то есть каждый из
первых пяти клиентов проксирует поток следующим пяти и так
далее.
Ложка 2. Для создания прямого интерактивного подключения
(ICE) между клиентами им нужно обойти сетевые экраны и
преобразователи NAT. Для этого WebRTC использует сервер
STUN, который сообщает клиентам внешние
параметры подключения. Считается, что в большинстве случаев этого
достаточно. Но мне почти сразу же повезло:
Как видите, отладчик ругается на невозможность ICE соединения и
требует подключения TURN сервера, то есть ретранслятора (relay). А
это уже ДОРОГО. Простым сервером сигнализации тут не обойтись.
Вывод придётся пропускать все потоки через медиа сервер.
Медиа сервер
Для тестирования я использовал
aiortc. Интересная разработка, позволяет подключить
браузер напрямую к серверу через WebRTC. Отдельная сигнализация не
нужна, можно использовать канал данных самого соединения. Это
работает, все мои тестовые точки подключились без проблем. Но вот с
производительностью беда. Простое эхо видео/аудио потока с теми же
ограничениями 720p и 15fps съели 50% моего виртуального CPU на
тестовом VDS. Причём, если увеличить нагрузку до 100%, видео поток
не успевает выгружаться клиентам и начинает забивать память, что в
итоге приводит к остановке сервера. Очевидно, Питон, который мы
любим использовать для задач обработки ввода/вывода, не очень
подходит для CPU bound. Придётся поискать более специализированное
решение, например,
Янус или
Джитси.
В любом случае, полноценное решение потребует выделенных серверов,
по моим прикидкам, из расчёта 1 ядро на комнату (класс). Это уже
стоит денег и выходит за рамки простого тестирования, поэтому в
этом месте первую фазу моего исследования я буду считать
завершённой.
Выводы
1. Мягко говоря, странно видеть на официальном портале Российской
Федерации
инструкции по скачиванию и ссылки на регистрацию
в программе бывшего потенциального врага 1 (тут про Microsoft
Teams). И это в эпоху санкций и импортозамещения.
Нет, лично я за дружбу народов и вообще всяческую толерантность, но
неужели только у меня от такой интеграции встают волосы дыбом?
Разве нет наших разработок?
2. Интеграция МЭШ/РЭШ со школами. Вообще-то, разработчики МЭШ
молодцы, даже с Яндекс.репетитором интеграцию сделали. А как быть с
выставлением оценок в реальном времени во время уроков, когда будет
API? Или я чего-то не знаю?
3. Выбирая дистанционную или семейную форму обучения, нужно быть
честным с собой: переложить ответственность за образование ребёнка
на школу не получится. Вся работа по проведению уроков (в случае
семейного образования), поддержанию дисциплины и
самоорганизованности (в любом случае) полностью ложится на
родителей. Нужно отдавать себе в этом отчёт и найти время на
занятия. Впрочем, в больших семьях с этим проблем быть не
должно.
4. Я не буду приводить здесь ссылки на выбранные нами онлайн школы,
чтобы это не сочли рекламой. Скажу только, что мы остановились на
частных школах среднего ценового диапазона. В любом случае,
окончательный результат будет зависеть от ребёнка и получим мы его
не раньше сентября.
Или есть смысл довести до логического конца начатую здесь
разработку и организовать свою школу? Что вы думаете? Есть
единомышленники, имеющие профильные знания и опыт в области
образования?
Полезные ссылки:
Российская Электронная Школа
Московская Электронная Школа
Библиотека МЭШ
Разработчику