- Migrate from Decap CMS to Directus

- Added Dockerfile
- New form submittion
- Refactored
This commit is contained in:
Sergey Yarkov 2026-02-25 14:09:03 +03:00
commit 0e93e72d40
130 changed files with 21594 additions and 18706 deletions

14
.dockerignore Normal file
View File

@ -0,0 +1,14 @@
node_modules
dist
.astro
.env
.env.example
.git
.gitignore
dockerfile
.dockerignore
.vscode
.prettierrc
codegen.ts
schema.graphql
README.md

View File

@ -1,3 +1,4 @@
REDIS_HOST=redis-1234.p124.eu-central-5-5.ec2.cloud.redislabs.com ASTRO_TELEMETRY_DISABLED=1
REDIS_PASSWORD=superpassword DIRECTUS_URL=
REDIS_PORT=1234 DIRECTUS_URL_GRAPHQL=
DIRECTUS_TOKEN=

View File

@ -1,5 +1,5 @@
{ {
"printWidth": 100, "printWidth": 120,
"semi": true, "semi": true,
"singleQuote": false, "singleQuote": false,
"tabWidth": 2, "tabWidth": 2,

View File

@ -1,3 +1,5 @@
{ {
"githubPullRequests.ignoredPullRequestBranches": ["develop"] "githubPullRequests.ignoredPullRequestBranches": ["develop"],
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
} }

19
Dockerfile Normal file
View File

@ -0,0 +1,19 @@
ARG NODE_IMAGE=node:24.0.0-alpine
FROM ${NODE_IMAGE} AS build
WORKDIR /app
COPY package*.json .
RUN npm ci
COPY . .
RUN npm run build
FROM ${NODE_IMAGE} AS production
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/package*.json .
RUN npm ci --omit=dev
EXPOSE 9966
CMD [ "node", "./dist/server/entry.mjs"]

View File

@ -13,7 +13,6 @@ Personal website created with Astro.
- [x] Multilang web-site - [x] Multilang web-site
- [x] Table of contents - [x] Table of contents
- [x] Content management via [NetlifyCMS](https://www.netlifycms.org/) - [x] Content management via [NetlifyCMS](https://www.netlifycms.org/)
- [x] Prefetch pages via [Quicklink](https://getquick.link/)
- [x] Form handling via [Netlify Forms](https://www.netlify.com/products/forms/) - [x] Form handling via [Netlify Forms](https://www.netlify.com/products/forms/)
- [x] Comment system via [Disqus](https://disqus.com/) - [x] Comment system via [Disqus](https://disqus.com/)
- [x] RSS Feed for articles - [x] RSS Feed for articles

View File

@ -1,90 +1,38 @@
import { defineConfig } from "astro/config"; import { defineConfig, envField } from "astro/config";
/** /**
* Integrations * Integrations
*/ */
import sitemap from "@astrojs/sitemap"; import node from "@astrojs/node";
import solidJs from "@astrojs/solid-js"; import solidJs from "@astrojs/solid-js";
import icon from "astro-icon"; import icon from "astro-icon";
/**
* Rehype plugins
*/
import rehypeSlug from "rehype-slug";
import rehypeAutolinkHeadings from "rehype-autolink-headings";
/**
* Other required modules
*/
import { h } from "hastscript";
import { toString } from "hast-util-to-string";
import { escape } from "html-escaper";
import languages from "./src/i18n/languages";
import { DEFAULT_LANGUAGE } from "./src/constants";
const createAnchorLabel = (heading) => {
const node = h("span.anchor-label", escape(heading));
if (node.properties) node.properties["is:raw"] = true;
return node;
};
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
site: "https://yarkov.tech", site: "https://yarkov.tech",
compressHTML: true, compressHTML: true,
integrations: [ output: "server",
sitemap({ prefetch: true,
i18n: { server: {
defaultLocale: DEFAULT_LANGUAGE, port: 9966,
locales: Object.fromEntries(Object.keys(languages).map((key) => [key, key])), host: true,
}, allowedHosts: ["showcase.yarkov.tech"],
}),
icon(),
solidJs(),
],
markdown: {
syntaxHighlight: "prism",
rehypePlugins: [
rehypeSlug,
[
rehypeAutolinkHeadings,
{
properties: {
class: "anchor-link",
},
behavior: "after",
group: ({ tagName }) =>
h(`div.heading-wrapper.level-${tagName}`, {
tabIndex: -1,
}),
content: (heading) => [
h(
`span.anchor-icon`,
undefined,
h(
"svg",
{
width: 18,
height: 18,
version: 1.1,
viewBox: "0 0 16 16",
xlmns: "http://www.w3.org/2000/svg",
ariaHidden: "true",
"data-filter": "none",
},
h("path", {
fillRule: "evenodd",
fill: "currentcolor",
d: "M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z",
})
)
),
createAnchorLabel(toString(heading)),
],
},
],
],
}, },
adapter: node({
mode: "standalone",
}),
env: {
schema: {
DIRECTUS_URL: envField.string({ context: "server", access: "secret", default: "" }),
DIRECTUS_URL_GRAPHQL: envField.string({
context: "server",
access: "secret",
default: "http://directus:8055/graphql",
}),
DIRECTUS_TOKEN: envField.string({ context: "server", access: "secret", default: "" }),
},
},
integrations: [icon(), solidJs()],
trailingSlash: "ignore", trailingSlash: "ignore",
scopedStyleStrategy: "where", scopedStyleStrategy: "where",
}); });

31
codegen.ts Normal file
View File

@ -0,0 +1,31 @@
import type { CodegenConfig } from "@graphql-codegen/cli";
const config: CodegenConfig = {
schema: [
{
[`${process.env.DIRECTUS_URL}/graphql`]: {
headers: {
Authorization: `Bearer ${process.env.DIRECTUS_TOKEN}`,
},
},
},
],
documents: ["src/**/*.ts"],
ignoreNoDocuments: true,
generates: {
"./src/graphql/": {
preset: "client",
config: {
documentMode: "string",
},
},
"./schema.graphql": {
plugins: ["schema-ast"],
config: {
includeDirectives: true,
},
},
},
};
export default config;

17
compose.yml Normal file
View File

@ -0,0 +1,17 @@
services:
yarkov-tech-node:
container_name: yarkov-tech-node
restart: unless-stopped
image: sergeyyarkov/yarkovtech:latest
networks:
- nginx_net
env_file: .env
build:
context: ./
dockerfile: Dockerfile
ports:
- 9966
networks:
nginx_net:
external: true

View File

@ -1,10 +0,0 @@
[build]
command = "npm run build"
publish = "dist"
functions = "netlify/functions"
[[redirects]]
from = "/api/*" # simplify all calls to serverless functions
to = "/.netlify/functions/:splat" # all function calls will go to this path
status = 200 # ok code
force = true # ensure to always redirect

View File

@ -1,35 +0,0 @@
import type { Handler } from "@netlify/functions";
import { redisClient } from "../../redis";
const getViewsBySlug = async (slug: string): Promise<number> => {
let views = 1;
const data = await redisClient.get(`views:${slug}`);
if (data) views = Number.parseInt(data, 10);
if (Number.isNaN(views)) views = 1;
return views;
};
const handler: Handler = async (event) => {
const slug = event.queryStringParameters?.slug;
switch (event.httpMethod) {
case "GET": {
if (!slug) return { statusCode: 400, body: "Bad request." };
const views = await getViewsBySlug(slug);
return { statusCode: 200, body: views.toString() };
}
case "POST": {
if (!slug) return { statusCode: 400, body: "Bad request." };
const views = await redisClient.incr(`views:${slug}`);
return { statusCode: 200, body: views.toString() };
}
default: {
return {
statusCode: 500,
body: "Unrecognized HTTP Method, must be one of GET/POST",
};
}
}
};
export { handler };

27202
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,42 +5,49 @@
"description": "Personal website created with Astro.", "description": "Personal website created with Astro.",
"author": { "author": {
"name": "Sergey Yarkov", "name": "Sergey Yarkov",
"email": "serzh.yarkov@gmail.com", "email": "email@yarkov.tech",
"url": "https://yarkov.tech" "url": "https://yarkov.tech"
}, },
"license": "MIT", "license": "MIT",
"type": "module",
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
"start": "astro dev",
"build": "astro check && astro build", "build": "astro check && astro build",
"start": "node --env-file=.env ./dist/server/entry.mjs",
"preview": "astro preview", "preview": "astro preview",
"astro": "astro" "astro": "astro",
"telemetry-disable": "astro telemetry disable",
"codegen": "graphql-codegen --require dotenv/config --config codegen.ts --watch"
}, },
"dependencies": { "dependencies": {
"@astrojs/check": "^0.9.4", "@astrojs/check": "^0.9.5",
"@astrojs/rss": "^3.0.0", "@astrojs/node": "^9.5.0",
"@astrojs/sitemap": "^3.0.0", "@astrojs/rss": "^4.0.13",
"@astrojs/solid-js": "^3.0.1", "@astrojs/solid-js": "^5.1.3",
"@iconify-json/mdi": "^1.2.3", "@iconify-json/mdi": "^1.2.3",
"@netlify/functions": "^2.1.0", "@types/markdown-it": "^14.1.2",
"@sindresorhus/slugify": "^2.2.1", "astro": "^5.15.8",
"astro": "^3.2.0",
"astro-icon": "^1.1.5", "astro-icon": "^1.1.5",
"decap-cms-app": "^3.6.3", "graphql": "^16.12.0",
"hast-util-to-string": "^3.0.0", "hast-util-select": "^6.0.4",
"hastscript": "^8.0.0", "markdown-it": "^14.1.0",
"html-escaper": "^3.0.3", "markdown-it-anchor": "^9.2.0",
"ioredis": "^5.3.2", "markdown-it-prism": "^3.0.1",
"markdown-it-table-of-contents": "^1.1.0",
"mime-types": "^2.1.35", "mime-types": "^2.1.35",
"quicklink": "^3.0.1",
"rehype-autolink-headings": "^7.0.0",
"rehype-slug": "^6.0.0",
"sharp": "^0.32.6", "sharp": "^0.32.6",
"solid-js": "^1.7.12", "solid-js": "^1.7.12"
"typescript": "^5.8.3"
}, },
"devDependencies": { "devDependencies": {
"@0no-co/graphqlsp": "^1.15.1",
"@graphql-codegen/cli": "^6.1.0",
"@graphql-codegen/schema-ast": "^5.0.0",
"@parcel/watcher": "^2.5.1",
"@types/html-escaper": "^3.0.4",
"@types/mime-types": "^2.1.4", "@types/mime-types": "^2.1.4",
"sass": "^1.68.0" "@types/remark-prism": "^1.3.7",
"dotenv": "^17.2.3",
"sass": "^1.68.0",
"typescript": "^5.9.3"
} }
} }

View File

@ -1,249 +0,0 @@
editor:
preview: false
publish_mode: editorial_workflow
site_url: https://yarkov.tech
display_url: https://yarkov.tech
i18n:
locales: [ru, en]
default_locale: ru
structure: multiple_folders
locale: en
backend:
name: github
repo: sergeyyarkov/yarkov.tech
branch: main
media_folder: public/media
public_folder: "/media"
collections:
- name: articles
label: Публикации
label_singular: публикацию
description: Технические статьи, туториалы и различные справочные материалы которые связаны с IT тематикой
folder: src/content/blog
slug: "{{slug}}"
create: true
delete: true
i18n: true
fields:
- name: coverImage
widget: image
label: Постер
required: false
i18n: true
- name: draft
label: В черновике
widget: boolean
hint: Опубликована ли статья
i18n: true
required: false
- name: author
widget: relation
value_field: "{{firstName}} {{lastName}}"
search_fields:
- firstName
- lastName
collection: authors
label: Автор
i18n: true
- name: title
widget: string
label: Заголовок
i18n: true
- name: description
widget: string
label: Описание
i18n: true
- name: tags
widget: relation
label: Тэги
collection: tags
multiple: true
value_field: title
search_fields:
- title
i18n: true
- name: pubDate
widget: datetime
label: Дата публикации
picker_utc: true
i18n: true
- name: updatedDate
widget: datetime
label: Дата обновления
picker_utc: true
i18n: true
- name: body
widget: markdown
label: Содержание
i18n: true
- name: tags
label: Тэги
label_singular: тэг
description: Используются для тегирования контента
folder: src/content/tags
create: true
delete: true
fields:
- name: title
widget: string
label: Название
required: true
- name: authors
label: Авторы
label_singular: автора
description: На странице статьи отображается имя автора
slug: "{{firstName}}-{{lastName}}"
folder: src/content/authors
create: true
delete: true
i18n: true
fields:
- label: Имя
name: firstName
widget: string
required: true
i18n: true
- label: Фамилия
name: lastName
widget: string
required: true
i18n: true
- label: Об авторе
name: body
widget: markdown
required: false
i18n: true
- name: projects
label: Проекты
label_singular: проект
description: Основные проекты, которые были разработаны в выложены в открытый
доступ
folder: src/content/projects
create: true
delete: true
i18n: true
fields:
- name: icon
label: Иконка
widget: string
required: true
i18n: true
- name: title
label: Название
widget: string
required: true
i18n: true
- name: year
label: В каком году разработал
widget: number
required: true
i18n: true
- name: description
label: Описание
widget: string
required: true
i18n: true
- name: sourceUrl
label: Ссылка на исходник
widget: string
required: true
i18n: true
- name: demoUrl
label: Ссылка на демонстрацию
widget: string
required: false
i18n: true
- name: articleUrl
label: Ссылка на статью
widget: string
required: false
i18n: true
- name: skills
label: Навыки
label_singular: навык
folder: src/content/skills
create: true
delete: true
description: Навыки отображаются на странице Обо мне
i18n: true
fields:
- name: title
label: Категория
widget: string
required: true
i18n: true
- name: list
label: Записи
required: true
widget: list
allow_add: true
min: 1
i18n: true
fields:
- name: icon
label: Иконка
widget: string
i18n: true
required: false
- name: title
label: Название
widget: string
i18n: true
- name: settings
label: Настройки
files:
- name: siteconfig
label: Настройки веб-сайта
file: siteconfig.json
fields:
- label: Название сайта
name: name
widget: string
hint: Отображается в шапке.
default: yarkov.tech
- label: Автор веб-сайта
name: author
widget: string
hint: Отображается в подвале.
- label: Глобальный заголовок
name: globalTitle
widget: string
hint: Отображется во вкладке браузера.
- label: В разработке
name: isSiteUnderConstruction
widget: boolean
required: false
default: false
hint: Отображает баннер на всех старницах о том, что веб-сайт находится в
разработке
- label: Счетчик Яндекс.Метрики
name: ymCounter
widget: number
hint: "Например: 90034656"
required: false
- label: Статьи
name: articles
widget: object
fields:
- name: recentLimit
label: Лимит на отображение
widget: number
hint: Кол-во публикаций на гл.странице
default: 5
- label: Контакты
name: contacts
widget: object
required: false
fields:
- label: Telegram
name: telegram
widget: string
required: false
- label: Почта
name: email
widget: string
required: false
- label: Github
name: github
widget: string
required: false

View File

@ -1,4 +1,4 @@
User-agent: * User-agent: *
Allow: / Allow: /
Sitemap: https://yarkov.tech/sitemap-index.xml Sitemap: https://yarkov.tech/sitemap.xml

View File

@ -1,9 +0,0 @@
import Redis from "ioredis";
const redisClient = new Redis({
host: process.env.REDIS_HOST,
port: Number.parseInt(process.env.REDIS_PORT || "6379", 10),
password: process.env.REDIS_PASSWORD,
});
export { redisClient };

2451
schema.graphql Normal file

File diff suppressed because it is too large Load Diff

42
src/actions/index.ts Normal file
View File

@ -0,0 +1,42 @@
import { ActionError, defineAction } from "astro:actions";
import { z } from "astro/zod";
import { createAppeal, getLatestAppealsByClientIp } from "../queries";
const CREATE_APPEAL_LIMIT_HRS = 1;
const APPEALS_LAST_HRS_LIMIT = 2;
export const server = {
appeal: defineAction({
accept: "form",
input: z.object({
name: z.string(),
email: z.string().email(),
subject: z.string(),
message: z.string(),
}),
handler: async (data, ctx) => {
const currentDate = new Date();
const latestAppeals = await getLatestAppealsByClientIp(ctx.clientAddress, 5);
const appealsLastHrs = latestAppeals.filter((a) => {
const hourDiff = (currentDate.getTime() - new Date(a.date_created).getTime()) / (1000 * 60 * 60);
return hourDiff <= CREATE_APPEAL_LIMIT_HRS;
});
if (appealsLastHrs.length >= APPEALS_LAST_HRS_LIMIT) {
throw new ActionError({
code: "TOO_MANY_REQUESTS",
message: "Appeal create limit exceeded.",
});
}
const appeal = await createAppeal({ ...data, client_ip: ctx.clientAddress });
if (appeal) return true;
throw new ActionError({
code: "INTERNAL_SERVER_ERROR",
message: "Unable to create appeal.",
});
},
}),
};

View File

@ -0,0 +1,12 @@
---
import { getLanguageFromURL } from "@i18n/utils";
import { getAboutMe } from "../queries/global";
import { markdownToHTML } from "../utils/markdown";
import ArticleContent from "@components/ArticleContent.astro";
const pageLang = getLanguageFromURL(Astro.url.pathname);
const data = await getAboutMe(pageLang);
const aboutMeHtmlContent = await markdownToHTML(data?.about || "");
---
<ArticleContent set:html={aboutMeHtmlContent} />

View File

@ -1,21 +1,19 @@
--- ---
export interface Props { import { getSiteSettings } from '@/src/queries';
counter: number;
}
const { counter } = Astro.props as Props const siteSettings = await getSiteSettings();
--- ---
<script type="text/javascript" define:vars={{ counter }} is:inline> {siteSettings ? <script type="text/javascript" define:vars={{ counter: siteSettings.ym_counter }} is:inline>
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)}; (function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
var z = null;m[i].l=1*new Date(); var z = null;m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }} for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)}) k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym"); (window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
ym(counter, "init", { ym(counter, "init", {
clickmap:true, clickmap: true,
trackLinks:true, trackLinks: true,
accurateTrackBounce:true accurateTrackBounce: true,
}); });
</script> </script> : null}

View File

@ -11,7 +11,7 @@ const { url, id } = Astro.props as Props;
<div id="disqus_thread"></div> <div id="disqus_thread"></div>
</footer> </footer>
<script define:vars={{ url, id }}> <script is:inline define:vars={{ url, id }}>
var disqus_config = function () { var disqus_config = function () {
this.page.url = url; this.page.url = url;
this.page.identifier = id; this.page.identifier = id;
@ -25,9 +25,9 @@ const { url, id } = Astro.props as Props;
((function () { ((function () {
var e = document, var e = document,
t = e.createElement("script"); t = e.createElement("script");
(t.src = "https://yarkov-tech.disqus.com/embed.js"), ((t.src = "https://yarkov-tech.disqus.com/embed.js"),
t.setAttribute("data-timestamp", +new Date()), t.setAttribute("data-timestamp", +new Date()),
(e.head || e.body).appendChild(t); (e.head || e.body).appendChild(t));
})(), })(),
t.unobserve(e.target)); t.unobserve(e.target));
}); });
@ -39,9 +39,6 @@ const { url, id } = Astro.props as Props;
"onthemetoggled", "onthemetoggled",
() => () =>
document.readyState == "complete" && document.readyState == "complete" &&
setTimeout( setTimeout(() => "DISQUS" in window && DISQUS.reset({ reload: true, config: disqus_config }), 100)
() => "DISQUS" in window && DISQUS.reset({ reload: true, config: disqus_config }),
100
)
); );
</script> </script>

View File

@ -1,8 +1,23 @@
<div itemprop="articleBody" class="article-content"><slot /></div> ---
import { useTranslation } from "../i18n/utils";
<style lang="scss" is:global> const t = useTranslation(Astro);
const tocTitle = t("article.tableOfContents");
---
<div itemprop='articleBody' class='article-content'><slot /></div>
<script is:inline define:vars={{ tocTitle }}>
var $toc = document.querySelector(".toc");
if ($toc) {
$toc.querySelector("summary").textContent = tocTitle;
document.querySelector("body")?.addEventListener("click", () => $toc?.removeAttribute("open"));
}
</script>
<style lang='scss' is:global>
.article-content { .article-content {
margin-top: 70px; padding-top: 20px;
& h1, & h1,
h2, h2,
h3, h3,
@ -200,6 +215,44 @@
content: ","; content: ",";
} }
} }
.toc {
cursor: pointer;
display: block;
position: absolute;
& summary {
font-size: var(--font-size-lg);
}
& nav {
background-color: var(--background-card);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-lg);
margin-top: 10px;
padding: 10px 16px;
& ul {
margin: 6px 0;
padding-left: 0;
margin-left: 0;
& a {
display: block;
// margin-bottom: 10px;
color: var(--font-color);
text-decoration: none;
font-size: var(--font-size-md);
&:hover {
color: var(--color-blue-500);
}
}
& li {
list-style: none;
padding: 0;
}
& li ul {
margin-left: 15px;
}
}
}
}
} }
@media screen and (max-width: 800px) { @media screen and (max-width: 800px) {

View File

@ -1,21 +1,22 @@
import type { Component } from "solid-js"; import type { Component } from "solid-js";
import { createDateFormatter, createRelativeArticleUrl } from "@root/utils"; import { createRelativeArticleUrl, createDateFormatter } from "@root/utils";
import { URL_BLOG_PREFIX } from "@root/constants"; import { DEFAULT_LANGUAGE, URL_BLOG_PREFIX } from "@root/constants";
import "./ArticleItem.scss"; import "./ArticleItem.scss";
import { Article_Translations } from "@/src/graphql/graphql";
export type ArticleItemProps = { export type ArticleItemProps = {
id: string; title: Article_Translations["title"];
title: string; pub_date: Article_Translations["pub_date"];
pubDate: Date; slug: Article_Translations["slug"];
languages_code: Article_Translations["languages_code"];
pageLang: string; pageLang: string;
}; };
const ArticleItem: Component<ArticleItemProps> = (props) => { const ArticleItem: Component<ArticleItemProps> = (props) => {
const articleLang = props.id.split("/")[0]; const { title, pub_date: pubDate, slug, languages_code, pageLang } = props;
const href = createRelativeArticleUrl( const articleLang = languages_code?.code.split("-")[0] || DEFAULT_LANGUAGE;
{ id: props.id, title: props.title, pubDate: props.pubDate },
URL_BLOG_PREFIX const href = createRelativeArticleUrl({ pubDate, slug, articleLang }, URL_BLOG_PREFIX);
);
return ( return (
<article <article
@ -26,13 +27,11 @@ const ArticleItem: Component<ArticleItemProps> = (props) => {
> >
<a href={href}> <a href={href}>
<div class="flex"> <div class="flex">
<h3 itemprop="headline">{props.title}</h3> <h3 itemprop="headline">{title}</h3>
{articleLang !== props.pageLang && <sup>{articleLang.toLocaleUpperCase()}</sup>} {articleLang !== pageLang && <sup>{articleLang.toLocaleUpperCase()}</sup>}
</div> </div>
<p> <p>
<time datetime={props.pubDate.toISOString()}> <time datetime={pubDate}>{createDateFormatter(pageLang).format(new Date(pubDate))}</time>
{createDateFormatter(props.pageLang).format(props.pubDate)}
</time>
</p> </p>
</a> </a>
</article> </article>

View File

@ -1,5 +1,4 @@
--- ---
import type { CollectionEntry } from "astro:content";
import { Icon } from "astro-icon/components"; import { Icon } from "astro-icon/components";
import { createDateFormatter } from "@root/utils"; import { createDateFormatter } from "@root/utils";
import ArticleCover from "@components/ArticleCover.astro"; import ArticleCover from "@components/ArticleCover.astro";
@ -7,23 +6,28 @@ import ArticleComments from "@components/ArticleComments.astro";
import TranslatedLink from "@components/TranslatedLink.astro"; import TranslatedLink from "@components/TranslatedLink.astro";
import AvailableOnLangs from "@components/AvailableOnLangs.astro"; import AvailableOnLangs from "@components/AvailableOnLangs.astro";
import { useTranslation, getLanguageFromURL } from "@i18n/utils"; import { useTranslation, getLanguageFromURL } from "@i18n/utils";
import * as utils from "@utils/index"; import { DIRECTUS_URL } from "astro:env/server";
import { Article_TranslationsQuery } from "../graphql/graphql";
export type Props = { export type Props = {
article: CollectionEntry<"blog">; article: Article_TranslationsQuery["article_translations"][0];
availableOnLangs?: CollectionEntry<"blog">[];
lang: string; lang: string;
}; };
const { article, availableOnLangs } = Astro.props as Props; const { article } = Astro.props as Props;
const pageLang = getLanguageFromURL(Astro.url.pathname); const pageLang = getLanguageFromURL(Astro.url.pathname);
const canonicalUrl = new URL(Astro.url.pathname, Astro.site); const canonicalUrl = new URL(Astro.url.pathname, Astro.site);
const { const {
id, cover_image,
data: { coverImage, title, author, description, tags, pubDate }, title,
author,
description,
tags,
pub_date,
slug,
views,
article_id: articleTranslations,
} = article; } = article;
const slug = utils.encodeString(id.split("/")[1]);
const t = useTranslation(Astro); const t = useTranslation(Astro);
--- ---
@ -36,8 +40,8 @@ const t = useTranslation(Astro);
<ul itemprop="keywords"> <ul itemprop="keywords">
{tags.map((tag) => ( {tags.map((tag) => (
<li> <li>
<TranslatedLink rel="tag" to={`/articles/?tags=${tag}`}> <TranslatedLink rel="tag" to={`/articles/?tags=${tag?.tag_id?.title}`}>
{tag} {tag?.tag_id?.title}
</TranslatedLink> </TranslatedLink>
</li> </li>
))} ))}
@ -47,42 +51,34 @@ const t = useTranslation(Astro);
} }
<div class="views"> <div class="views">
<Icon name="eye" width="21" height="21" /> <Icon name="eye" width="21" height="21" />
<span><span id="views">---</span>&nbsp;{t("article.views")}</span> <span><span>{views}</span>&nbsp;{t("article.views")}</span>
</div> </div>
</div> </div>
<div class="heading"> <div class="heading">
<h1 itemprop="headline">{title}</h1> <h1 itemprop="headline">{title}</h1>
</div> </div>
<div class="published"> <div class="published">
<time itemprop="datePublished" datetime={pubDate.toString()}> <time itemprop="datePublished" datetime={pub_date}>
{createDateFormatter(pageLang).format(pubDate)}&nbsp; {createDateFormatter(pageLang).format(new Date(pub_date))}&nbsp;
</time> </time>
<span itemprop="author">{author}</span> <span itemprop="author">{`${author?.first_name} ${author?.last_name}`}</span>
</div> </div>
{coverImage && <ArticleCover src={coverImage} alt={title} />} {
cover_image && (
<ArticleCover src={`${DIRECTUS_URL}/assets/${cover_image.filename_disk}`} alt={title} />
)
}
<div class="description"> <div class="description">
<p> <p>
{description} {description}
</p> </p>
</div> </div>
{availableOnLangs && <AvailableOnLangs articles={availableOnLangs} />} <AvailableOnLangs articles={articleTranslations} />
</header> </header>
<slot /> <slot />
<ArticleComments url={canonicalUrl.href} id={slug} /> <ArticleComments url={canonicalUrl.href} id={slug} />
</article> </article>
<script define:vars={{ slug, IS_DEV: import.meta.env.DEV }}>
(async () => {
if (IS_DEV) return;
const response = await fetch(`/api/views?slug=${slug}`, { method: "POST" });
if (!response.ok) return;
const views = await response.text();
const $views = document.getElementById("views");
if (isNaN(parseInt(views, 10))) return;
if ($views) $views.textContent = views;
})();
</script>
<style lang="scss" is:global> <style lang="scss" is:global>
.article { .article {
& header { & header {

View File

@ -0,0 +1,14 @@
---
import { useTranslation, getLanguageFromURL } from "@i18n/utils";
import * as utils from "@utils/article";
import List from "@components/ArticlesList";
import { getArticleList } from "@root/queries/article";
const t = useTranslation(Astro);
const pageLang = getLanguageFromURL(Astro.url.pathname);
const data = await getArticleList();
const articles = utils.filterArticlesByPageLang(data, pageLang);
const articleBlocks = utils.formatToArticleBlocks(articles);
---
<List client:load i18n={{ "articles.empty": t("articles.empty") }} articles={articleBlocks} pageLang={pageLang} />

View File

@ -1,25 +1,16 @@
import type { Component } from "solid-js"; import type { Component } from "solid-js";
import { createSignal, createEffect, For } from "solid-js"; import { createSignal, createEffect, For } from "solid-js";
import { search, selectedTags, setCount } from "@stores/searchStore"; import { search, selectedTags, setCount } from "@stores/searchStore";
import SearchInput from "./SearchInput";
import TagsList from "./TagsList";
import ArticleItem from "@components/ArticleItem"; import ArticleItem from "@components/ArticleItem";
import { ArticleQuery } from "@/src/graphql/graphql";
import "./ArticlesList.scss"; import "./ArticlesList.scss";
type UiStringsType = { "articles.empty": string; "input.search": string }; type UiStringsType = { "articles.empty": string };
type Articles = ArticleQuery["article"][0]["translations"];
type ArticlesType = Array<{ type ArticlesBlockType = Record<string, Articles>;
id: string;
title: string;
tags: string[];
pubDate: Date;
}>;
type ArticlesBlockType = Record<string, ArticlesType>;
type ArticlesListProps = { type ArticlesListProps = {
articles: ArticlesBlockType; articles: ArticlesBlockType;
tags: string[];
pageLang: string; pageLang: string;
i18n: UiStringsType; i18n: UiStringsType;
}; };
@ -29,7 +20,7 @@ const ArticlesList: Component<ArticlesListProps> = (props) => {
const isEmpty = () => Object.keys(articles()).length === 0; const isEmpty = () => Object.keys(articles()).length === 0;
const sortYears = (articles: ArticlesBlockType) => { const sortByYears = (articles: ArticlesBlockType) => {
return Object.keys(articles) return Object.keys(articles)
.map(Number) .map(Number)
.sort((a, b) => b - a); .sort((a, b) => b - a);
@ -40,20 +31,24 @@ const ArticlesList: Component<ArticlesListProps> = (props) => {
}: { }: {
params: { search: string; tags?: string[] }; params: { search: string; tags?: string[] };
}): ArticlesBlockType => { }): ArticlesBlockType => {
const filter = (cb: (articles: ArticlesType) => ArticlesType) => { const filter = (cb: (articles: Articles) => Articles) => {
return Object.fromEntries( return Object.fromEntries(
Object.keys(props.articles) Object.keys(props.articles)
.map((year) => [year, cb(props.articles[year])]) .map((year) => [year, cb(props.articles[year])])
.filter((articles) => articles[1].length !== 0) .filter((articles) => articles[1]?.length !== 0)
); );
}; };
return filter((articles) => { return filter((articles) => {
let filtered = articles.filter((a) => let filtered = articles?.filter(
a.title.toLocaleLowerCase().includes(search.trim().toLocaleLowerCase()) (a) => a && a.title.toLocaleLowerCase().includes(search.trim().toLocaleLowerCase())
); );
if (tags && tags.length > 0) { if (tags && tags.length > 0) {
filtered = filtered.filter((a) => tags.some((t) => a.tags.includes(t))); filtered = filtered?.filter((a) =>
tags.some((t) => {
return a?.tags?.map((t) => t?.tag_id?.title).includes(t);
})
);
} }
return filtered; return filtered;
}); });
@ -67,23 +62,24 @@ const ArticlesList: Component<ArticlesListProps> = (props) => {
return ( return (
<> <>
<SearchInput i18n={{ "input.search": props.i18n["input.search"] }} />
<TagsList tags={props.tags} />
<div itemscope itemtype="http://schema.org/Blog" class="articles-list"> <div itemscope itemtype="http://schema.org/Blog" class="articles-list">
{!isEmpty() ? ( {!isEmpty() ? (
<For each={sortYears(articles())}> <For each={sortByYears(articles())}>
{(year) => ( {(year) => (
<div class="articles-list__wrapper"> <div class="articles-list__wrapper">
<h2>{year}</h2> <h2>{year}</h2>
<For each={articles()[year]}> <For each={articles()[year]}>
{({ id, title, pubDate }) => ( {(a) =>
<ArticleItem a && (
id={id} <ArticleItem
title={title} slug={a.slug}
pubDate={pubDate} title={a.title}
pageLang={props.pageLang} languages_code={a.languages_code}
/> pub_date={a.pub_date}
)} pageLang={props.pageLang}
/>
)
}
</For> </For>
</div> </div>
)} )}

View File

@ -62,6 +62,7 @@ const SearchInput: Component<SearchInputProps> = (props) => {
onInput={onSearch} onInput={onSearch}
value={search()} value={search()}
type="search" type="search"
name="search"
placeholder={props.i18n["input.search"] || "Search article by title..."} placeholder={props.i18n["input.search"] || "Search article by title..."}
autocapitalize="off" autocapitalize="off"
autocomplete="off" autocomplete="off"

View File

@ -1,12 +1,10 @@
--- ---
import type { CollectionEntry } from "astro:content";
import { getLanguageFromURL, useTranslation } from "@root/i18n/utils"; import { getLanguageFromURL, useTranslation } from "@root/i18n/utils";
import { URL_BLOG_PREFIX } from "@root/constants";
import { createRelativeArticleUrl } from "@root/utils"; import { createRelativeArticleUrl } from "@root/utils";
import languages from "@i18n/languages"; import { Article_TranslationsQuery } from "../graphql/graphql";
export interface Props { export interface Props {
articles: CollectionEntry<"blog">[]; articles: Article_TranslationsQuery["article_translations"][0]["article_id"];
} }
const pageLang = getLanguageFromURL(Astro.url.pathname); const pageLang = getLanguageFromURL(Astro.url.pathname);
@ -15,23 +13,26 @@ const { articles } = Astro.props;
--- ---
{ {
articles.length !== 1 && ( articles && articles.translations && articles.translations?.length !== 1 && (
<div> <div>
{t("article.availableOn")}:{" "} {t("article.availableOn")}:{" "}
{articles {articles.translations
.filter((a) => !a.id.startsWith(`${pageLang}/`)) .filter((a) => a && a.languages_code?.code.split("-")[0] !== pageLang)
.map(({ id, data: { title, pubDate } }) => ( .map(
<a href={`${createRelativeArticleUrl({ id, title, pubDate }, URL_BLOG_PREFIX)}`}>{`${ (a) =>
languages[id.split("/")[0] as LanguageKeys] a && (
}`}</a> <a
))} href={`${createRelativeArticleUrl({ slug: a.slug, articleLang: `${a.languages_code?.code.split("-")[0]}`, pubDate: a.pub_date })}`}
>{`${a.languages_code?.name}`}</a>
)
)}
</div> </div>
) )
} }
<style lang="scss"> <style lang="scss">
div { div {
margin: 25px 0; margin: 10px 0;
font-size: var(--font-size-lg); font-size: var(--font-size-lg);
& a { & a {
&:not(:last-child)::after { &:not(:last-child)::after {

View File

@ -1,6 +1,5 @@
--- ---
import "@styles/global.scss"; import "@styles/global.scss";
import siteconfig from "siteconfig.json";
import { SITE_METADATA } from "@root/constants"; import { SITE_METADATA } from "@root/constants";
import YaMetrika from "./Analytics/YaMetrika.astro"; import YaMetrika from "./Analytics/YaMetrika.astro";
import { useTranslation } from "@root/i18n/utils"; import { useTranslation } from "@root/i18n/utils";
@ -19,64 +18,52 @@ const t = useTranslation(Astro);
<head> <head>
<!-- Fonts --> <!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel='preconnect' href='https://fonts.googleapis.com' />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel='preconnect' href='https://fonts.gstatic.com' crossorigin />
<link <link
href="https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,400;0,500;0,600;0,700;1,400&family=IBM+Plex+Mono&display=swap" href='https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,400;0,500;0,600;0,700;1,400&family=IBM+Plex+Mono&display=swap'
rel="preload" rel='preload'
as="style" as='style'
onload="this.rel='stylesheet'" onload={`this.rel='stylesheet'`}
/> />
<!-- Canonical URL --> <!-- Canonical URL -->
<link rel="canonical" href={canonicalURL.href} /> <link rel='canonical' href={canonicalURL.href} />
<!-- Base metadata --> <!-- Base metadata -->
<meta charset="utf-8" /> <meta charset='utf-8' />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name='viewport' content='width=device-width, initial-scale=1' />
<title>{title || SITE_METADATA.title}</title> <title>{title || SITE_METADATA.title}</title>
<!-- Metadata --> <!-- Metadata -->
<meta name="title" content={title || SITE_METADATA.title} /> <meta name='title' content={title || SITE_METADATA.title} />
<meta name="generator" content={Astro.generator} /> <meta name='generator' content={Astro.generator} />
<meta name="description" content={description || t("site.description")} /> <meta name='description' content={description || t("site.description")} />
<meta name="theme-color" content={SITE_METADATA.themeColor} /> <meta name='theme-color' content={SITE_METADATA.themeColor} />
<meta name="msapplication-TileColor" content="#da532c" /> <meta name='msapplication-TileColor' content='#da532c' />
<!-- Favicons --> <!-- Favicons -->
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" /> <link rel='apple-touch-icon' sizes='180x180' href='/apple-touch-icon.png' />
<link <link
rel="icon" rel='icon'
type="image/png" type='image/png'
sizes="32x32" sizes='32x32'
href="/favicon-light-32x32.png" href='/favicon-light-32x32.png'
media="(prefers-color-scheme: light)" media='(prefers-color-scheme: light)'
/> />
<link <link
rel="icon" rel='icon'
type="image/png" type='image/png'
sizes="16x16" sizes='16x16'
href="/favicon-light-16x16.png" href='/favicon-light-16x16.png'
media="(prefers-color-scheme: light)" media='(prefers-color-scheme: light)'
/> />
<link <link rel='icon' type='image/png' sizes='32x32' href='/favicon-dark-32x32.png' media='(prefers-color-scheme: dark)' />
rel="icon" <link rel='icon' type='image/png' sizes='16x16' href='/favicon-dark-16x16.png' media='(prefers-color-scheme: dark)' />
type="image/png" <link rel='manifest' href='/site.webmanifest' />
sizes="32x32" <link rel='mask-icon' href='/safari-pinned-tab.svg' color='#5bbad5' />
href="/favicon-dark-32x32.png" <link rel='sitemap' href='/sitemap-index.xml' />
media="(prefers-color-scheme: dark)"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon-dark-16x16.png"
media="(prefers-color-scheme: dark)"
/>
<link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
<slot /> <slot />
<script is:inline src="/assets/js/detect-theme.js"></script> <script is:inline src='/assets/js/detect-theme.js'></script>
<script src="../quicklink.js"></script> {!isDev && <YaMetrika />}
{!isDev && siteconfig?.ymCounter && <YaMetrika counter={siteconfig.ymCounter} />}
</head> </head>

View File

@ -2,57 +2,45 @@
import Button from "@components/Button.astro"; import Button from "@components/Button.astro";
import UIString from "./UIString.astro"; import UIString from "./UIString.astro";
import { useTranslation } from "@i18n/utils"; import { useTranslation } from "@i18n/utils";
import { actions } from "astro:actions";
const t = useTranslation(Astro); const t = useTranslation(Astro);
const formResult = Astro.getActionResult(actions.appeal);
--- ---
<form name="contact" method="post" netlify-honeypot="bot-field" data-netlify="true"> <>
<div hidden> <form name="contact" action={actions.appeal} method="post">
Don't fill this out if you're human <div class="flex">
<input name="bot-field" /> <div>
</div> <input name="name" placeholder={t("input.nameField")} type="text" maxlength="20" autocomplete="off" required />
<div class="flex"> </div>
<div> <div>
<input <input name="email" placeholder="example@email.com" type="email" autocomplete="off" required />
name="name" </div>
placeholder={t("input.nameField")}
type="text"
maxlength="20"
autocomplete="off"
required
/>
</div> </div>
<div> <input name="subject" placeholder={t("input.subjectField")} type="text" autocomplete="off" required />
<input <textarea
name="email" name="message"
placeholder="example@email.com" placeholder={t("input.messageField")}
autocomplete="off" id="message"
type="email" autocomplete="off"
required cols={30}
/> rows={10}
required></textarea>
<div class="footer">
{
formResult?.error && (
<span>
<UIString key={formResult.error.status === 429 ? "submitted.limit" : "submitted.error"} />
</span>
)
}
<Button type={"submit"}>
<UIString key={"btn.submit"} />
</Button>
</div> </div>
</div> </form>
<input </>
name="subject"
placeholder={t("input.subjectField")}
type="text"
autocomplete="off"
required
/>
<textarea
name="message"
placeholder={t("input.messageField")}
id="message"
autocomplete="off"
cols={30}
rows={10}
required></textarea>
<div class="footer">
<Button type={"submit"}>
<UIString key={"btn.submit"} />
</Button>
</div>
</form>
<style lang="scss"> <style lang="scss">
form { form {
@ -71,7 +59,10 @@ const t = useTranslation(Astro);
border: 1px solid var(--color-error); border: 1px solid var(--color-error);
} }
& .footer { & .footer {
text-align: right; display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
} }
} }
@media screen and (max-width: 560px) { @media screen and (max-width: 560px) {

View File

@ -1,23 +1,32 @@
--- ---
import { Icon } from "astro-icon/components"; import { Icon } from "astro-icon/components";
import { getContacts } from "../queries";
const contacts = await getContacts();
--- ---
<div> <div>
<a href={"mailto:" + "serzh.yarkov@gmail.com"} {
>Email contacts && (
<Icon name="arrow-up-right" width="15" height="15" data-filter="none" /> <>
</a> <a href={"mailto:" + contacts.email}>
<a href={"https://github.com/sergeyyarkov"} target="_blank" Email
>Github <Icon name='arrow-up-right' width='15' height='15' data-filter='none' />
<Icon name="arrow-up-right" width="15" height="15" data-filter="none" /> </a>
</a> <a href={contacts.github} target='_blank'>
<a href={"https://t.me/sergeyyarkov"} target="_blank" Github
>Telegram <Icon name='arrow-up-right' width='15' height='15' data-filter='none' />
<Icon name="arrow-up-right" width="15" height="15" data-filter="none" /> </a>
</a> <a href={contacts.telegram} target='_blank'>
Telegram
<Icon name='arrow-up-right' width='15' height='15' data-filter='none' />
</a>
</>
)
}
</div> </div>
<style lang="scss"> <style lang='scss'>
div { div {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;

View File

@ -1,22 +1,22 @@
--- ---
import { SITE_METADATA } from "@root/constants";
import { Icon } from "astro-icon/components"; import { Icon } from "astro-icon/components";
import TranslatedLink from "./TranslatedLink.astro"; import TranslatedLink from "./TranslatedLink.astro";
import UIString from "./UIString.astro"; import UIString from "./UIString.astro";
import { getLanguageFromURL } from "../i18n/utils";
const pageLang = getLanguageFromURL(Astro.url.pathname);
--- ---
<footer> <footer>
<div class="container"> <div class="container">
<div> <div>
<i> <i>
<UIString key={"footer.developed"} />: <TranslatedLink to="/contacts/"> <UIString key={"footer.developed"} />: <TranslatedLink to="/contacts/"> Sergey Yarkov </TranslatedLink>
{SITE_METADATA.author}
</TranslatedLink>
&copy; {new Date().getFullYear()} &copy; {new Date().getFullYear()}
</i> </i>
</div> </div>
<div> <div>
<a href="/rss.xml" target="_blank"><UIString key={"footer.rssFeed"} /></a> <a href=`/${pageLang}/rss.xml` target="_blank"><UIString key={"footer.rssFeed"} /></a>
<span> <span>
<Icon name="rss" width="20" height="20" /> <Icon name="rss" width="20" height="20" />
</span> </span>

View File

@ -1,11 +1,11 @@
--- ---
import { Icon } from "astro-icon/components";
import { IS_SITE_UNDER_CONSTRUCTION, SITE_METADATA } from "@root/constants"; import { IS_SITE_UNDER_CONSTRUCTION, SITE_METADATA } from "@root/constants";
import ThemeSwitcher from "@components/ThemeSwticher"; import ThemeSwitcher from "@components/ThemeSwticher";
import LanguageSelect from "@components/LanguageSelect.astro"; import LanguageSelect from "@components/LanguageSelect.astro";
import NavToggle from "@components/NavToggle"; import NavToggle from "@components/NavToggle";
import UIString from "@components/UIString.astro"; import UIString from "@components/UIString.astro";
import TranslatedLink from "@components/TranslatedLink.astro"; import TranslatedLink from "@components/TranslatedLink.astro";
import HeaderContacts from "./HeaderContacts.astro";
--- ---
<header {...IS_SITE_UNDER_CONSTRUCTION ? { style: "top: 45px;" } : {}}> <header {...IS_SITE_UNDER_CONSTRUCTION ? { style: "top: 45px;" } : {}}>
@ -31,24 +31,7 @@ import TranslatedLink from "@components/TranslatedLink.astro";
)) ))
} }
</ul> </ul>
{ <HeaderContacts />
SITE_METADATA.contacts && (
<div class="contacts">
<a target="_blank" href={"mailto:" + SITE_METADATA.contacts.email}>
Email
<Icon name="email" width="18" height="18" />
</a>
<a target="_blank" href={SITE_METADATA.contacts.github}>
Github
<Icon name="github_filled" width="18" height="18" />
</a>
<a target="_blank" href={SITE_METADATA.contacts.telegram}>
Telegram
<Icon name="telegram" width="18" height="18" />
</a>
</div>
)
}
<div class="flex settings"> <div class="flex settings">
<LanguageSelect /> <LanguageSelect />
<ThemeSwitcher client:visible /> <ThemeSwitcher client:visible />
@ -104,19 +87,6 @@ import TranslatedLink from "@components/TranslatedLink.astro";
text-transform: lowercase; text-transform: lowercase;
} }
} }
& .contacts {
display: flex;
align-items: center;
column-gap: 10px;
margin-right: 30px;
& a {
text-decoration: none;
font-size: 0;
&:hover {
opacity: var(--opacity-hover);
}
}
}
& .settings { & .settings {
column-gap: 13.5px; column-gap: 13.5px;
} }
@ -162,17 +132,6 @@ import TranslatedLink from "@components/TranslatedLink.astro";
} }
} }
} }
& .contacts {
margin-right: 0;
margin-top: 2.5rem;
& a {
margin-right: 0.2rem;
}
& svg {
width: 21px;
height: 21px;
}
}
} }
} }
} }

View File

@ -0,0 +1,54 @@
---
import { Icon } from "astro-icon/components";
import { getContacts } from "../queries";
const contacts = await getContacts();
---
{
contacts && (
<div class="contacts">
<a target="_blank" href={"mailto:" + contacts.email}>
Email
<Icon name="email" width="18" height="18" />
</a>
<a target="_blank" href={contacts.github}>
Github
<Icon name="github_filled" width="18" height="18" />
</a>
<a target="_blank" href={contacts.telegram}>
Telegram
<Icon name="telegram" width="18" height="18" />
</a>
</div>
)
}
<style lang="scss">
.contacts {
display: flex;
align-items: center;
column-gap: 10px;
margin-right: 30px;
& a {
text-decoration: none;
font-size: 0;
&:hover {
opacity: var(--opacity-hover);
}
}
}
@media screen and (max-width: 768px) {
.contacts {
margin-right: 0;
margin-top: 2.5rem;
& a {
margin-right: 0.2rem;
}
& svg {
width: 21px;
height: 21px;
}
}
}
</style>

View File

@ -19,7 +19,7 @@ const lang = getLanguageFromURL(Astro.url.pathname);
</select> </select>
</div> </div>
<script define:vars={{ lang, DEFAULT_LANGUAGE, URL_BLOG_PREFIX }}> <script is:inline define:vars={{ lang, DEFAULT_LANGUAGE, URL_BLOG_PREFIX }}>
const $select = document.getElementById("languageSelect"); const $select = document.getElementById("languageSelect");
const changeLanguageHandler = (e) => { const changeLanguageHandler = (e) => {
@ -37,8 +37,7 @@ const lang = getLanguageFromURL(Astro.url.pathname);
/* Move user to articles page with new language */ /* Move user to articles page with new language */
if (window.location.pathname.includes(`/${URL_BLOG_PREFIX}/`)) { if (window.location.pathname.includes(`/${URL_BLOG_PREFIX}/`)) {
window.location.pathname = window.location.pathname = `${newLang === DEFAULT_LANGUAGE ? "" : newLang + "/"}` + "articles/";
`${newLang === DEFAULT_LANGUAGE ? "" : newLang + "/"}` + "articles/";
return; return;
} }

View File

@ -1,56 +1,56 @@
--- ---
import { Icon } from "astro-icon/components"; import { Icon } from "astro-icon/components";
import type { CollectionEntry } from "astro:content";
import UIString from "@components/UIString.astro"; import UIString from "@components/UIString.astro";
import { Project_TranslationsQuery } from "../graphql/graphql";
import { DIRECTUS_URL } from "astro:env/server";
export interface Props { export interface Props {
data: CollectionEntry<"projects">["data"]; data: Project_TranslationsQuery["project_translations"][0];
} }
const { data } = Astro.props as Props; const { data } = Astro.props as Props;
// const lang = getLanguageFromURL(Astro.url.pathname); const { icon, title, year, description, article_url, source_url, demo_url } = data;
const { icon, title, year, description, articleUrl, sourceUrl, demoUrl } = data;
--- ---
<div class='project-item'> <div class="project-item">
<div class='project-item__heading'> <div class="project-item__heading">
<h2> <h2>
<Icon name={icon} width='20' height='20' /> {icon && <img data-icon src={`${DIRECTUS_URL}/assets/${icon.filename_disk}`} />}
{title} {title}
</h2> </h2>
<span>{year}</span> <span>{year}</span>
</div> </div>
<div class='project-item__description'> <div class="project-item__description">
<p>{description}</p> <p>{description}</p>
</div> </div>
<div class='project-item__links'> <div class="project-item__links">
{ {
articleUrl && ( article_url && (
<a class='btn-link' href={articleUrl} target='_blank'> <a class="btn-link" href={article_url} target="_blank">
<UIString key={"btn.article"} /> <UIString key={"btn.article"} />
</a> </a>
) )
} }
{ {
sourceUrl && ( source_url && (
<a class='btn-link' href={sourceUrl} target='_blank'> <a class="btn-link" href={source_url} target="_blank">
<UIString key={"btn.sourceCode"} /> <UIString key={"btn.sourceCode"} />
<Icon name='mdi:link-variant' width='15' height='15' /> <Icon name="mdi:link-variant" width="15" height="15" />
</a> </a>
) )
} }
{ {
demoUrl && ( demo_url && (
<a class='btn-link' href={demoUrl} target='_blank'> <a class="btn-link" href={demo_url} target="_blank">
<UIString key={"btn.demo"} /> <UIString key={"btn.demo"} />
<Icon name='mdi:link-variant' width='15' height='15' /> <Icon name="mdi:link-variant" width="15" height="15" />
</a> </a>
) )
} }
</div> </div>
</div> </div>
<style lang='scss'> <style lang="scss">
.project-item { .project-item {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -1,8 +1,14 @@
--- ---
import ProjectItem from "@components/ProjectItem.astro";
import { getLanguageFromURL } from "../i18n/utils";
import { getProjectList } from "../queries";
const pageLang = getLanguageFromURL(Astro.url.pathname);
const projects = await getProjectList(pageLang);
--- ---
<div> <div>
<slot /> {projects.map((data) => <ProjectItem data={data} />)}
</div> </div>
<style> <style>

View File

@ -1,21 +1,16 @@
--- ---
import type { CollectionEntry } from "astro:content";
import siteconfig from "siteconfig.json";
import { getLanguageFromURL } from "@i18n/utils"; import { getLanguageFromURL } from "@i18n/utils";
import * as utils from "@utils/index"; import * as utils from "@utils/index";
import ArticleItem from "@components/ArticleItem"; import ArticleItem from "@components/ArticleItem";
import EmptyList from "@components/EmptyList.astro"; import EmptyList from "@components/EmptyList.astro";
import UIString from "@components/UIString.astro"; import UIString from "@components/UIString.astro";
import TranslatedLink from "@components/TranslatedLink.astro"; import TranslatedLink from "@components/TranslatedLink.astro";
import { getRecentArticleList, getSiteSettings } from "../queries";
export interface Props {
articles: CollectionEntry<"blog">[];
length?: number | undefined;
}
const { articles, length = siteconfig.articles.recentLimit } = Astro.props as Props;
const pageLang = getLanguageFromURL(Astro.url.pathname); const pageLang = getLanguageFromURL(Astro.url.pathname);
const filtered = utils.removeDuplicates(articles.slice(0, length), pageLang); const siteSettings = await getSiteSettings();
const articles = await getRecentArticleList(siteSettings?.recent_articles_limit || 5);
const filteredArticles = utils.filterArticlesByPageLang(articles, pageLang);
const isEmpty = articles.length === 0; const isEmpty = articles.length === 0;
--- ---
@ -30,15 +25,19 @@ const isEmpty = articles.length === 0;
</div> </div>
<div itemscope itemtype="http://schema.org/Blog"> <div itemscope itemtype="http://schema.org/Blog">
{ {
!isEmpty ? ( !isEmpty && !!filteredArticles ? (
filtered.map((article) => ( filteredArticles.map(
<ArticleItem (a) =>
id={article.id} a && (
title={article.data.title} <ArticleItem
pubDate={article.data.pubDate} slug={a.slug}
pageLang={pageLang} title={a.title}
/> languages_code={a.languages_code}
)) pub_date={a.pub_date}
pageLang={pageLang}
/>
)
)
) : ( ) : (
<EmptyList /> <EmptyList />
) )

View File

@ -1,69 +0,0 @@
---
import { Icon } from "astro-icon/components";
import type { CollectionEntry } from "astro:content";
export interface Props {
data: CollectionEntry<"skills">["data"];
}
const {
data: { title, list },
} = Astro.props as Props;
---
<div class="skills-block">
<div>
<h3>{title}</h3>
</div>
<div>
<ul>
{
list.map((entry) => (
<li>
{entry.icon && <Icon name={entry.icon} width="20" height="20" />}
{entry.title}
</li>
))
}
</ul>
</div>
</div>
<style lang="scss">
.skills-block {
& h3 {
font-size: var(--font-size-md);
color: var(--font-color-grayer);
font-weight: 500;
}
& ul {
margin: 0;
padding: 0;
list-style: none;
& li {
font-weight: 400;
margin-bottom: 10px;
font-size: var(--font-size-sm);
display: flex;
align-items: center;
column-gap: 5px;
}
}
}
@media screen and (max-width: 500px) {
.skills-block {
& h3 {
text-align: center;
}
& ul {
display: flex;
flex-direction: column;
align-items: center;
}
}
}
</style>

View File

@ -1,20 +0,0 @@
---
---
<div class="skills-list"><slot /></div>
<style lang="scss">
.skills-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
column-gap: 40px;
row-gap: 30px;
margin-top: 5rem;
}
@media screen and (max-width: 500px) {
.skills-list {
display: flex;
flex-direction: column;
}
}
</style>

View File

@ -2,7 +2,7 @@
<slot /> <slot />
</header> </header>
<style lang="scss"> <style lang='scss'>
header { header {
& :global(p) { & :global(p) {
font-size: var(--font-size-lg); font-size: var(--font-size-lg);
@ -14,7 +14,7 @@
& :global(h1) { & :global(h1) {
font-size: var(--font-size-5xl); font-size: var(--font-size-5xl);
font-weight: bold; font-weight: bold;
margin-bottom: 0.5rem; margin-bottom: 16px;
} }
& :global(h2) { & :global(h2) {

View File

@ -1,63 +0,0 @@
---
import { buildToc } from "@root/utils";
import type { MarkdownHeading } from "astro";
import TableOfContentsHeading from "./TableOfContentsHeading.astro";
import { useTranslation } from "@root/i18n/utils";
export interface Props {
headings: MarkdownHeading[];
}
const toc = buildToc(Astro.props.headings, 3);
const t = useTranslation(Astro);
---
<details class="toc">
<summary>{t("article.tableOfContents")}</summary>
<nav>
<div>
<ul>
{toc.map((heading) => <TableOfContentsHeading heading={heading} />)}
</ul>
</div>
</nav>
</details>
<script>
var $toc = document.querySelector(".toc");
if ($toc) {
$toc.addEventListener(
"click",
(e) => (e.target as HTMLElement).tagName === "A" && $toc!.removeAttribute("open")
);
}
</script>
<style lang="scss" is:global>
.toc {
cursor: pointer;
display: block;
position: absolute;
& summary {
font-size: var(--font-size-lg);
}
& nav {
background-color: var(--background-card);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-lg);
margin-top: 10px;
padding: 10px 16px;
& ul {
& a {
display: block;
margin-bottom: 10px;
color: var(--font-color);
text-decoration: none;
}
& li ul {
margin-left: 15px;
}
}
}
}
</style>

View File

@ -1,16 +0,0 @@
---
import type { MarkdownHeadingToc } from "@root/utils";
export interface Props {
heading: MarkdownHeadingToc;
}
const { heading } = Astro.props;
---
<li>
<a href={`#${heading.slug}`}>{heading.text}</a>
<ul>
{heading.subheadings?.map((subheading) => <Astro.self heading={subheading} />)}
</ul>
</li>

View File

@ -0,0 +1,8 @@
---
import { getTagList } from "@root/queries/tag";
import List from "@components/ArticlesList/TagsList";
const tags = (await getTagList()).map((t) => t.title);
---
<List client:load tags={tags} />

View File

@ -13,4 +13,4 @@ if (!SUPPORTED_LANGUAGES.includes(pageLang)) pageLang = DEFAULT_LANGUAGE;
const href = pageLang === DEFAULT_LANGUAGE ? to : `/${pageLang}${to}`; const href = pageLang === DEFAULT_LANGUAGE ? to : `/${pageLang}${to}`;
--- ---
<a href={href} {...attrs}><slot /></a> <a data-astro-prefetch href={href} {...attrs}><slot /></a>

View File

@ -1,24 +0,0 @@
---
firstName: Sergey
lastName: Yarkov
---
My name is Sergey and I am a web developer. My main activity is creating web sites. I am interested in areas such as programming, embedded systems, and electronics.
I started making web sites in 2017, starting with simple HTML pages, and in 2019 I started learning programming. At present, I have experience in **full-stack** web development of client-server applications that solve a wide range of tasks.
Some of my personal [projects](/projects/) I put in public access on [Github](https://github.com/sergeyyarkov).
## Purpose of this site
I originally created this website for myself, to consolidate my knowledge by writing articles.
Since 2022, I have been posting materials on this website, and now at any time I can return to any topic and learn it again.
My articles can also help someone with mastering a particular topic. The main direction of this site is IT,
and [here](/articles/) I publish materials on topics that interest me, as well as talk about my [projects](/projects/).
I am absolutely calm about any criticism and I do not claim to be the best programmer in the world,
so do not judge the materials of this resource harshly.
## Skills
This list shows the skills and technologies that I have used/use in my projects.
For creating web projects, I use **Node.js** and the **Adonis.js** framework for the server-side.
For the client-side, I develop using **React.js** or **Alpine.js**.
<q>this website is made using the [**Astro.js**](https://astro.build) framework and [**Solid.js**](https://www.solidjs.com/) library.</q>

View File

@ -1,39 +0,0 @@
---
firstName: Сергей
lastName: Ярков
---
Я являюсь веб-разработчиком с опытом создания веб-сайтов с 2017 года. Специализируюсь на **full-stack** веб-разработке клиент-серверных приложений, решающих разнообразные задачи. Мне интересны такие сферы как программирование, встраиваемые системы и электроника. Начал с верстки простых HTML страниц и с 2019 года углубленно занимаюсь программированием. На сегодняшний день имею опыт разработки сложных приложений и публикую некоторые [проекты ](https://yarkov.tech/projects/)в открытый доступ на [GitHub](https://github.com/sergeyyarkov).
## О ресурсе
Изначально этот сайт был создан для личного использования, чтобы систематизировать и закреплять свои знания через написание статей. С 2022 года я начал публиковать материалы на этом сайте, и теперь в любой момент могу вернуться к любой теме для её повторного изучения. Я надеюсь, что мои статьи окажутся полезными и для других, помогая освоить различные темы.
Основное направление сайта — информационные технологии. В разделе [статей ](https://yarkov.tech/articles/)я делюсь материалами на интересующие меня темы, а также рассказываю о своих [проектах](https://yarkov.tech/projects/).
## Навыки
**Проектирование баз данных 📊**: Умею проектировать ERD (диаграммы сущностей и связей) базы данны.
**Деплой приложений 🚀**: Опыт деплоя приложений с использованием Docker и базовых инструментов CI/CD.
**Веб-Разработка 🌐**: Использую Node.js для разработки веб-приложений и веб-сайтов с использованием таких технологий как Directus, Astro.js, Adonis.js, Express.js, Alpine.js, Vite.js, TailwindCSS, SASS.
**Серверы 🖥️**: Умею работать с Linux и настраивать веб-сервер Nginx.
**Интерфейсы 🎨**: Имею опыт работы в Figma и могу составлять пользовательские интерфейсы (есть базовое понимание веб-дизайна).
**Управление базами данных 🗄️**: Умею использовать и настраивать PostgreSQL, MongoDB и Redis (реляционные и нереляционные базы данных).
**Отладка и оптимизация кода 🐞**: Опыт отладки кода,поиска ошибок и проведения рефакторинга с целью оптимизации.
**Архитектура** 🏛️: Понимание принципов работы микросервисной и монолитной архитектуры приложения.
**GIT** 🔧: Я использую Gitflow для контроля версий веб-проектов и Git Submodules. [Пример ](https://github.com/sergeyyarkov/educt/tree/main)организации проекта
**Встраиваемые системы 🔌**: Умею программировать микроконтроллеры AVR и разрабатывать электронные устройства, включая разработку схемотехники и трассировку печатных плат в KiCAD.
**Сборка проектов на C 🛠️**: Имею базовые знания сборки проектов на C с использованием Makefile.
Ниже представлен более подробный список того что я использую.
_этот веб-сайт сделан с помощью фреймворка **[Astro.js](https://astro.build)** и библиотеки **[Solid.js](https://www.solidjs.com/)**_

View File

@ -1,898 +0,0 @@
---
coverImage: /media/serial-port.jpg
draft: false
author: Sergey Yarkov
title: Using the WebSerial API to communicate with the microcontroller
description: We will talk about what is WebSerial API, how it can be used in a
browser to read and write data to a serial device and where it can be applied.
tags:
- JavaScript
- AVR
- WebSerial API
- Tutorial
pubDate: 2022-11-04T21:00:00.000Z
updatedDate: 2023-02-13T16:34:23.326Z
---
# What you will learn
By the end you will learn how to send and receive data from a serial port device in a browser, develop a demo web application that sends commands to a serial port microcontroller and write firmware for the [AVR128DA48](http://ww1.microchip.com/downloads/en/DeviceDoc/40002183A.pdf) microcontroller to process those commands. You can see the finished version of the project [here](https://web-serial-example.netlify.app/), the sources are uploaded to [repository](https://github.com/sergeyyarkov/avr128da48_web-serial-example) on GitHub.
# What it is and where it's used
The WebSerial API is a browser-based API that provides the ability to read and write data to a serial device. Using this API is quite easy, you only need to write a few lines of code to receive or send your data. There is also a detailed [documentation](https://wicg.github.io/serial/) with examples of how to use this API.
Thanks to this API, new possibilities open up. For example you can flash your microcontroller right from the browser which sounds really interesting (but you need to write a bootloader) or you can just output the sensor info to a web page. The possibilities are endless, it all depends on your imagination.
There are also high level APIs to access devices like a gamepad or camera. I'll leave this link to [this](https://web.dev/devices-introduction/) article which talks about a list of these APIs.
# Browser support.
As of this writing (2022), full support for this API is only available in Chromium-based browsers. You can see the full table at [Can I Use](https://caniuse.com/web-serial).
On Android, support can be provided based on [WebUSB API](https://wicg.github.io/webusb/) and [Serial API polyfill](https://github.com/google/web-serial-polyfill).
# A short excursus about UART
Before you work with serial port it would be nice to know the basics of UART data transfer protocol, if you are already familiar with it you can skip this part.
**UART** stands for Universal Asynchronous Receiver Transmitter and defines a protocol for sending and receiving data between two devices. **Universal** means that we can customize the parameters, including the baud rate. **Asynchronous**, on the other hand, means we have no synchronizing signal between transmitter and receiver. There is also a synchronous version of data transmission with a common clock signal, which can transmit data much faster, but it is rarely used. Only two lines are used to communicate between the two devices and these are designated as RX (**receiver**) and TX (**transmitter**).
<figure>
<img alt="Wiring diagram of two UART devices" width="550" height="300" src="/media/uart-connection.jpg" loading="lazy" decoding="async">
<figcaption>Wiring diagram of two UART devices</figcaption>
</figure>
UART transmits data one bit at a time in one of three modes:
* one-way mode - data is sent in one direction only, from transmitter to receiver
* half-duplex mode - devices can transmit and receive data one at a time
* full-duplex mode - devices can transmit and receive data simultaneously.
Since this project is supposed to use asynchronous version of the UART, then accordingly you need to set **the same**bit rate per second (`baud rate`, `bps`) on the two devices. The most common bit rates are: **4800, 9600, 19.2K, 57.6K, and 115.2K.**
Data is transmitted in so-called `packets`, where each packet contains:
* start bit and stop bit - the start bit signals the arrival of data bits, the stop bit signals the end of the data, respectively.
* data bits - user data that comes right after the start bit, it can contain from 5 to 8 bits.
* the parity bit is an optional bit that comes after the data bits and before the stop bit and is used to detect errors.
<figure>
<img src="/media/uart-packet.jpg" width="700" height="300" alt="Structure of the UART packet" format="webp">
<figcaption>Structure of the UART packet</figcaption>
</figure>
I think this information is enough to understand what UART is all about. If you want to learn more about this interface there are many articles about it on the internet.
# Interface Converter
Since you will most likely connect the microcontroller to the USB port of your computer, you need some kind of converter between the two interfaces to send and receive data, since USB and UART are completely different data transfer protocols. There are many such translators available, here are some of them: [CH340](https://pdf1.alldatasheet.com/datasheet-pdf/view/1132602/WCH/CH340.html), [CP2102](https://pdf1.alldatasheet.com/datasheet-pdf/view/201067/SILABS/CP2102.html) or [FT232](https://pdf1.alldatasheet.com/datasheet-pdf/view/197629/FTDI/FT232BL.html). If you are using an Arduino UNO debug board then you already have one of these converters on your board.
<figure>
<img src="/media/arduino-uart-converter.png" width="470" height="320" alt="CH340G interface converter on Arduino UNO board" format="webp"><figcaption>
CH340G interface converter on Arduino UNO board
</figcaption>
</figure>
If you don't have this interface converter on your development board, you can buy it in any store and it usually costs less than $5.
# Microcontroller firmware development
As I mentioned before the firmware will be written for the AVR128DA48 Microcontroller. It doesn't really matter what kind of microcontroller you use, you just need to be able to send and receive data using the UART peripherals on your microcontroller.
The program will be very simple: you need to write a ***command handler*** which will take a string (like `led_toggle`) and call a function which will do a certain action, in this case switch the led.
The problem statement is clear, now let's look at the implementation of this program. I will use the MPLAB X IDE and the C language to write the firmware.
## Necessary constants
First, we define a list of constants for the whole project by creating a file `constants.h`:
```c
#ifndef CONSTANTS_H
#define CONSTANTS_H
#define F_CPU 4000000UL
#define BAUD_RATE 9600
#define BUFFER_SIZE 20
#define EOT 0x04
#endif /* CONSTANTS_H */
```
* `F_CPU` - microcontroller clock speed
* `BAUD_RATE` - UART bit rate
* `BUFFER_SIZE` - buffer size, where we will put the incoming data
* `EOT` - End-of-Transmission, ASCII character for the end of data transfer
## USART initialization
Create a header file `usart.h` and define some functions to initialize USART, send a char and string.
```c
#include "constants.h"
#ifndef USART_H
#define USART_H
#define USART1_BAUD_RATE(BAUD_RATE) ((float)(64 * F_CPU / (16 * (float)BAUD_RATE)) + 0.5)
#include <avr/io.h>
#include <stdio.h>
#include <string.h>
void USART1_Initialize(void);
void USART1_SendChar(char c);
void USART1_SendString(char *str);
#endif /* USART_H */
```
The macro `USART1_BAUD_RATE` calculates the value to write to the register `USARTn.BAUD` to set the baud rate. The formula is taken from [documentation](https://ww1.microchip.com/downloads/aemDocuments/documents/MCU08/ProductDocuments/DataSheets/AVR128DA28-32-48-64-Data-Sheet-40002183C.pdf) for the microcontroller (chapter 25.3.2.2.1, table 25-1).
Let's implement the USART initialization function in the file `usart.c`:
```c
void USART1_Initialize(void) {
/* set baud rate */
USART1.BAUD = (uint16_t) (USART1_BAUD_RATE(BAUD_RATE));
/* set char size in a frame to 8 bit */
USART1.CTRLC = USART_CHSIZE0_bm | USART_CHSIZE1_bm;
/* config pins for TX and RX */
PORTC.DIRSET = PIN0_bm;
PORTC.DIRCLR = PIN1_bm;
/* enable reveice complete interrupt */
USART1.CTRLA = USART_RXCIE_bm;
/* enable transmitter and receiver */
USART1.CTRLB = USART_TXEN_bm | USART_RXEN_bm;
}
```
For full-duplex mode initialization of the asynchronous version of USART is as follows:
* Configuration of the baud rate by writing a value to the register `USARTn.BAUD`.
* Configuration of the `frame` size, in our case it is 8 bits.
* Configuration of the `TX` pin for output and `RX` pin for input.
* Enabling an interrupt for when the data reception is over (when one packet has arrived).
* Enabling the receiver and transmitter.
Now, let's take a look at the functions of sending data:
```c
void USART1_SendChar(char c) {
while (!(USART1.STATUS & USART_DREIF_bm));
USART1.TXDATAL = c;
}
void USART1_SendString(char *str) {
for (size_t i = 0; i < strlen(str); i++) {
USART1_SendChar(str[i]);
}
}
```
To send data you must write one byte to the `USARTn.TXDATAL` register, but before doing so you must ensure that the previous transfer was completed by checking the `USARTn.STATUS` register. As stated in the documentation, the `DREIF` (Data Register Empty Flag) bit is set if the data in the TX buffer is **absent**. Accordingly it is possible to make an empty loop which will run as long as we have data in the TX buffer.
The send string function is a kind of wrapper which will call the send one character function.
## Command Handler
As you can see from the problem statement, we get a string that tells us what action should be executed.
Let's define a header file `command.h` and write the following in it:
```c
#ifndef COMMAND_H
#define COMMAND_H
#include <avr/io.h>
#include <string.h>
#define COMMAND_SIZE 5
#define COMMAND_MAX_NAME_LENGTH 20
struct Command {
void *addr;
char name[COMMAND_MAX_NAME_LENGTH];
};
uint8_t command_define(void *fp, char name[COMMAND_MAX_NAME_LENGTH]);
uint8_t command_process(char *cmd_name);
void command_list(void);
#endif /* COMMAND_H */
```
To solve this problem, I propose to create an array of structures, where each structure will contain a pointer to the function we need to execute, and the name of the command itself.
* `command_define` will create a new structure in the array.
* `command_process` will call the corresponding function on the command name.
* `command_list` will send information about existing commands via UART.
You can implement this by creating a file `command.c` as follows:
```c
#include "command.h"
#include "usart.h"
struct Command commands[COMMAND_SIZE];
uint8_t cmd_idx = 0;
uint8_t command_define(void *fp, char name[COMMAND_MAX_NAME_LENGTH]) {
size_t name_length = strlen(name);
if (name_length > COMMAND_MAX_NAME_LENGTH) {
return 0;
}
if (cmd_idx > COMMAND_SIZE) {
cmd_idx = 0;
}
struct Command command;
command.addr = fp;
strcpy(command.name, name);
commands[cmd_idx++] = command;
return 1;
}
uint8_t command_process(char *cmd_name) {
uint8_t i = 0;
do {
if (strcmp(cmd_name, commands[i].name) == 0) {
int (*execute)();
execute = commands[i].addr;
execute();
USART1_SendString("[LOG]: Command completed successfully with code \"0\".\n");
USART1_SendChar(EOT);
return 0;
}
} while (i++ < COMMAND_SIZE);
USART1_SendString("[LOG]: Invalid command name!\n");
USART1_SendChar(EOT);
return 1;
}
void command_list(void) {
USART1_SendString("[LOG]: List of available commands:\n");
for (uint8_t i = 0; i < COMMAND_SIZE; i++) {
if (commands[i].addr) {
USART1_SendChar((i + 1) + '0');
USART1_SendString(". ");
USART1_SendString(commands[i].name);
USART1_SendChar('\n');
}
}
}
```
It is important to note that after sending the data, you must send the `EOT` symbol, which will tell you that the transmission is complete, this will help in the future development of the web application.
## Receiving data
The last thing to do is to receive commands from USART and send them to a handler. You can use an interrupt to do this:
```c
char buffer[BUFFER_SIZE];
volatile uint8_t buff_idx = 0;
ISR(USART1_RXC_vect) {
if (buff_idx > BUFFER_SIZE) buff_idx = 0;
buffer[buff_idx] = USART1.RXDATAL;
buff_idx++;
}
```
When the interrupt `USART1_RXC_vect` is triggered, data (one byte) appears in the `USART1.RXDATAL` register. This byte we have to write to our fixed size buffer. There is also another way to accept data without using an interrupt. To do this simply check the bit `RXCIF` (USART Receive Complete Interrupt Flag) in the register `USARTn.STATUS`. This bit is set when there is data in the receive buffer.
In order to process incoming commands you can check the buffer in the main loop for the `\n` character, this character will signify the end of the command name:
```c
while (1) {
if (buff_idx >= 1) {
if (buffer[buff_idx - 1] == '\n') {
buffer[buff_idx - 1] = '\0';
command_process(buffer);
buff_idx = 0;
}
}
}
```
After finding this character, you can pass the buffer to the handler, then reset the buffer index to zero. As a result, the `main.c` file will look like this
```c
#include <avr/io.h>
#include <avr/interrupt.h>
#include <string.h>
#include "constants.h"
#include "command.h"
#include "usart.h"
char buffer[BUFFER_SIZE];
volatile uint8_t buff_idx = 0;
void led_toggle(void);
void hello_world(void);
ISR(USART1_RXC_vect) {
if (buff_idx > BUFFER_SIZE) buff_idx = 0;
buffer[buff_idx] = USART1.RXDATAL;
buff_idx++;
}
void MCU_Init_Ports(void) {
/* led pin PC6 to output */
PORTC.DIRSET = PIN6_bm;
}
int main(void) {
MCU_Init_Ports();
USART1_Initialize();
sei();
command_define(led_toggle, "led_toggle");
command_define(hello_world, "hello");
command_define(command_list, "list");
while (1) {
if (buff_idx >= 1) {
if (buffer[buff_idx - 1] == '\n') {
buffer[buff_idx - 1] = '\0';
command_process(buffer);
buff_idx = 0;
}
}
}
}
void led_toggle(void) {
PORTC.OUTTGL = PIN6_bm;
}
void hello_world(void) {
USART1_SendString("Hello world!\n");
}
```
After initialization of the microcontroller ports and USART it is obligatory to enable the global interrupts by calling the function `sei`.
With the function `command_define` we can define by which USART command to start the function:
* `led_toggle` will toggle the LED.
* `hello_world` will output the message "Hello world".
* `command_list` will show a list of available commands.
## Operation demonstration
Now with the firmware assembled and loaded into your Microcontroller you can test it's functionality. I will use the program `CuteCom` to open the serial port, you can use any other similar program.
Select your device from the list (in my case it is `/dev/ttyACM1`) and set the following parameters:
* Speed: **9600** baud
* Number of bits: **8**
* Parity: **None**
* Stop bits: **1**
<figure>
<img
src="/media/uart-connect-settings.png"
width="700"
height="400"
alt="Settings for opening a serial port"
format="webp"
fit="fill"
>
<figcaption>Settings for opening a serial port</figcaption>
</figure>
Now after opening the port you can send commands, don't forget to set the carriage return as `LF`, because our firmware only recognizes the end of the command by this character.
<figure>
<img
src="/media/webserial-demo-1.png"
width="700"
height="400"
alt="An example of sending the ”hello” command"
format="webp"
fit="fill"
>
<figcaption>An example of sending the ”hello” command</figcaption>
</figure>
# Web application development
The web application will consist of just a few elements: an input box where we can write our commands, buttons to open and close the serial port, connection status. You can see the finished version [here](https://web-serial-example.netlify.app/).
## Create HTML markup and styles
The HTML markup will look like this:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Web Serial API Example</title>
<link href="./style.css" rel="stylesheet" />
<script defer src="./app.js"></script>
</head>
<body>
<div id="app">
<div>
<button id="connect">Connect</button>
<button id="disconnect">Disconnect</button>
</div>
<form id="terminal_form" action="#">
<label for="input">Enter command name:</label><br />
<div>
<input id="input" name="input" placeholder="e.g led_toggle" type="text" disabled />
<button name="send" type="submit" disabled>Send</button>
</div>
</form>
<textarea id="serial_log" placeholder=">" readonly></textarea>
<div id="port_info">
<div>
<p id="status">not connected</p>
</div>
<div>
<span>vendorId: <span id="vendor_id">-</span>&nbsp;|</span>
<span>deviceId: <span id="product_id">-</span></span>
</div>
</div>
</div>
</body>
</html>
```
Let's add some styles by creating a `style.css` file:
```css
:root {
--border-color: #c9d1d9;
--border-color-hover: #6e7681;
--font-family: monospace;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
#app {
display: flex;
flex-direction: column;
width: 100%;
max-width: 600px;
row-gap: 5px;
}
html,
body {
box-sizing: border-box;
font-size: 16px;
}
body {
display: flex;
justify-content: center;
margin-top: 2rem;
font-family: var(--font-family);
}
p {
padding: 0;
margin: 5px;
}
input,
textarea {
font-family: var(--font-family);
padding: 5px 15px;
border-radius: 4px;
width: 100%;
border: 1px solid var(--border-color);
}
textarea {
resize: vertical;
min-height: 200px;
line-height: 1.4;
font-size: 16px;
outline: none;
font-size: 12px;
}
label {
font-size: 14px;
font-style: italic;
}
button {
cursor: pointer;
background: none;
font-family: var(--font-family);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 5px 15px;
}
button:hover:not(:disabled) {
border: 1px solid var(--border-color-hover);
}
#terminal_form div {
display: flex;
justify-content: space-between;
gap: 5px;
margin-bottom: 5px;
margin-top: 5px;
}
#port_info {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
font-size: 14px;
}
#port_info #status {
text-transform: uppercase;
}
```
## Creating a SerialPortHandler class
The logic of working with serial port will be implemented in this class, which will be a small wrapper for easy use of WebSerial API, it will have only 5 methods and a few properties.
Let's define this class in the `app.js` file:
```jsx
class SerialPortHandler {
constructor(options, onConnect, onDisconnect) {
this.encoder = new TextEncoder();
this.decoder = new TextDecoder();
this.onConnect = onConnect;
this.onDisconnect = onDisconnect;
this.options = options;
this.port = null;
this.isOpened = false;
this.#setupListeners();
}
async open() {}
async close() {}
async write(data) {}
async read() {}
#setupListeners() {}
}
```
* [`TextEncoder`](https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder) is a class that encodes a string into an array of unsigned 8-bit integers [`Uint8Array`](https://webidl.spec.whatwg.org/#idl-Uint8Array).
* [`TextDecoder`](https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder#examples), on the contrary, decodes into a string. We will need instances of these classes, because we will be operating with data of type `Uint8Array`.
* The `onConnect` and `onDisconnect` functions will be called when the device is connected or disconnected.
* The [`options`](https://wicg.github.io/serial/#serialoptions-dictionary) property contains the necessary parameters that will be used for transmitting and receiving data.
* Property [`port`](https://wicg.github.io/serial/#serialport-interface) - object which will contain methods for working with the port and information about the device after the connection is established.
* The function `setupListeners` will simply add event handlers.
The following is the implementation of these methods.
## Opening a serial port
To open a serial port you first need to call the [`requestPort`](https://wicg.github.io/serial/#requestport-method) method which will open a special window with a list of devices. It is also necessary that the user himself activate the call to this window, otherwise there will be an error. This can be done just by the event of pressing the button.
```jsx
async open() {
try {
const port = await navigator.serial.requestPort();
await port.open(this.options);
this.port = port;
this.isOpened = true;
return this.port.getInfo();
} catch (error) {
console.error(error);
throw error;
}
}
```
You can pass an optional filter parameter to the `requestPort` function, which will limit the list of selectable ports according to the USB ID:
```jsx
const port = await navigator.serial.requestPort({
filters: [{ usbVendorId: 0x7522 }],
});
```
A list of USB IDs can be found at [this](https://devicehunt.com/all-usb-vendors).
<figure>
<img
src="/media/webserial-connect.png"
width="700"
height="400"
alt="Selecting a device through a special window"
format="webp"
fit="fill"
>
<figcaption>Selecting a device through a special window</figcaption>
</figure>
After the user has selected the device, the `requestPort` method returns [`port`](https://wicg.github.io/serial/#serialport-interface) which can be opened by passing [parameters](https://wicg.github.io/serial/#serialoptions-dictionary) there:
* `baudRate` : baud rate.
* `dataBits` : number of data bits in the frame (7 or 8).
* `stopBits` : number of stop bits at the end of the packet (1 or 2).
* `parity` : parity mode ( `none` , `even` or `odd` ).
* `bufferSize` : the size of the read and write buffers to be created.
* `flowControl` : flow control mode ( `none` or `hardware`).
The `baudRate` property is the only mandatory parameter, the others are optional and have [default values](https://wicg.github.io/serial/#serialoptions-dictionary).
At the end our method returns [information](https://wicg.github.io/serial/#dom-serialportinfo) about the connected device using the method [`getInfo`](https://wicg.github.io/serial/#dom-serialport-getinfo).
## Read data
Note that the WebSerial API is asynchronous, which prevents the user interface from locking up while accepting data.
It is also important to know that the WebSerial API uses streams [Stream API](https://streams.spec.whatwg.org/). When streaming, the data is split into `(chunks)`, this allows the data to be processed without completely waiting for it to arrive.
Receiving data can be implemented as follows:
```jsx
async read() {
while (this.port.readable) {
const reader = this.port.readable.getReader();
let chunks = '';
try {
while (true) {
const { value, done } = await reader.read();
const decoded = this.decoder.decode(value);
chunks += decoded;
if (done || decoded.includes(EOT)) {
console.log('Reading done.');
reader.releaseLock();
break;
}
}
return chunks;
} catch (error) {
console.error(error);
throw error;
} finally {
reader.releaseLock();
}
}
}
```
When the user has connected to the device, `this.port` will have properties like `writable` and `readable` which are instances of the [WritableStream](https://streams.spec.whatwg.org/#writablestream) and [ReadableStream](https://streams.spec.whatwg.org/#readablestream) classes. We will need them to read and write data.
The external `while` loop is for error checking, such as parity checks. If a fatal error occurs, the `readable` property will become `null`.
The method [`getReader`](https://streams.spec.whatwg.org/#rs-get-reader) creates [reader](https://streams.spec.whatwg.org/#reader) which will allow to get data in fragments. A readable thread has no more than one reader at a time, so after that the thread is **[locked](https://streams.spec.whatwg.org/#lock)**, but the reader remains **active**.
The internal `while` loop reads data using the `read` method of the reader. This method returns the `value` property, which has data of type `UInt8Array`, and the `done` property, which becomes `true` if the serial device no longer transmits any data. Then we decode the data into a string and write it into the `chunks` variable. There is also a check for the `EOT` (End-of-Transmission) character, which means the end of the data transfer. Define this character at the beginning of the `app.js` file as a global variable:
```jsx
var EOT = "\u0004";
```
After the end of data reception, the thread must be unlocked by calling the `releaseLock` method to make the reader inactive.
## Write data
In order to send data to a serial device, you must create a [writer](https://streams.spec.whatwg.org/#writer) that will provide the ability to send snippets to the writing stream:
```jsx
async write(data) {
const writer = this.port.writable.getWriter();
const encoded = this.encoder.encode(data);
await writer.write(encoded);
writer.releaseLock();
}
```
Before sending, you need to encode your string into data of type `UInt8Array`. After creating a writer, the writing thread is also locked, so after writing it must be unlocked using the same `releaseLock` method.
## Closing a port
You can use the [`close`](https://wicg.github.io/serial/#dom-serialport-close) method to close a port when communication with the device is no longer needed:
```jsx
async close() {
await this.port.close();
this.isOpened = false;
}
```
The port cannot be closed when the thread that is writing or reading is blocked.
## Event Handling
The API allows you to subscribe to events when a trusted device connects or when it disconnects:
```jsx
#setupListeners() {
navigator.serial.addEventListener('connect', this.onConnect);
navigator.serial.addEventListener('disconnect', this.onDisconnect);
}
```
## Using the created class
Now you can write the front-end part of the web application using our created class `SerialPortHandler`:
```jsx
class Application {
constructor(root) {
if (!("serial" in navigator)) {
console.error("Web Serial API is not supported in your browser.");
return;
}
this.serialPortHandler = new SerialPortHandler(
{ baudRate: 9600 },
() => console.log("Device connected."),
() => {
console.log("Device disconnected.");
this.#disconnectHandler();
}
);
/**
* DOM Elements
*/
this.$root = root;
this.$connectButton = this.$root.querySelector("#connect");
this.$disconnectButton = this.$root.querySelector("#disconnect");
this.$terminalForm = this.$root.querySelector("#terminal_form");
this.$serialLog = this.$root.querySelector("#serial_log");
this.$status = this.$root.querySelector("#status");
this.$vendorId = this.$root.querySelector("#vendor_id");
this.$productId = this.$root.querySelector("#product_id");
this.#setupEvents();
}
/**
* Handlers for connecting, disconnecting and sending a command
*/
#setupEvents() {
this.$connectButton.addEventListener("click", this.#connectHandler.bind(this));
this.$disconnectButton.addEventListener("click", this.#disconnectHandler.bind(this));
this.$terminalForm.addEventListener("submit", this.#submitHandler.bind(this));
}
/**
* Open serial port and notify user of connection status
* @returns {Promise<void>}
*/
async #connectHandler() {
try {
if (this.serialPortHandler.isOpened) return;
const info = await this.serialPortHandler.open();
console.log("Port opened: ", info);
this.$terminalForm.elements.input.removeAttribute("disabled");
this.$terminalForm.elements.send.removeAttribute("disabled");
this.$vendorId.textContent = "0x" + info.usbVendorId.toString(16);
this.$productId.textContent = "0x" + info.usbProductId.toString(16);
this.$status.textContent = "CONNECTED";
} catch (error) {
this.$status.textContent = "ERROR";
}
}
/**
* Closes the serial port and updates the connection status.
* @returns {Promise<void>}
*/
async #disconnectHandler() {
if (!this.serialPortHandler.isOpened) return;
await this.serialPortHandler.close();
this.$terminalForm.elements.input.setAttribute("disabled", "true");
this.$terminalForm.elements.send.setAttribute("disabled", "true");
this.$vendorId.textContent = "-";
this.$productId.textContent = "-";
this.$status.textContent = "NOT CONNECTED";
}
/**
* Writes data to the serial port and reads the response
* @param {SubmitEvent} e - Form submit event
*/
async #submitHandler(e) {
e.preventDefault();
const $form = e.target;
const data = $form.elements.input.value;
$form.reset();
if (this.serialPortHandler.isOpened && data) {
this.$serialLog.innerHTML += ">" + data + "\n";
await this.serialPortHandler.write(data + "\n");
const message = await this.serialPortHandler.read();
this.$serialLog.textContent += message.replaceAll(EOT, "");
console.log("Message received: \n" + message);
}
this.$serialLog.scrollTo(0, this.$serialLog.scrollHeight);
}
}
```
Basically there is a work with interface updates and I think it is not necessary to explain the logic in detail, I will note only the key points:
In the constructor of this class, you can determine whether the browser supports WebSerial API by checking the `serial` key in the `navigator` object, and if not, we write an error to the console. Object `this.serialPortHandler` will be an instance of our created class `SerialPortHandler`, we pass the speed **9600** and functions which will be the handlers of events to connect and disconnect the device as arguments. Then we look for the necessary DOM elements and add an event listener to some of them.
The `#submitHandler` method is designed to send commands and accept the result of command execution. When you use the `write` method, be sure to add a `\n` character at the end, so the program in the microcontroller knows where the command name ends.
Initialize the application at the end of the `app.js` file:
```js
new Application(document.getElementById("app"));
```
## Checking the web application
You can now send commands to the microcontroller:
<figure>
<img
src="/media/webserial-demo-2.png"
width="550"
height="300"
alt="The finished result of the web application"
format="webp"
>
<figcaption>The finished result of the web application</figcaption>
</figure>
For fun, you can flash the LED:
```js
const app = new Application(document.getElementById("app"));
setInterval(async () => {
if (app.serialPortHandler.isOpened) {
await app.serialPortHandler.write("led_toggle" + "\n");
const message = await app.serialPortHandler.read();
console.log(message);
}
}, 500);
```
<figure>
<img src="/media/webserial-demo-3.gif" loading="lazy" alt="blinking LED" />
<figcaption>Blinking LED</figcaption>
</figure>
# Conclusion
Having developed the web application we have seen that using the WebSerial API is quite easy. I hope that this article will be useful to you and help you in developing your project.
# Useful stuff
* [Browser support status Can I use](https://caniuse.com/web-serial)
* [WebSerial API documentation](https://wicg.github.io/serial/)
* [Stream API detailed guide](https://web.dev/streams/)
* [USB ID list](https://devicehunt.com/all-usb-vendors)
* [Access to different devices from the browser](https://web.dev/devices-introduction/)

View File

@ -1,612 +0,0 @@
---
coverImage: null
draft: true
author: Sergey Yarkov
title: Implementation of delays in the AVR microcontroller
description: Очень часто в программах приходится выполнять задержку. Например
при реализации какого либо протокола необходимо в точности выдерживать
тайминги, или же необходимо выполнять какую либо логику раз в несколько часов
или дней. В этой статье рассмотрены несколько способов реализации задержек.
tags:
- AVR
- Tutorial
pubDate: 2023-10-15T16:28:52.587Z
updatedDate: 2023-10-15T16:28:52.601Z
---
# Способы реализации
Существует два способа как можно реализовать задержку - это использовать аппаратные таймеры микроконтроллера или использовать простой цикл который выполняется определенное количество тактов. Преимущество использования аппаратного таймера микроконтроллера перед обычным программным циклом заключается в неблокирующем выполнении программы за счет того что работа таймера-счетчика не зависит от выполнения основной программы. Но иногда нужно использовать задержку всего в несколько микросекунд, что особо не повлияет на работу программы в некоторых случаях, тогда можно посмотреть в сторону простых циклов. Будет рассмотрен вариант и с длительной задержкой в несколько часов или дней. Программы будут представлены на языке Си и Ассемблера для микроконтроллера AVR ATtiny45V. В качестве основной программы будет моргающий светодиод который переключается раз в 0.5 секунд.
## Используем цикл
Для реализации этого способа задержки для начала нужно посчитать кол-во тактов процессора для выполнения задержки в N секунд и затем сделать цикл. Например для МК с частотой в 8 MHz один такт будет равен `1/8 = 0.125uS`. Значит чтобы рассчитать кол-во тактов например в 0.5 секунд то это будет `(0.5 × 1,000,000) × 8 = 4,000,000` тактов, т.е половина от частоты МК.
Реализация цикла задержки на ассемблере:
```asmatmel
.MACRO delay16
ldi delay8_r, LOW(@0 * F_CPU / 4 - 3) ; 1
ldi delay16_r, HIGH(@0 * F_CPU / 4 - 3) ; 1
rcall DELAY16_LOOP ; 3
.ENDMACRO
DELAY16_LOOP:
subi delay8_r, 1 ; 1
sbci delay16_r, 0 ; 1
brcc DELAY16_LOOP ; 1 = false, 2 = true
ret ; 4
```
Каждая инструкция в микроконтроллере выполняется за определенное кол-во тактов (комментарии в коде выше). Например инструкция `ldi` выполняется за один такт, а инструкция `brcc` выполняется за два такта - когда условие верно и за один такт когда условие неверно. На кол-во тактов так же влияет и разрядность программного счетчика (см. [AVR Instruction Set](http://ww1.microchip.com/downloads/en/devicedoc/atmel-0856-avr-instruction-set-manual.pdf)).
### Пояснения к программе
В данной реализации используется макрос `delay16` который загружает кол-во тактов в два 8-ми битных регистра которые помечены как младший и старший. Затем вызывается подпрограмма выполнения цикла. Зачем нужно в расчете тактов делить полученное число на 4? Посмотрите на подпрограмму `DELAY16_LOOP` которая выполняет декрементирование 16-ти битного числа, вы увидите что одна итерация цикла занимает 4 такта. От полученного числа отнимаем 3 для компенсации времени.
В итоге максимальная задержка которая может получиться при работе МК в 8 MHz вызвав этот макрос равняется `65535/8 * 4 = 32767.5uS` что не совсем подходит для мигания светодиодом. Что бы сделать задержку дольшо можно использовать 24 бита для хранения кол-ва тактов. Тогда максимальная задержка составит `16,777,216/8 * 5 = 10.48сек`. В итоге получается такая программа с переключающимся светодиодом раз в 0.5 секунд:
```asmatmel
.EQU F_CPU = 8 ; MHz
.DEF t1 = r16
.DEF t2 = r17
.DEF delay24_r = r18
.DEF delay16_r = r19
.DEF delay8_r = r20
.EQU LED_DDR = DDRB
.EQU LED_PORT = PORTB
.EQU LED_PIN_DATA = PINB
.EQU LED_PIN = PB0
.MACRO delay24
ldi delay8_r, LOW(@0 * F_CPU / 5 - 3)
ldi delay16_r, HIGH(@0 * F_CPU / 5 - 3)
ldi delay24_r, BYTE3(@0 * F_CPU / 5 - 3)
rcall DELAY24_LOOP
.ENDMACRO
.CSEG
.ORG 0x00
rjmp RESET_vect
RESET_vect:
ldi t1, LOW(RAMEND)
out SPL, t1
INIT:
ldi t1, (1<<LED_PIN)
out LED_DDR, t1
clr t1
out LED_PORT, t1
LOOP:
nop
delay24 500000
nop
rcall LED_TOGGLE
rjmp LOOP
LED_TOGGLE:
push t1
push t2
in t1, LED_PIN_DATA
ldi t2, (1<<LED_PIN)
eor t1, t2
out LED_PORT, t1
pop t2
pop t1
ret
DELAY24_LOOP:
subi delay8_r, 1
sbci delay16_r, 0
sbci delay24_r, 0
brcc DELAY24_LOOP
ret
```
Выполнить компиляцию:
```bash
avrasm2 -fI ./main.asm
```
Если использовать 32 бита для хранения кол-ва тактов то максимальная задержка уже составит 3221 секунда! Также вместо регистров общего назначения можно использовать ячейки, определенные в оперативной памяти (SRAM).
Чтобы проверить точно ли выполняется задержка, воспользуемся симулятором который встроен в IDE MPLAB X. Перед этим необходимо настроить частоту работы МК в конфигурации проекта:
<figure>
<img src="/media/mplabx-ide-sim-option.png" alt="Выбор частоты выполнения инструкций для симулятора">
<figcaption>Выбор частоты выполнения инструкций для симулятора</figcaption>
</figure>
Установим точки останова на холостые операции **nop** до и после вызова макроса и выполним отладку:
![Отладка программы](/media/screen-1.png)
До вызова инструкции **nop** прошло 8 тактов или 1 микросекунда. Очищаем Stopwatch и нажимаем кнопку продолжения программы (F5):
![Отладка программы](/media/screen-2.png)
Можно видеть что после выхода из подпрограммы `DELAY24_LOOP` прошло ровно 500 миллисекунд.
Для языка Си все будет довольно просто. Библиотека [AVR Libc](https://www.nongnu.org/avr-libc/) поставляется с набором утилит которые упрощают программирование. Поэтому нам достаточно указать частоту МК и вызвать определенную функцию. Эта функция как раз из рассчитает нужное кол-во тактов процессора и выполнит цикл.
Код основной программы на языке Си:
```c
#define F_CPU 8000000
#include <avr/io.h>
#include <util/delay.h>
#define PIN_LED PB0
int main(void) {
DDRB |= _BV(PIN_LED);
PORTB = 0x00;
for(;;) {
_delay_ms(500);
PORTB ^= _BV(PIN_LED);
}
return 0;
}
```
Выполнить компиляцию:
```c
avr-gcc -Wall -mmcu=attiny45 -Os main.c
avr-objcopy -j .text -j .data -O ihex a.out main.hex
```
## Используем таймеры
Что если программе не нужно затрачивать процессорное время на выполнение цикла задержки? Как отмечалось ранее работа таймера-счетчика не зависит от выполнения программы. Счетчик может инкрементироваться хоть раз в секунду при работе процессора с частотой в 8MHz. Существует несколько способов как сделать задержку используя 8-ми или 16-ти битные таймеры. Я опишу некоторые из них.
### 8-ми битный таймер с предделителем 16384
Для микроконтроллера ATtiny45V имеются только 8-ми битные таймеры, причем у одного из них (TIMER1) имеется делитель частоты на 16384 что очень удобно:
<figure>
<img src="/media/screen-3.png" alt="Таблица выбора предделителя для таймера-счетчика 1">
<figcaption>Таблица выбора предделителя для таймера-счетчика 1</figcaption>
</figure>
Будем использовать таймер в CTC (Clear on Timer Compare match) режиме. Итоговая частота работы таймера-счетчика будет равна: `8MHz/16384 = 488.28Hz`. Один тик таймера будет равняться `1/488.28 = 2048uS`. Значит для 0.5 секунд значение счетчика будет равняться `0.5×488.38 = 244.14` тиков что достаточно для 8-бит. Будем записывать число в регистр сравнения `OCRn` и генерировать прерывание при совпадении.
Программа на ассемблере:
```asmatmel
.DEF t1 = r16
.DEF t2 = r17
.EQU LED_DDR = DDRB
.EQU LED_PORT = PORTB
.EQU LED_PIN_DATA = PINB
.EQU LED_PIN = PB0
.CSEG
.ORG 0x00
rjmp RESET_vect
.ORG 0x03
rjmp TIMER1_COMPA
RESET_vect:
ldi t1, LOW(RAMEND)
out SPL, t1
INIT:
ldi t1, (1<<LED_PIN)
out DDRB, t1
clr t1
out LED_PORT, t1
; ========= Setup 8-bit timer-1 in CTC mode with 16384 prescaler =============
; CPU Clock Freq = 8MHz
; Freq(t) = 8MHz / 16384 = 488.28125 Hz
; Tick = 1 / Freq(t) = 1 / 488.28125 = 2048uS (0.002048 sec)
; Ticks = 0.5 sec / 0.002048 sec = 244
; ============================================================================
ldi t1, (1<<CTC1) | (1<<CS13) | (1<<CS12) | (1<<CS11) | (1<<CS10)
out TCCR1, t1 ; setup timer
clr t1 ; clear timer counter register
out TCNT1, t1
ldi t1, 244 ; 0.5 sec
out OCR1A, t1
out OCR1C, t1 ; for clearing timer on compare match
ldi t1, (1<<OCIE1A) ; enable output compare A interrupt
out TIMSK, t1
sei
LOOP:
nop
rjmp LOOP
LED_TOGGLE:
push t1
push t2
in t1, LED_PIN_DATA
ldi t2, (1<<LED_PIN)
eor t1, t2
out LED_PORT, t1
pop t2
pop t1
ret
TIMER1_COMPA:
push t1
in t1, SREG
rcall LED_TOGGLE
out SREG, t1
pop t1
reti
```
Программа на языке Си:
```c
#define F_CPU 8000000
#include <avr/interrupt.h>
#include <avr/io.h>
#define LED_DDR DDRB
#define LED_PORT PORTB
#define LED_PIN_DATA PINB
#define LED_PIN PB0
/*
* Timer Counter 1
* Mode: CTC
* Prescaler: 16384
*/
void setup_tc1(void) {
TCCR1 |= _BV(CTC1) | _BV(CS13) | _BV(CS12) | _BV(CS11) | _BV(CS10);
TCNT1 = 0x00;
OCR1A = 244;
OCR1C = 244;
TIMSK |= _BV(OCIE1A);
}
void setup_ports(void) {
DDRB |= _BV(LED_PIN);
PORTB = 0x00;
}
/*
* Interrupt every 0.5 sec
*/
ISR(TIMER1_COMPA_vect) {
LED_PORT ^= _BV(LED_PIN);
}
void system_init(void) {
setup_ports();
setup_tc1();
sei();
}
int main(void) {
system_init();
for (;;) {
asm("nop");
}
return 0;
}
```
### 8-ми битный таймер с предделителем 1024
Что если доступен предделитель частоты только на 1024? Тогда регистр для сравнения `OCRn` будет равняться 3906, что не подходит для 8-ми битного регистра. Как вариант можно считать количество переполнений таймера. `3906.25/255 = 15` переполнений.
Программа на ассемблере:
```asmatmel
.DEF t1 = r16
.DEF t2 = r17
.DEF tc1_ovf_cnt = r18
.EQU LED_DDR = DDRB
.EQU LED_PORT = PORTB
.EQU LED_PIN_DATA = PINB
.EQU LED_PIN = PB0
.EQU TC1_OVF_COMPARE_VAL = 15
.CSEG
.ORG 0x00
rjmp RESET_vect
.ORG 0x04
rjmp TIMER1_OVF
RESET_vect:
ldi t1, LOW(RAMEND)
out SPL, t1
INIT:
ldi t1, (1<<LED_PIN)
out DDRB, t1
clr t1
out LED_PORT, t1
; ========= Setup 8-bit timer-1 in Normal mode with 1024 prescaler =======================
; CPU Clock Freq = 8MHz
; Freq(t) = 8MHz / 1024 = 7812.5 Hz
; Tick = 1 / Freq(t) = 1 / 7812.5 = 128uS (0.000128 sec)
; Ticks = 0.5 sec / 0.000128 sec = 3906.25
; Overflows count = 3906 / 255 = 15
; ======================================================================================
ldi t1, (1<<CS13) | (1<<CS11) | (1<<CS10)
out TCCR1, t1 ; setup timer
clr t1 ; clear timer counter register
out TCNT1, t1
ldi t1, (1<<TOIE1) ; enable overflow interrupt
out TIMSK, t1
sei
LOOP:
nop
rjmp LOOP
LED_TOGGLE:
push t1
push t2
in t1, LED_PIN_DATA
ldi t2, (1<<LED_PIN)
eor t1, t2
out LED_PORT, t1
pop t2
pop t1
ret
TIMER1_OVF:
push t1
in t1, SREG
inc tc1_ovf_cnt
cpi tc1_ovf_cnt, TC1_OVF_COMPARE_VAL
brne PC+3
rcall LED_TOGGLE
clr tc1_ovf_cnt ; clear timer overflow counter register
out SREG, t1
pop t1
reti
```
Программа на языке Си:
```c
#define F_CPU 8000000
#include <avr/interrupt.h>
#include <avr/io.h>
#define TC1_OVF_COMPARE_VAL 15
#define LED_DDR DDRB
#define LED_PORT PORTB
#define LED_PIN_DATA PINB
#define LED_PIN PB0
static volatile uint8_t tc1_ovf_cnt = 0;
/*
* Timer Counter 1
* Mode: Normal
* Prescaler: 1024
*/
void setup_tc1(void) {
TCCR1 |= _BV(CS13) | _BV(CS11) | _BV(CS10);
TCNT1 = 0x00;
TIMSK |= _BV(TOIE1);
}
void setup_ports(void) {
DDRB |= _BV(LED_PIN);
PORTB = 0x00;
}
ISR(TIMER1_OVF_vect) {
tc1_ovf_cnt++;
if (tc1_ovf_cnt == TC1_OVF_COMPARE_VAL) {
LED_PORT ^= _BV(LED_PIN);
tc1_ovf_cnt = 0;
}
}
void system_init(void) {
setup_ports();
setup_tc1();
sei();
}
int main(void) {
system_init();
for (;;) {
asm("nop");
}
return 0;
}
```
### Длительная задержка
Бывают случаи когда нужно выполнять какую либо логику раз в несколько часов или дней. Решением является завести глобальную переменную где будем хранить количество прошедших секунд с момента начала отсчета таймера, а в прерывании переполнения таймера будем уже сравнивать значение счетчика с заданным числом. Следующая программа переключает светодиод раз в 6 часов.
```asmatmel
.DEF t1 = r16
.DEF t2 = r17
.DEF tc1_ovf_cnt = r18
.EQU LED_DDR = DDRB
.EQU LED_PORT = PORTB
.EQU LED_PIN_DATA = PINB
.EQU LED_PIN = PB0
.EQU TC1_OVF_COMPARE_VAL = 30
.DSEG
.ORG SRAM_START
ellapsed_seconds: .BYTE 2
.CSEG
.ORG 0x00
rjmp RESET_vect
.ORG 0x04
rjmp TIMER1_OVF
RESET_vect:
ldi t1, LOW(RAMEND)
out SPL, t1
INIT:
ldi t1, (1<<LED_PIN)
out DDRB, t1
clr t1
out LED_PORT, t1
sts ellapsed_seconds, t1
sts ellapsed_seconds+1, t1
; ========= Setup 8-bit timer-1 in Normal mode with 1024 prescaler =======================
; CPU Clock Freq = 8MHz
; Freq(t) = 8MHz / 1024 = 7812.5 Hz
; Tick = 1 / Freq(t) = 1 / 7812.5 = 128uS (0.000128 sec)
; Ticks = 1 sec / 0.000128 sec = 7812,5
; Overflows count = 7812 / 255 = 30
; ======================================================================================
ldi t1, (1<<CS13) | (1<<CS11) | (1<<CS10)
out TCCR1, t1 ; setup timer
clr t1 ; clear timer counter register
out TCNT1, t1
ldi t1, (1<<TOIE1) ; enable overflow interrupt
out TIMSK, t1
sei
LOOP:
nop
rjmp LOOP
LED_TOGGLE:
push t1
push t2
in t1, LED_PIN_DATA
ldi t2, (1<<LED_PIN)
eor t1, t2
out LED_PORT, t1
pop t2
pop t1
ret
TIMER1_OVF:
push t1
in t1, SREG
inc tc1_ovf_cnt
cpi tc1_ovf_cnt, TC1_OVF_COMPARE_VAL
brne _TIMER1_OVF_EXIT
clr tc1_ovf_cnt ; clear timer overflow counter register
lds r24,ellapsed_seconds
lds r25,ellapsed_seconds+1
adiw r24, 1
sts ellapsed_seconds+1, r25
sts ellapsed_seconds, r24
cpi r24, LOW(21600)
sbci r25, HIGH(21600)
brne _TIMER1_OVF_EXIT
rcall LED_TOGGLE
clr r25
sts ellapsed_seconds, r25 ; clear ellapsed seconds
sts ellapsed_seconds+1, r25 ; -----------------------
_TIMER1_OVF_EXIT:
out SREG, t1
pop t1
reti
```
Программа на языке Си:
```c
#include <avr/interrupt.h>
#include <avr/io.h>
#define TC1_OVF_COMPARE_VAL 30
#define LED_DDR DDRB
#define LED_PORT PORTB
#define LED_PIN_DATA PINB
#define LED_PIN PB0
static volatile uint8_t tc1_ovf_cnt = 0;
static volatile uint16_t ellapsed_seconds = 0;
/*
* Timer Counter 1
* Mode: Normal
* Prescaler: 1024
*/
void setup_tc1(void) {
TCCR1 |= _BV(CS13) | _BV(CS11) | _BV(CS10);
TCNT1 = 0x00;
TIMSK |= _BV(TOIE1);
}
void setup_ports(void) {
DDRB |= _BV(LED_PIN);
PORTB = 0x00;
}
ISR(TIMER1_OVF_vect) {
tc1_ovf_cnt++;
if (tc1_ovf_cnt == TC1_OVF_COMPARE_VAL) {
tc1_ovf_cnt = 0;
ellapsed_seconds++;
/* Every 6 hours */
if (ellapsed_seconds == 21600) {
LED_PORT ^= _BV(LED_PIN);
ellapsed_seconds = 0;
}
}
}
void system_init(void) {
setup_ports();
setup_tc1();
sei();
}
int main(void) {
system_init();
for (;;) {
asm("nop");
}
return 0;
}
```
## Заключение
Рассмотрены два основных способа реализации задержки используя таймеры и циклы. Выяснили что используя таймер-счетчик можно сделать задержку, которая не будет блокировать основной поток выполнения программы. Для длительных задержек, где необходимо отслеживать прошедшее время в секундах или часах, можно использовать глобальные переменные.
## Полезные материалы
- [AVR Instruction Set](https://ww1.microchip.com/downloads/en/devicedoc/atmel-0856-avr-instruction-set-manual.pdf)
- [AVR130: Setup and Use of AVR Timers](https://ww1.microchip.com/downloads/en/Appnotes/Atmel-2505-Setup-and-Use-of-AVR-Timers_ApplicationNote_AVR130.pdf)
- [AVR Delay Loop Calculator](http://darcy.rsgc.on.ca/ACES/TEI4M/AVRdelay.html)
- [AVR Timer Calculator](https://eleccelerator.com/avr-timer-calculator/)

View File

@ -1,899 +0,0 @@
---
coverImage: /media/serial-port.jpg
draft: false
author: Сергей Ярков
title: Использование WebSerial API для общения с микроконтроллером
description: Речь пойдет о том, что такое WebSerial API, как его можно
использовать в браузере для чтения и записи данных в последовательное
устройство и где его можно применить.
tags:
- JavaScript
- AVR
- WebSerial API
- Tutorial
pubDate: 2022-11-04T21:00:00.000Z
updatedDate: 2023-02-13T16:34:23.326Z
---
# Что вы узнаете
По итогу вы узнаете, как отправлять и принимать данные от устройства с последовательным портом в браузере, разработаете демонстрационное веб-приложение, которое отправляет команды в микроконтроллер с последовательным портом и напишите прошивку для микроконтроллера [AVR128DA48](http://ww1.microchip.com/downloads/en/DeviceDoc/40002183A.pdf) для обработки этих команд. Готовый вариант проекта можно посмотреть [здесь](https://web-serial-example.netlify.app/), исходники загружены в [репозиторий](https://github.com/sergeyyarkov/avr128da48_web-serial-example) на GitHub.
# Что это и где применяется
WebSerial API - это браузерный API, который предоставляет возможность к чтению и записи данных в последовательное устройство. Использование этого API довольно простое, нужно лишь написать несколько строк кода, чтобы получить или отправить ваши данные. К тому же имеется подробная [документация](https://wicg.github.io/serial/), где имеются примеры использования этого API.
Благодаря этому API, открываются новые возможности. Например, вы можете перепрошивать ваш микроконтроллер прямо из браузера, что звучит весьма интересно (но перед этим вам необходимо написать bootloader), или же просто выводить информацию с датчиков на веб страницу. Вариантов применения неограниченно, все зависит только от вашей фантазии.
Еще существуют и высокоуровневые API для доступа к таким устройствам как геймпад или камера. Я оставлю эту ссылку на [эту](https://web.dev/devices-introduction/) статью, где рассказывается про список этих API.
# Поддержка браузеров
На момент написания статьи (2022 год) полноценная поддержка этого API доступна лишь в браузерах на базе Chromium. Полную таблицу можно посмотреть на сайте [Can I Use](https://caniuse.com/web-serial).
На Android поддержка может быть осуществлена на базе [WebUSB API](https://wicg.github.io/webusb/) и [полифилла Serial API](https://github.com/google/web-serial-polyfill).
# Краткий экскурс про UART
Перед работой с последовательным портом, неплохо было бы знать основы протокола передачи данных UART, если вы с ним уже знакомы, можете пропустить эту часть.
**UART** - расшифровывается как (Universal Asynchronous Receiver Transmitter) - универсальный асинхронный приемопередатчик, который определяет протокол для приема и передачи данных между двумя устройствами. **Универсальность** означает, что мы можем настраивать параметры, включая скорость передачи данных. **Асинхронность** же означает, что у нас нет синхронизирующего сигнала между передатчиком и приёмником. Существует также и синхронная версия передачи данных с общим тактовым сигналом, которая может передавать данные намного быстрее, но она используется редко. Для связи двух устройств используются лишь две линии, которые обозначаются как RX (**receiver**) и TX (**transmitter**).
<figure>
<img alt="Схема подключения двух UART устройств" width="550" height="300" src="/media/uart-connection.jpg" loading="lazy" decoding="async">
<figcaption>Схема подключения двух UART устройств</figcaption>
</figure>
UART передает данные последовательно по одному биту в одном из трех режимов:
* односторонний режим - данные отправляются только в одном направлении, от передатчика к приемнику
* полудуплексный режим - устройства могут передавать и принимать данные по очереди
* полнодуплексный режим - устройства могут передавать и принимать данные одновременно
Так как в этом проекте предполагается использование асинхронной версии UART, то соответственно необходимо устанавливать **одинаковую** скорость передачи битов в секунду (`baud rate`, `bps`) на двух устройствах. Наиболее распространенные скорости являются: **4800, 9600, 19.2 К, 57.6К и 115.2К.**
Данные передаются в виде так называемых `пакетов`, где каждый пакет содержит в себе:
* стартовый и стоповый биты - стартовый бит сигнализирует о поступлении битов данных, стоповый соответственно о конце данных.
* биты данных - пользовательские данные, которые поступают сразу после стартового бита, может содержаться от 5 до 8 битов.
* бит четности - необязательный бит, который идет после битов данных и перед стоповым битом и используется для обнаружения ошибок.
<figure>
<img src="/media/uart-packet.jpg" width="700" height="300" alt="Структура UART пакета" format="webp">
<figcaption>Структура UART пакета</figcaption>
</figure>
Я думаю этой информации вполне достаточно для того, чтобы понимать, что из себя представляет UART. Если вы хотите более подробно изучить этот интерфейс, то в интернете существует множество статей на эту тему.
# Преобразователь интерфейсов
Поскольку скорее всего вы будете подключать микроконтроллер к USB порту компьютера, вам необходим некий переводчик между двумя интерфейсами для отправки и получения данных, так как USB и UART совершенно разные протоколы передачи данных. Таких преобразователей существует множество, вот одни из них: [CH340](https://pdf1.alldatasheet.com/datasheet-pdf/view/1132602/WCH/CH340.html), [CP2102](https://pdf1.alldatasheet.com/datasheet-pdf/view/201067/SILABS/CP2102.html) или [FT232](https://pdf1.alldatasheet.com/datasheet-pdf/view/197629/FTDI/FT232BL.html). Если вы используете отладочную плату Arduino UNO, то у вас уже используется один из этих преобразователей на вашей плате.
<figure>
<img src="/media/arduino-uart-converter.png" width="470" height="320" alt="Преобразователь интерфейсов CH340G на плате Arduino UNO" format="webp"><figcaption>
Преобразователь интерфейсов CH340G на плате Arduino UNO
</figcaption>
</figure>
Если же на вашей отладочной плате не оказалось этого преобразователя интерфейсов, то вы можете приобрести модуль в любом магазине, цена на него обычно не превышает $5.
# Разработка прошивки для микроконтроллера
Как я упомянул ранее, прошивка будет написана для микроконтроллера AVR128DA48. На самом деле неважно какой микроконтроллер вы используете, вам достаточно нужно уметь отправлять и принимать данные используя UART периферию вашего микроконтроллера.
Суть программы будет очень проста: необходимо написать некий ***обработчик команд*** который будет принимать на вход строку (например `led_toggle`) и вызывать необходимую функцию, которая будет выполнять какое-то действие, в данном случае переключать светодиод.
Постановка задачи ясна, теперь рассмотрим реализацию данной программы. Я буду использовать среду разработки MPLAB X IDE и язык C для написания прошивки.
## Необходимые константы
Для начала для всего проекта определим список констант, создав файл `constants.h`:
```c
#ifndef CONSTANTS_H
#define CONSTANTS_H
#define F_CPU 4000000UL
#define BAUD_RATE 9600
#define BUFFER_SIZE 20
#define EOT 0x04
#endif /* CONSTANTS_H */
```
* `F_CPU` - тактовая частота микроконтроллера
* `BAUD_RATE` - скорость передачи данных UART
* `BUFFER_SIZE` - размер буфера, куда будем складывать поступающие данные
* `EOT` - End-of-Transmission, ASCII символ конца передачи данных
## Инициализация USART
Создадим заголовочный файл `usart.h` и определим несколько функций для инициализации USART, отправки символа и отправки строки.
```c
#include "constants.h"
#ifndef USART_H
#define USART_H
#define USART1_BAUD_RATE(BAUD_RATE) ((float)(64 * F_CPU / (16 * (float)BAUD_RATE)) + 0.5)
#include <avr/io.h>
#include <stdio.h>
#include <string.h>
void USART1_Initialize(void);
void USART1_SendChar(char c);
void USART1_SendString(char *str);
#endif /* USART_H */
```
Макрос `USART1_BAUD_RATE` вычисляет значение, которое необходимо записать в регистр `USARTn.BAUD` для установки скорости передачи данных. Формула взята из [документации](https://ww1.microchip.com/downloads/aemDocuments/documents/MCU08/ProductDocuments/DataSheets/AVR128DA28-32-48-64-Data-Sheet-40002183C.pdf) к микроконтроллеру (глава 25.3.2.2.1, таблица 25-1).
Реализуем функцию инициализации USART в файле `usart.c`:
```c
void USART1_Initialize(void) {
/* set baud rate */
USART1.BAUD = (uint16_t) (USART1_BAUD_RATE(BAUD_RATE));
/* set char size in a frame to 8 bit */
USART1.CTRLC = USART_CHSIZE0_bm | USART_CHSIZE1_bm;
/* config pins for TX and RX */
PORTC.DIRSET = PIN0_bm;
PORTC.DIRCLR = PIN1_bm;
/* enable reveice complete interrupt */
USART1.CTRLA = USART_RXCIE_bm;
/* enable transmitter and receiver */
USART1.CTRLB = USART_TXEN_bm | USART_RXEN_bm;
}
```
Для полнодуплексного режима инициализация асинхронной версии USART происходит следующим образом:
* Конфигурация скорости передачи данных путем записывания значения в регистр `USARTn.BAUD`
* Конфигурация размера `фрейма`, в нашем случае это 8 бит.
* Конфигурация пина `TX` на выход и пина `RX` на вход.
* Включение прерывания на то, когда закончился прием данных (когда пришел один пакет).
* Включение приемника и передатчика.
Теперь, рассмотрим функции отправки данных:
```c
void USART1_SendChar(char c) {
while (!(USART1.STATUS & USART_DREIF_bm));
USART1.TXDATAL = c;
}
void USART1_SendString(char *str) {
for (size_t i = 0; i < strlen(str); i++) {
USART1_SendChar(str[i]);
}
}
```
Для того чтобы отправить данные, необходимо записать в регистр `USARTn.TXDATAL` один байт, но перед этим нужно убедиться, что предыдущая передача была завершена путем проверки регистра `USARTn.STATUS`. Как указано в документации, бит `DREIF` (Data Register Empty Flag) является установленным, если данные в TX буфере **отсутствуют**. Соответственно можно сделать пустой цикл который будет выполняться пока у нас есть данные в TX буфере.
Функция отправки строки является некой оберткой, которая будет вызывать функцию отправки одного символа.
## Обработчик команд
Как видно из постановки задачи, на вход нам поступает некая строка, которая говорит о том, какое действие должно выполниться.
Определим заголовочный файл `command.h` и напишем туда следующее:
```c
#ifndef COMMAND_H
#define COMMAND_H
#include <avr/io.h>
#include <string.h>
#define COMMAND_SIZE 5
#define COMMAND_MAX_NAME_LENGTH 20
struct Command {
void *addr;
char name[COMMAND_MAX_NAME_LENGTH];
};
uint8_t command_define(void *fp, char name[COMMAND_MAX_NAME_LENGTH]);
uint8_t command_process(char *cmd_name);
void command_list(void);
#endif /* COMMAND_H */
```
Для решения этой задачи я предлагаю создавать массив структур, где в каждой структуре будет содержаться указатель на функцию которую нам нужно выполнить, и название самой команды.
* `command_define` будет создавать новую структуру в массиве.
* `command_process` будет вызывать соответствующую функцию по имени команды.
* `command_list` будет отправлять по UART информацию о существующих командах.
Реализовать задуманное можно следующим образом создав файл `command.c`:
```c
#include "command.h"
#include "usart.h"
struct Command commands[COMMAND_SIZE];
uint8_t cmd_idx = 0;
uint8_t command_define(void *fp, char name[COMMAND_MAX_NAME_LENGTH]) {
size_t name_length = strlen(name);
if (name_length > COMMAND_MAX_NAME_LENGTH) {
return 0;
}
if (cmd_idx > COMMAND_SIZE) {
cmd_idx = 0;
}
struct Command command;
command.addr = fp;
strcpy(command.name, name);
commands[cmd_idx++] = command;
return 1;
}
uint8_t command_process(char *cmd_name) {
uint8_t i = 0;
do {
if (strcmp(cmd_name, commands[i].name) == 0) {
int (*execute)();
execute = commands[i].addr;
execute();
USART1_SendString("[LOG]: Command completed successfully with code \"0\".\n");
USART1_SendChar(EOT);
return 0;
}
} while (i++ < COMMAND_SIZE);
USART1_SendString("[LOG]: Invalid command name!\n");
USART1_SendChar(EOT);
return 1;
}
void command_list(void) {
USART1_SendString("[LOG]: List of available commands:\n");
for (uint8_t i = 0; i < COMMAND_SIZE; i++) {
if (commands[i].addr) {
USART1_SendChar((i + 1) + '0');
USART1_SendString(". ");
USART1_SendString(commands[i].name);
USART1_SendChar('\n');
}
}
}
```
Важно отметить, что после отправки данных, необходимо отослать символ `EOT`, который будет говорить о том, что передача закончена, это поможет в будущем в разработке веб-приложения.
## Прием данных
Последнее, что необходимо сделать, это принимать команды по USART и отправлять их обработчику. Для этого можно использовать прерывание:
```c
char buffer[BUFFER_SIZE];
volatile uint8_t buff_idx = 0;
ISR(USART1_RXC_vect) {
if (buff_idx > BUFFER_SIZE) buff_idx = 0;
buffer[buff_idx] = USART1.RXDATAL;
buff_idx++;
}
```
После срабатывания прерывания `USART1_RXC_vect`, в регистре `USART1.RXDATAL` появляются данные (один байт). Этот байт мы должны записать в свой буфер с фиксированным размером. Еще есть другой вариант принятия данных не используя прерывание. Для этого достаточно проверять бит `RXCIF` (USART Receive Complete Interrupt Flag) в регистре `USARTn.STATUS`. Этот бит является установленным, когда в приемном буфере присутствуют данные.
Для того того чтобы обработать входящие команды, в главном цикле можно проверять буфер на наличие символа `\n`, этот символ будет означать конец названия команды:
```c
while (1) {
if (buff_idx >= 1) {
if (buffer[buff_idx - 1] == '\n') {
buffer[buff_idx - 1] = '\0';
command_process(buffer);
buff_idx = 0;
}
}
}
```
После нахождения этого символа, можно передать буфер обработчику, затем сбросить индекс буфера в ноль. В итоге файл `main.c` будет выглядеть следующим образом:
```c
#include <avr/io.h>
#include <avr/interrupt.h>
#include <string.h>
#include "constants.h"
#include "command.h"
#include "usart.h"
char buffer[BUFFER_SIZE];
volatile uint8_t buff_idx = 0;
void led_toggle(void);
void hello_world(void);
ISR(USART1_RXC_vect) {
if (buff_idx > BUFFER_SIZE) buff_idx = 0;
buffer[buff_idx] = USART1.RXDATAL;
buff_idx++;
}
void MCU_Init_Ports(void) {
/* led pin PC6 to output */
PORTC.DIRSET = PIN6_bm;
}
int main(void) {
MCU_Init_Ports();
USART1_Initialize();
sei();
command_define(led_toggle, "led_toggle");
command_define(hello_world, "hello");
command_define(command_list, "list");
while (1) {
if (buff_idx >= 1) {
if (buffer[buff_idx - 1] == '\n') {
buffer[buff_idx - 1] = '\0';
command_process(buffer);
buff_idx = 0;
}
}
}
}
void led_toggle(void) {
PORTC.OUTTGL = PIN6_bm;
}
void hello_world(void) {
USART1_SendString("Hello world!\n");
}
```
Обязательно после инициализации портов микроконтроллера и USART нужно включить глобальные прерывания, вызвав функцию `sei`
С помощью функции `command_define` мы можем определить, по какой переданной команде по USART запускать функцию:
* `led_toggle` будет переключать светодиод.
* `hello_world` выводить сообщение “Hello world”.
* `command_list` будет показывать список имеющихся команд.
## Демонстрация работы
Теперь собрав и загрузив прошивку в ваш микроконтроллер, можно проверить работоспособность. Я буду использовать программу `CuteCom` для открытия последовательного порта, вы можете использовать любую другую подобную.
Выберете ваше устройство из списка (в моем случае это `/dev/ttyACM1`) и установите следующие настройки:
* Скорость: **9600** бод
* Количество битов: **8**
* Четность: **None**
* Стоповые биты: **1**
<figure>
<img
src="/media/uart-connect-settings.png"
width="700"
height="400"
alt="Настройки для открытия последовательного порта"
format="webp"
fit="fill"
>
<figcaption>Настройки для открытия последовательного порта</figcaption>
</figure>
Теперь после открытия порта можно отправлять команды, не забудьте установить возврат каретки как `LF`, т.к наша прошивка распознает окончание команды только по этому символу.
<figure>
<img
src="/media/webserial-demo-1.png"
width="700"
height="400"
alt="Пример отправки команды “hello”"
format="webp"
fit="fill"
>
<figcaption>Пример отправки команды “hello”</figcaption>
</figure>
# Разработка веб-приложения
Веб-приложение будет состоять всего из нескольких элементов: поле для ввода, куда мы сможем писать наши команды, кнопки для открытия и закрытия последовательного порта, статус соединения. Готовый вариант можно посмотреть [здесь](https://web-serial-example.netlify.app/).
## Создание разметки HTML и стилей
Разметка HTML будет выглядеть следующим образом:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Web Serial API Example</title>
<link href="./style.css" rel="stylesheet" />
<script defer src="./app.js"></script>
</head>
<body>
<div id="app">
<div>
<button id="connect">Connect</button>
<button id="disconnect">Disconnect</button>
</div>
<form id="terminal_form" action="#">
<label for="input">Enter command name:</label><br />
<div>
<input id="input" name="input" placeholder="e.g led_toggle" type="text" disabled />
<button name="send" type="submit" disabled>Send</button>
</div>
</form>
<textarea id="serial_log" placeholder=">" readonly></textarea>
<div id="port_info">
<div>
<p id="status">not connected</p>
</div>
<div>
<span>vendorId: <span id="vendor_id">-</span>&nbsp;|</span>
<span>deviceId: <span id="product_id">-</span></span>
</div>
</div>
</div>
</body>
</html>
```
Добавим немного стилей создав файл `style.css`:
```css
:root {
--border-color: #c9d1d9;
--border-color-hover: #6e7681;
--font-family: monospace;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
#app {
display: flex;
flex-direction: column;
width: 100%;
max-width: 600px;
row-gap: 5px;
}
html,
body {
box-sizing: border-box;
font-size: 16px;
}
body {
display: flex;
justify-content: center;
margin-top: 2rem;
font-family: var(--font-family);
}
p {
padding: 0;
margin: 5px;
}
input,
textarea {
font-family: var(--font-family);
padding: 5px 15px;
border-radius: 4px;
width: 100%;
border: 1px solid var(--border-color);
}
textarea {
resize: vertical;
min-height: 200px;
line-height: 1.4;
font-size: 16px;
outline: none;
font-size: 12px;
}
label {
font-size: 14px;
font-style: italic;
}
button {
cursor: pointer;
background: none;
font-family: var(--font-family);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 5px 15px;
}
button:hover:not(:disabled) {
border: 1px solid var(--border-color-hover);
}
#terminal_form div {
display: flex;
justify-content: space-between;
gap: 5px;
margin-bottom: 5px;
margin-top: 5px;
}
#port_info {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
font-size: 14px;
}
#port_info #status {
text-transform: uppercase;
}
```
## Создание класса SerialPortHandler
Логика работы с последовательным портом будет реализована в этом классе, который будет небольшой оберткой для удобного использования WebSerial API, он будет иметь всего 5 методов и несколько свойств.
Определим этот класс в файле `app.js`:
```jsx
class SerialPortHandler {
constructor(options, onConnect, onDisconnect) {
this.encoder = new TextEncoder();
this.decoder = new TextDecoder();
this.onConnect = onConnect;
this.onDisconnect = onDisconnect;
this.options = options;
this.port = null;
this.isOpened = false;
this.#setupListeners();
}
async open() {}
async close() {}
async write(data) {}
async read() {}
#setupListeners() {}
}
```
* [`TextEncoder`](https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder) является классом, который кодирует строку в массив без знаковых 8-ми битных целых чисел [`Uint8Array`](https://webidl.spec.whatwg.org/#idl-Uint8Array).
* [`TextDecoder`](https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder#examples) наоборот, декодирует в строку. Экземпляры этих классов нам понадобятся, т.к мы будет оперировать данными с типом `Uint8Array`.
* Функции `onConnect` и `onDisconnect` будут вызываться при подключении или отключении устройства.
* Свойство [`options`](https://wicg.github.io/serial/#serialoptions-dictionary) содержит необходимые параметры, которые будут использоваться для передачи и приема данных.
* Свойство [`port`](https://wicg.github.io/serial/#serialport-interface) - объект, который будет содержать методы для работы с портом и информацию об устройстве после установки соединения.
* Функция `setupListeners` просто будет добавлять обработчики событий.
Далее рассматривается реализация этих методов.
## Открытие последовательного порта
Для открытия последовательного порта для начала необходимо вызвать метод [`requestPort`](https://wicg.github.io/serial/#requestport-method), который откроет специальное окно со списком устройств. Также необходимо, чтобы пользователь сам активировал вызов этого окна, иначе будет ошибка. Это как раз можно сделать по событию нажатия кнопки.
```jsx
async open() {
try {
const port = await navigator.serial.requestPort();
await port.open(this.options);
this.port = port;
this.isOpened = true;
return this.port.getInfo();
} catch (error) {
console.error(error);
throw error;
}
}
```
В функцию `requestPort` в качестве параметра можно передать необязательный параметр фильтр, который ограничит список выбираемых портов в соответствии с USB идентификатором:
```jsx
const port = await navigator.serial.requestPort({
filters: [{ usbVendorId: 0x7522 }],
});
```
Список USB идентификаторов можно посмотреть на [этом](https://devicehunt.com/all-usb-vendors) сайте.
<figure>
<img
src="/media/webserial-connect.png"
width="700"
height="400"
alt="Выбор устройства через специальное окно"
format="webp"
fit="fill"
>
<figcaption>Выбор устройства через специальное окно</figcaption>
</figure>
После того как пользователь выбрал устройство, метод `requestPort` возвращает [`port`](https://wicg.github.io/serial/#serialport-interface), который можно открыть передав туда [параметры](https://wicg.github.io/serial/#serialoptions-dictionary):
* `baudRate` : скорость передачи данных.
* `dataBits` : количество бит данных во фрейме (7 или 8).
* `stopBits` : количество стоповых битов в конце пакета (1 или 2).
* `parity` : режим четности ( `none` , `even` или `odd` ).
* `bufferSize` : размер буферов чтения и записи, которые должны быть созданы.
* `flowControl` : режим управления потоком ( `none` или `hardware` ).
Свойство `baudRate` является единственным обязательным параметром, остальные же являются необязательными и имеют [значения по умолчанию](https://wicg.github.io/serial/#serialoptions-dictionary).
В конце наш метод возвращает [информацию](https://wicg.github.io/serial/#dom-serialportinfo) о подключенном устройстве используя метод [`getInfo`](https://wicg.github.io/serial/#dom-serialport-getinfo).
## Чтение данных
Отмечу, что WebSerial API является асинхронным, что позволяет предотвращать блокировку пользовательского интерфейса во время принятия данных.
Еще важно знать, что WebSerial API использует потоки [Stream API](https://streams.spec.whatwg.org/). При потоковой передаче данные разбиваются на `фрагменты` `(chunks)`, это позволяет обрабатывать данные без полного ожидания их поступления.
Получение данных можно реализовать следующим образом:
```jsx
async read() {
while (this.port.readable) {
const reader = this.port.readable.getReader();
let chunks = '';
try {
while (true) {
const { value, done } = await reader.read();
const decoded = this.decoder.decode(value);
chunks += decoded;
if (done || decoded.includes(EOT)) {
console.log('Reading done.');
reader.releaseLock();
break;
}
}
return chunks;
} catch (error) {
console.error(error);
throw error;
} finally {
reader.releaseLock();
}
}
}
```
Когда пользователь подключился к устройству, `this.port` будет иметь такие свойства как `writable` и `readable`, которые являются экземплярами классов [WritableStream](https://streams.spec.whatwg.org/#writablestream) и [ReadableStream](https://streams.spec.whatwg.org/#readablestream). Они нам понадобятся для чтения и записи данных.
Внешний цикл `while` предназначен для проверки ошибок, такие как проверка четности. При возникновении фатальной ошибки, свойство `readable` станет `null`.
Метод [`getReader`](https://streams.spec.whatwg.org/#rs-get-reader) создает [читатель](https://streams.spec.whatwg.org/#reader), который даст возможность получать данные фрагментами. Доступный для чтения поток одновременно имеет не более одного читателя, соответственно после этого поток является **[заблокированным](https://streams.spec.whatwg.org/#lock)**, но читатель остается **активным**.
Внутренний цикл `while` читает данные используя метод `read` у читателя. Этот метод возвращает свойство `value`, который имеет данные c типом `UInt8Array`, и свойство `done`, которое станет `true`, если последовательное устройство больше не передает никаких данных. Далее мы декодируем данные в строку и записываем в переменную `chunks`. Так же идет проверка на наличие символа `EOT` (End-of-Transmission), который означает конец передачи данных. Определите этот символ в начале файла `app.js` как глобальную переменную:
```jsx
var EOT = "\u0004";
```
После конца приема данных необходимо разблокировать поток, вызвав метод `releaseLock` для того чтобы сделать читателя неактивным.
## Запись данных
Для того чтобы отправить данные в последовательное устройство, необходимо создать [писателя](https://streams.spec.whatwg.org/#writer), который предоставит возможность отправлять фрагменты в записывающий поток:
```jsx
async write(data) {
const writer = this.port.writable.getWriter();
const encoded = this.encoder.encode(data);
await writer.write(encoded);
writer.releaseLock();
}
```
Перед отправкой нужно закодировать вашу строку в данные с типом `UInt8Array`. После создания писателя записывающий поток так же является заблокированным, соответственно после записи необходимо этот поток разблокировать используя тот же метод `releaseLock`.
## Закрытие порта
Для закрытия порта, когда коммуникация с устройством больше не требуется можно использовать метод [`close`](https://wicg.github.io/serial/#dom-serialport-close):
```jsx
async close() {
await this.port.close();
this.isOpened = false;
}
```
Порт невозможно закрыть когда записывающий или читающий поток является заблокированным.
## Обработка событий
API дает возможность подписываться на события, когда подключается доверенное устройство или когда оно отключается:
```jsx
#setupListeners() {
navigator.serial.addEventListener('connect', this.onConnect);
navigator.serial.addEventListener('disconnect', this.onDisconnect);
}
```
## Использование созданного класса
Теперь можно написать front-end часть веб-приложения используя наш созданный класс `SerialPortHandler`:
```jsx
class Application {
constructor(root) {
if (!("serial" in navigator)) {
console.error("Web Serial API is not supported in your browser.");
return;
}
this.serialPortHandler = new SerialPortHandler(
{ baudRate: 9600 },
() => console.log("Device connected."),
() => {
console.log("Device disconnected.");
this.#disconnectHandler();
}
);
/**
* DOM Elements
*/
this.$root = root;
this.$connectButton = this.$root.querySelector("#connect");
this.$disconnectButton = this.$root.querySelector("#disconnect");
this.$terminalForm = this.$root.querySelector("#terminal_form");
this.$serialLog = this.$root.querySelector("#serial_log");
this.$status = this.$root.querySelector("#status");
this.$vendorId = this.$root.querySelector("#vendor_id");
this.$productId = this.$root.querySelector("#product_id");
this.#setupEvents();
}
/**
* Handlers for connecting, disconnecting and sending a command
*/
#setupEvents() {
this.$connectButton.addEventListener("click", this.#connectHandler.bind(this));
this.$disconnectButton.addEventListener("click", this.#disconnectHandler.bind(this));
this.$terminalForm.addEventListener("submit", this.#submitHandler.bind(this));
}
/**
* Open serial port and notify user of connection status
* @returns {Promise<void>}
*/
async #connectHandler() {
try {
if (this.serialPortHandler.isOpened) return;
const info = await this.serialPortHandler.open();
console.log("Port opened: ", info);
this.$terminalForm.elements.input.removeAttribute("disabled");
this.$terminalForm.elements.send.removeAttribute("disabled");
this.$vendorId.textContent = "0x" + info.usbVendorId.toString(16);
this.$productId.textContent = "0x" + info.usbProductId.toString(16);
this.$status.textContent = "CONNECTED";
} catch (error) {
this.$status.textContent = "ERROR";
}
}
/**
* Closes the serial port and updates the connection status.
* @returns {Promise<void>}
*/
async #disconnectHandler() {
if (!this.serialPortHandler.isOpened) return;
await this.serialPortHandler.close();
this.$terminalForm.elements.input.setAttribute("disabled", "true");
this.$terminalForm.elements.send.setAttribute("disabled", "true");
this.$vendorId.textContent = "-";
this.$productId.textContent = "-";
this.$status.textContent = "NOT CONNECTED";
}
/**
* Writes data to the serial port and reads the response
* @param {SubmitEvent} e - Form submit event
*/
async #submitHandler(e) {
e.preventDefault();
const $form = e.target;
const data = $form.elements.input.value;
$form.reset();
if (this.serialPortHandler.isOpened && data) {
this.$serialLog.innerHTML += ">" + data + "\n";
await this.serialPortHandler.write(data + "\n");
const message = await this.serialPortHandler.read();
this.$serialLog.textContent += message.replaceAll(EOT, "");
console.log("Message received: \n" + message);
}
this.$serialLog.scrollTo(0, this.$serialLog.scrollHeight);
}
}
```
В основном здесь происходит работа с обновлением интерфейса и я думаю не стоит подробно разъяснять логику, отмечу только ключевые моменты:
В конструкторе этого класса можно определить, поддерживает ли браузер WebSerial API, проверив ключ `serial` в объекте `navigator` и если нет, то пишем ошибку в консоль. Объект `this.serialPortHandler` будет являться экземпляром нашего созданного класса `SerialPortHandler`, в качестве аргументов передаем скорость **9600** и функции, которые будут являться обработчиками событий подключения и отключения устройства. Далее ищем необходимые DOM элементы и добавляем на некоторые из них прослушиватель событий.
Метод `#submitHandler` предназначен для отправки команд и принятия результата выполнения команды. При использовании метода `write` обязательно нужно в конце добавить символ `\n`, так программа в микроконтроллере узнает, где заканчивается название команды.
Инициализируйте приложение в конце файла `app.js`:
```js
new Application(document.getElementById("app"));
```
## Проверка работы веб-приложения
Теперь можно отсылать команды микроконтроллеру:
<figure>
<img
src="/media/webserial-demo-2.png"
width="550"
height="300"
alt="Готовый результат веб-приложения"
format="webp"
>
<figcaption>Готовый результат веб-приложения</figcaption>
</figure>
Ради забавы можно помигать светодиодом:
```js
const app = new Application(document.getElementById("app"));
setInterval(async () => {
if (app.serialPortHandler.isOpened) {
await app.serialPortHandler.write("led_toggle" + "\n");
const message = await app.serialPortHandler.read();
console.log(message);
}
}, 500);
```
<figure>
<img src="/media/webserial-demo-3.gif" loading="lazy" alt="мигание светодиодом" />
<figcaption>Мигание светодиодом</figcaption>
</figure>
# Заключение
Разработав веб-приложение мы убедились, что использование WebSerial API довольно простое. Надеюсь, что эта статья пригодится вам и поможет в разработке вашего проекта.
# Полезные материалы
* [Статус поддержки браузеров Can I use](https://caniuse.com/web-serial)
* [Документация к WebSerial API](https://wicg.github.io/serial/)
* [Подробное руководство по Stream API](https://web.dev/streams/)
* [Список USB идентификаторов](https://devicehunt.com/all-usb-vendors)
* [Доступ к различным устройствам из браузера](https://web.dev/devices-introduction/)

View File

@ -1,611 +0,0 @@
---
author: Сергей Ярков
title: Реализация задержек в AVR микроконтроллере
description: Очень часто в программах приходится выполнять задержку. Например
при реализации какого либо протокола необходимо в точности выдерживать
тайминги, или же необходимо выполнять какую либо логику раз в несколько часов
или дней. В этой статье рассмотрены несколько способов реализации задержек.
tags:
- AVR
- Tutorial
- Microcontrollers
pubDate: 2023-10-15T16:28:52.587Z
updatedDate: 2023-10-15T16:28:52.601Z
---
# Способы реализации
Существует два способа как можно реализовать задержку - это использовать аппаратные таймеры микроконтроллера или использовать простой цикл который выполняется определенное количество тактов. Преимущество использования аппаратного таймера микроконтроллера перед обычным программным циклом заключается в неблокирующем выполнении программы за счет того что работа таймера-счетчика не зависит от выполнения основной программы. Но иногда нужно использовать задержку всего в несколько микросекунд, что особо не повлияет на работу программы в некоторых случаях, тогда можно посмотреть в сторону простых циклов. Будет рассмотрен вариант и с длительной задержкой в несколько часов или дней. Программы будут представлены на языке Си и Ассемблера для микроконтроллера AVR ATtiny45V. В качестве основной программы будет моргающий светодиод который переключается раз в 0.5 секунд.
## Используем цикл
Для реализации этого способа задержки для начала нужно посчитать кол-во тактов процессора для выполнения задержки в N секунд и затем сделать цикл. Например для МК с частотой в 8 MHz один такт будет равен `1/8 = 0.125uS`. Значит чтобы рассчитать кол-во тактов например в 0.5 секунд то это будет `(0.5 × 1,000,000) × 8 = 4,000,000` тактов, т.е половина от частоты МК.
Реализация цикла задержки на ассемблере:
```asmatmel
.MACRO delay16
ldi delay8_r, LOW(@0 * F_CPU / 4 - 3) ; 1
ldi delay16_r, HIGH(@0 * F_CPU / 4 - 3) ; 1
rcall DELAY16_LOOP ; 3
.ENDMACRO
DELAY16_LOOP:
subi delay8_r, 1 ; 1
sbci delay16_r, 0 ; 1
brcc DELAY16_LOOP ; 1 = false, 2 = true
ret ; 4
```
Каждая инструкция в микроконтроллере выполняется за определенное кол-во тактов (комментарии в коде выше). Например инструкция `ldi` выполняется за один такт, а инструкция `brcc` выполняется за два такта - когда условие верно и за один такт когда условие неверно. На кол-во тактов так же влияет и разрядность программного счетчика (см. [AVR Instruction Set](http://ww1.microchip.com/downloads/en/devicedoc/atmel-0856-avr-instruction-set-manual.pdf)).
### Пояснения к программе
В данной реализации используется макрос `delay16` который загружает кол-во тактов в два 8-ми битных регистра которые помечены как младший и старший. Затем вызывается подпрограмма выполнения цикла. Зачем нужно в расчете тактов делить полученное число на 4? Посмотрите на подпрограмму `DELAY16_LOOP` которая выполняет декрементирование 16-ти битного числа, вы увидите что одна итерация цикла занимает 4 такта. От полученного числа отнимаем 3 для компенсации времени.
В итоге максимальная задержка которая может получиться при работе МК в 8 MHz вызвав этот макрос равняется `65535/8 * 4 = 32767.5uS` что не совсем подходит для мигания светодиодом. Что бы сделать задержку дольше, можно использовать 24 бита для хранения кол-ва тактов. Тогда максимальная задержка составит `16,777,216/8 * 5 = 10.48сек`. В итоге получается такая программа с переключающимся светодиодом раз в 0.5 секунд:
```asmatmel
.EQU F_CPU = 8 ; MHz
.DEF t1 = r16
.DEF t2 = r17
.DEF delay24_r = r18
.DEF delay16_r = r19
.DEF delay8_r = r20
.EQU LED_DDR = DDRB
.EQU LED_PORT = PORTB
.EQU LED_PIN_DATA = PINB
.EQU LED_PIN = PB0
.MACRO delay24
ldi delay8_r, LOW(@0 * F_CPU / 5 - 3)
ldi delay16_r, HIGH(@0 * F_CPU / 5 - 3)
ldi delay24_r, BYTE3(@0 * F_CPU / 5 - 3)
rcall DELAY24_LOOP
.ENDMACRO
.CSEG
.ORG 0x00
rjmp RESET_vect
RESET_vect:
ldi t1, LOW(RAMEND)
out SPL, t1
INIT:
ldi t1, (1<<LED_PIN)
out LED_DDR, t1
clr t1
out LED_PORT, t1
LOOP:
nop
delay24 500000
nop
rcall LED_TOGGLE
rjmp LOOP
LED_TOGGLE:
push t1
push t2
in t1, LED_PIN_DATA
ldi t2, (1<<LED_PIN)
eor t1, t2
out LED_PORT, t1
pop t2
pop t1
ret
DELAY24_LOOP:
subi delay8_r, 1
sbci delay16_r, 0
sbci delay24_r, 0
brcc DELAY24_LOOP
ret
```
Выполнить компиляцию:
```bash
avrasm2 -fI ./main.asm
```
Если использовать 32 бита для хранения кол-ва тактов то максимальная задержка уже составит 3221 секунда! Также вместо регистров общего назначения можно использовать ячейки, определенные в оперативной памяти (SRAM).
Чтобы проверить точно ли выполняется задержка, воспользуемся симулятором который встроен в IDE MPLAB X. Перед этим необходимо настроить частоту работы МК в конфигурации проекта:
<figure>
<img src="/media/mplabx-ide-sim-option.png" alt="Выбор частоты выполнения инструкций для симулятора">
<figcaption>Выбор частоты выполнения инструкций для симулятора</figcaption>
</figure>
Установим точки останова на холостые операции `nop` до и после вызова макроса и выполним отладку:
![Отладка программы](/media/screen-1.png)
До вызова инструкции `nop` прошло 8 тактов или 1 микросекунда. Очищаем Stopwatch и нажимаем кнопку продолжения программы (F5):
![Отладка программы](/media/screen-2.png)
Можно видеть что после выхода из подпрограммы `DELAY24_LOOP` прошло ровно 500 миллисекунд.
Для языка Си все будет довольно просто. Библиотека [AVR Libc](https://www.nongnu.org/avr-libc/) поставляется с набором утилит которые упрощают программирование. Поэтому нам достаточно указать частоту МК и вызвать определенную функцию. Эта функция как раз из рассчитает нужное кол-во тактов процессора и выполнит цикл.
Код основной программы на языке Си:
```c
#define F_CPU 8000000
#include <avr/io.h>
#include <util/delay.h>
#define PIN_LED PB0
int main(void) {
DDRB |= _BV(PIN_LED);
PORTB = 0x00;
for(;;) {
_delay_ms(500);
PORTB ^= _BV(PIN_LED);
}
return 0;
}
```
Выполнить компиляцию:
```c
avr-gcc -Wall -mmcu=attiny45 -Os main.c
avr-objcopy -j .text -j .data -O ihex a.out main.hex
```
## Используем таймеры
Что если программе не нужно затрачивать процессорное время на выполнение цикла задержки? Как отмечалось ранее работа таймера-счетчика не зависит от выполнения программы. Счетчик может инкрементироваться хоть раз в секунду при работе процессора с частотой в 8MHz. Существует несколько способов как сделать задержку используя 8-ми или 16-ти битные таймеры. Я опишу некоторые из них.
### 8-ми битный таймер с предделителем 16384
Для микроконтроллера ATtiny45V имеются только 8-ми битные таймеры, причем у одного из них (TIMER1) имеется делитель частоты на 16384 что очень удобно:
<figure>
<img src="/media/screen-3.png" alt="Таблица выбора предделителя для таймера-счетчика 1">
<figcaption>Таблица выбора предделителя для таймера-счетчика 1</figcaption>
</figure>
Будем использовать таймер в CTC (Clear on Timer Compare match) режиме. Итоговая частота работы таймера-счетчика будет равна: `8MHz/16384 = 488.28Hz`. Один тик таймера будет равняться `1/488.28 = 2048uS`. Значит для 0.5 секунд значение счетчика будет равняться `0.5×488.38 = 244.14` тиков что достаточно для 8-бит. Будем записывать число в регистр сравнения `OCRn` и генерировать прерывание при совпадении.
Программа на ассемблере:
```asmatmel
.DEF t1 = r16
.DEF t2 = r17
.EQU LED_DDR = DDRB
.EQU LED_PORT = PORTB
.EQU LED_PIN_DATA = PINB
.EQU LED_PIN = PB0
.CSEG
.ORG 0x00
rjmp RESET_vect
.ORG 0x03
rjmp TIMER1_COMPA
RESET_vect:
ldi t1, LOW(RAMEND)
out SPL, t1
INIT:
ldi t1, (1<<LED_PIN)
out DDRB, t1
clr t1
out LED_PORT, t1
; ========= Setup 8-bit timer-1 in CTC mode with 16384 prescaler =============
; CPU Clock Freq = 8MHz
; Freq(t) = 8MHz / 16384 = 488.28125 Hz
; Tick = 1 / Freq(t) = 1 / 488.28125 = 2048uS (0.002048 sec)
; Ticks = 0.5 sec / 0.002048 sec = 244
; ============================================================================
ldi t1, (1<<CTC1) | (1<<CS13) | (1<<CS12) | (1<<CS11) | (1<<CS10)
out TCCR1, t1 ; setup timer
clr t1 ; clear timer counter register
out TCNT1, t1
ldi t1, 244 ; 0.5 sec
out OCR1A, t1
out OCR1C, t1 ; for clearing timer on compare match
ldi t1, (1<<OCIE1A) ; enable output compare A interrupt
out TIMSK, t1
sei
LOOP:
nop
rjmp LOOP
LED_TOGGLE:
push t1
push t2
in t1, LED_PIN_DATA
ldi t2, (1<<LED_PIN)
eor t1, t2
out LED_PORT, t1
pop t2
pop t1
ret
TIMER1_COMPA:
push t1
in t1, SREG
rcall LED_TOGGLE
out SREG, t1
pop t1
reti
```
Программа на языке Си:
```c
#define F_CPU 8000000
#include <avr/interrupt.h>
#include <avr/io.h>
#define LED_DDR DDRB
#define LED_PORT PORTB
#define LED_PIN_DATA PINB
#define LED_PIN PB0
/*
* Timer Counter 1
* Mode: CTC
* Prescaler: 16384
*/
void setup_tc1(void) {
TCCR1 |= _BV(CTC1) | _BV(CS13) | _BV(CS12) | _BV(CS11) | _BV(CS10);
TCNT1 = 0x00;
OCR1A = 244;
OCR1C = 244;
TIMSK |= _BV(OCIE1A);
}
void setup_ports(void) {
DDRB |= _BV(LED_PIN);
PORTB = 0x00;
}
/*
* Interrupt every 0.5 sec
*/
ISR(TIMER1_COMPA_vect) {
LED_PORT ^= _BV(LED_PIN);
}
void system_init(void) {
setup_ports();
setup_tc1();
sei();
}
int main(void) {
system_init();
for (;;) {
asm("nop");
}
return 0;
}
```
### 8-ми битный таймер с предделителем 1024
Что если доступен предделитель частоты только на 1024? Тогда регистр для сравнения `OCRn` будет равняться 3906, что не подходит для 8-ми битного регистра. Как вариант можно считать количество переполнений таймера. `3906.25/255 = 15` переполнений.
Программа на ассемблере:
```asmatmel
.DEF t1 = r16
.DEF t2 = r17
.DEF tc1_ovf_cnt = r18
.EQU LED_DDR = DDRB
.EQU LED_PORT = PORTB
.EQU LED_PIN_DATA = PINB
.EQU LED_PIN = PB0
.EQU TC1_OVF_COMPARE_VAL = 15
.CSEG
.ORG 0x00
rjmp RESET_vect
.ORG 0x04
rjmp TIMER1_OVF
RESET_vect:
ldi t1, LOW(RAMEND)
out SPL, t1
INIT:
ldi t1, (1<<LED_PIN)
out DDRB, t1
clr t1
out LED_PORT, t1
; ========= Setup 8-bit timer-1 in Normal mode with 1024 prescaler =======================
; CPU Clock Freq = 8MHz
; Freq(t) = 8MHz / 1024 = 7812.5 Hz
; Tick = 1 / Freq(t) = 1 / 7812.5 = 128uS (0.000128 sec)
; Ticks = 0.5 sec / 0.000128 sec = 3906.25
; Overflows count = 3906 / 255 = 15
; ======================================================================================
ldi t1, (1<<CS13) | (1<<CS11) | (1<<CS10)
out TCCR1, t1 ; setup timer
clr t1 ; clear timer counter register
out TCNT1, t1
ldi t1, (1<<TOIE1) ; enable overflow interrupt
out TIMSK, t1
sei
LOOP:
nop
rjmp LOOP
LED_TOGGLE:
push t1
push t2
in t1, LED_PIN_DATA
ldi t2, (1<<LED_PIN)
eor t1, t2
out LED_PORT, t1
pop t2
pop t1
ret
TIMER1_OVF:
push t1
in t1, SREG
inc tc1_ovf_cnt
cpi tc1_ovf_cnt, TC1_OVF_COMPARE_VAL
brne PC+3
rcall LED_TOGGLE
clr tc1_ovf_cnt ; clear timer overflow counter register
out SREG, t1
pop t1
reti
```
Программа на языке Си:
```c
#define F_CPU 8000000
#include <avr/interrupt.h>
#include <avr/io.h>
#define TC1_OVF_COMPARE_VAL 15
#define LED_DDR DDRB
#define LED_PORT PORTB
#define LED_PIN_DATA PINB
#define LED_PIN PB0
static volatile uint8_t tc1_ovf_cnt = 0;
/*
* Timer Counter 1
* Mode: Normal
* Prescaler: 1024
*/
void setup_tc1(void) {
TCCR1 |= _BV(CS13) | _BV(CS11) | _BV(CS10);
TCNT1 = 0x00;
TIMSK |= _BV(TOIE1);
}
void setup_ports(void) {
DDRB |= _BV(LED_PIN);
PORTB = 0x00;
}
ISR(TIMER1_OVF_vect) {
tc1_ovf_cnt++;
if (tc1_ovf_cnt == TC1_OVF_COMPARE_VAL) {
LED_PORT ^= _BV(LED_PIN);
tc1_ovf_cnt = 0;
}
}
void system_init(void) {
setup_ports();
setup_tc1();
sei();
}
int main(void) {
system_init();
for (;;) {
asm("nop");
}
return 0;
}
```
### Длительная задержка
Бывают случаи когда нужно выполнять какую либо логику раз в несколько часов или дней. Решением является завести глобальную переменную где будем хранить количество прошедших секунд с момента начала отсчета таймера, а в прерывании переполнения таймера будем уже сравнивать значение счетчика с заданным числом. Следующая программа переключает светодиод раз в 6 часов.
```asmatmel
.DEF t1 = r16
.DEF t2 = r17
.DEF tc1_ovf_cnt = r18
.EQU LED_DDR = DDRB
.EQU LED_PORT = PORTB
.EQU LED_PIN_DATA = PINB
.EQU LED_PIN = PB0
.EQU TC1_OVF_COMPARE_VAL = 30
.DSEG
.ORG SRAM_START
ellapsed_seconds: .BYTE 2
.CSEG
.ORG 0x00
rjmp RESET_vect
.ORG 0x04
rjmp TIMER1_OVF
RESET_vect:
ldi t1, LOW(RAMEND)
out SPL, t1
INIT:
ldi t1, (1<<LED_PIN)
out DDRB, t1
clr t1
out LED_PORT, t1
sts ellapsed_seconds, t1
sts ellapsed_seconds+1, t1
; ========= Setup 8-bit timer-1 in Normal mode with 1024 prescaler =======================
; CPU Clock Freq = 8MHz
; Freq(t) = 8MHz / 1024 = 7812.5 Hz
; Tick = 1 / Freq(t) = 1 / 7812.5 = 128uS (0.000128 sec)
; Ticks = 1 sec / 0.000128 sec = 7812,5
; Overflows count = 7812 / 255 = 30
; ======================================================================================
ldi t1, (1<<CS13) | (1<<CS11) | (1<<CS10)
out TCCR1, t1 ; setup timer
clr t1 ; clear timer counter register
out TCNT1, t1
ldi t1, (1<<TOIE1) ; enable overflow interrupt
out TIMSK, t1
sei
LOOP:
nop
rjmp LOOP
LED_TOGGLE:
push t1
push t2
in t1, LED_PIN_DATA
ldi t2, (1<<LED_PIN)
eor t1, t2
out LED_PORT, t1
pop t2
pop t1
ret
TIMER1_OVF:
push t1
in t1, SREG
inc tc1_ovf_cnt
cpi tc1_ovf_cnt, TC1_OVF_COMPARE_VAL
brne _TIMER1_OVF_EXIT
clr tc1_ovf_cnt ; clear timer overflow counter register
lds r24,ellapsed_seconds
lds r25,ellapsed_seconds+1
adiw r24, 1
sts ellapsed_seconds+1, r25
sts ellapsed_seconds, r24
cpi r24, LOW(21600)
sbci r25, HIGH(21600)
brne _TIMER1_OVF_EXIT
rcall LED_TOGGLE
clr r25
sts ellapsed_seconds, r25 ; clear ellapsed seconds
sts ellapsed_seconds+1, r25 ; -----------------------
_TIMER1_OVF_EXIT:
out SREG, t1
pop t1
reti
```
Программа на языке Си:
```c
#include <avr/interrupt.h>
#include <avr/io.h>
#define TC1_OVF_COMPARE_VAL 30
#define LED_DDR DDRB
#define LED_PORT PORTB
#define LED_PIN_DATA PINB
#define LED_PIN PB0
static volatile uint8_t tc1_ovf_cnt = 0;
static volatile uint16_t ellapsed_seconds = 0;
/*
* Timer Counter 1
* Mode: Normal
* Prescaler: 1024
*/
void setup_tc1(void) {
TCCR1 |= _BV(CS13) | _BV(CS11) | _BV(CS10);
TCNT1 = 0x00;
TIMSK |= _BV(TOIE1);
}
void setup_ports(void) {
DDRB |= _BV(LED_PIN);
PORTB = 0x00;
}
ISR(TIMER1_OVF_vect) {
tc1_ovf_cnt++;
if (tc1_ovf_cnt == TC1_OVF_COMPARE_VAL) {
tc1_ovf_cnt = 0;
ellapsed_seconds++;
/* Every 6 hours */
if (ellapsed_seconds == 21600) {
LED_PORT ^= _BV(LED_PIN);
ellapsed_seconds = 0;
}
}
}
void system_init(void) {
setup_ports();
setup_tc1();
sei();
}
int main(void) {
system_init();
for (;;) {
asm("nop");
}
return 0;
}
```
# Заключение
Рассмотрены два основных способа реализации задержки используя таймеры и циклы. Выяснили что используя таймер-счетчик можно сделать задержку, которая не будет блокировать основной поток выполнения программы. Для длительных задержек, где необходимо отслеживать прошедшее время в секундах или часах, можно использовать глобальные переменные.
# Полезные материалы
- [AVR Instruction Set](https://ww1.microchip.com/downloads/en/devicedoc/atmel-0856-avr-instruction-set-manual.pdf)
- [AVR130: Setup and Use of AVR Timers](https://ww1.microchip.com/downloads/en/Appnotes/Atmel-2505-Setup-and-Use-of-AVR-Timers_ApplicationNote_AVR130.pdf)
- [AVR Delay Loop Calculator](http://darcy.rsgc.on.ca/ACES/TEI4M/AVRdelay.html)
- [AVR Timer Calculator](https://eleccelerator.com/avr-timer-calculator/)

View File

@ -1,15 +0,0 @@
---
draft: true
author: Сергей Ярков
title: Управление сдвиговым регистром 74HC595 микроконтроллером
description: В этом туториале рассматривается то, как управлять сдвиговым
регистром 74HC595 в связке с микроконтроллером AVR.
tags:
- AVR
- Digital Electronics
- Tutorial
pubDate: 2023-02-14T19:23:55.622Z
updatedDate: 2023-02-14T19:23:55.646Z
---
...

View File

@ -1,60 +0,0 @@
import { defineCollection, z } from "astro:content";
const blog = defineCollection({
schema: z.object({
title: z.string(),
draft: z.boolean().nullable().optional(),
coverImage: z.string().nullable().optional(),
author: z.string(),
tags: z.string().array(),
description: z.string(),
pubDate: z
.string()
.or(z.date())
.transform((val) => new Date(val)),
updatedDate: z
.string()
.or(z.date())
.optional()
.transform((str) => (str ? new Date(str) : undefined)),
}),
});
const tags = defineCollection({
schema: z.object({
title: z.string(),
}),
});
const authors = defineCollection({
schema: z.object({
firstName: z.string(),
lastName: z.string(),
}),
});
const projects = defineCollection({
schema: z.object({
icon: z.string(),
title: z.string(),
year: z.number(),
description: z.string(),
sourceUrl: z.string(),
demoUrl: z.string().nullable().optional(),
articleUrl: z.string().nullable().optional(),
}),
});
const skills = defineCollection({
schema: z.object({
title: z.string(),
list: z
.object({
icon: z.string().optional(),
title: z.string(),
})
.array(),
}),
});
export const collections = { blog, tags, authors, projects, skills };

View File

@ -1,9 +0,0 @@
---
icon: round-shop-two
title: digital-store
year: 2019
description: Customizable online store with control panel for selling digital goods
sourceUrl: https://github.com/sergeyyarkov/digital-store/tree/digital-store-docker
demoUrl: http://urls.yarkov.tech/digital-store
articleUrl: null
---

View File

@ -1,9 +0,0 @@
---
icon: round-school
title: educt
year: 2022
description: Learning Management System (LMS) to publish your courses, lectures, lessons
sourceUrl: https://github.com/sergeyyarkov/educt
demoUrl: http://urls.yarkov.tech/educt
articleUrl: null
---

View File

@ -1,9 +0,0 @@
---
icon: chip
title: memory-led-game
year: 2022
description: Analogue of the popular game "Simon says" on the AVR microcontroller
sourceUrl: https://github.com/sergeyyarkov/attiny24a_memory-led-game
demoUrl: null
articleUrl: null
---

View File

@ -1,9 +0,0 @@
---
icon: nodejs
title: node-docker-example
year: 2022
description: An example of using Docker to deploy a Node.js application
sourceUrl: https://github.com/sergeyyarkov/node-docker-example
demoUrl: http://urls.yarkov.tech/node-docker-example
articleUrl: null
---

View File

@ -1,9 +0,0 @@
---
icon: chat
title: react-chat-app
year: 2021
description: Client-server web chat application developed with Node.js and Socket.io
sourceUrl: https://github.com/sergeyyarkov/react-chat-app
demoUrl: null
articleUrl: null
---

View File

@ -1,7 +0,0 @@
---
icon: chip
title: thermometer
year: 2023
description: A simple thermometer on an AVR microcontroller with a DS18B20 sensor
sourceUrl: https://github.com/sergeyyarkov/attiny45v_thermometer
---

View File

@ -1,10 +0,0 @@
---
icon: serial
title: web-serial-example
year: 2022
description: An example of using the Web Serial API to interact with the
microcontroller through a browser
sourceUrl: https://github.com/sergeyyarkov/avr128da48_web-serial-example
demoUrl: https://web-serial-example.netlify.app
articleUrl: https://yarkov.tech/en/blog/2022-11-04/using-the-web-serial-api-to-communicate-with-the-microcontroller/
---

View File

@ -1,10 +0,0 @@
---
icon: book
title: yarkov.tech
year: 2023
description: My personal website where you can find information about me and a
list of various articles and tutorials...
sourceUrl: https://github.com/sergeyyarkov/yarkov.tech
demoUrl: https://yarkov.tech
articleUrl: null
---

View File

@ -1,9 +0,0 @@
---
icon: round-shop-two
title: digital-store
year: 2019
description: Настраиваемый интернет-магазин с панелью управления для продажи
цифровых товаров
sourceUrl: https://github.com/sergeyyarkov/digital-store/tree/digital-store-docker
demoUrl: http://urls.yarkov.tech/digital-store
---

View File

@ -1,8 +0,0 @@
---
icon: round-school
title: educt
year: 2022
description: Система управления обучением (LMS) для публикации ваших курсов, лекций, уроков
sourceUrl: https://github.com/sergeyyarkov/educt
demoUrl: http://urls.yarkov.tech/educt
---

View File

@ -1,7 +0,0 @@
---
icon: chip
title: memory-led-game
year: 2022
description: Аналог популярной игры "Саймон говорит" на AVR микроконтроллере
sourceUrl: https://github.com/sergeyyarkov/attiny24a_memory-led-game
---

View File

@ -1,8 +0,0 @@
---
icon: nodejs
title: node-docker-example
year: 2022
description: Пример использования Docker для развертывания Node.js приложения
sourceUrl: https://github.com/sergeyyarkov/node-docker-example
demoUrl: http://urls.yarkov.tech/node-docker-example
---

View File

@ -1,8 +0,0 @@
---
icon: chat
title: react-chat-app
year: 2021
description: Клиент-серверное веб приложение чата разработанное при помощи
Node.js и Socket.io
sourceUrl: https://github.com/sergeyyarkov/react-chat-app
---

View File

@ -1,7 +0,0 @@
---
icon: chip
title: thermometer
year: 2023
description: Простой термометр на AVR микроконтроллере с датчиком DS18B20
sourceUrl: https://github.com/sergeyyarkov/attiny45v_thermometer
---

View File

@ -1,10 +0,0 @@
---
icon: serial
title: web-serial-example
year: 2022
description: Пример использования Web Serial API для взаимодействия с
микроконтроллером через браузер.
sourceUrl: https://github.com/sergeyyarkov/avr128da48_web-serial-example
demoUrl: https://web-serial-example.netlify.app
articleUrl: https://yarkov.tech/blog/2022-11-04/ispolzovanie-web-serial-api-dlya-obscheniya-s-mikrokontrollerom/
---

View File

@ -1,9 +0,0 @@
---
icon: book
title: yarkov.tech
year: 2023
description: Мой персональный веб-сайт где находятся информация обо мне и список
различных статей и туториалов...
sourceUrl: https://github.com/sergeyyarkov/yarkov.tech
demoUrl: https://yarkov.tech
---

View File

@ -1,10 +0,0 @@
---
title: Databases
list:
- icon: postgresql
title: PostgreSQL
- icon: mongodb
title: MongoDB
- icon: redis
title: Redis
---

View File

@ -1,16 +0,0 @@
---
title: Tools
list:
- icon: nodejs
title: Node.js
- icon: docker
title: Docker
- icon: git-branch
title: Git/gitflow
- icon: sass
title: SASS
- icon: webpack
title: Webpack
- icon: vite
title: Vite.js
---

View File

@ -1,12 +0,0 @@
---
title: Programming
list:
- icon: nodejs
title: Javascript/Typescript
- icon: language-c
title: C Language
- icon: mysql
title: SQL
- icon: avr
title: AVR instruction set
---

View File

@ -1,18 +0,0 @@
---
title: Frameworks
list:
- icon: adonisjs
title: Adonis.js
- icon: alpinejs
title: Alpine.js
- icon: expressjs
title: Express.js
- icon: socket-io
title: Socket.io
- icon: astro_filled
title: Astro.js
- icon: reactjs
title: React.js
- icon: gatsbyjs
title: Gatsby.js
---

View File

@ -1,6 +0,0 @@
---
title: Languages
list:
- title: Russian (native)
- title: English (B1)
---

View File

@ -1,10 +0,0 @@
---
title: Базы данных
list:
- icon: postgresql
title: PostgreSQL
- icon: mongodb
title: MongoDB
- icon: redis
title: Redis
---

View File

@ -1,16 +0,0 @@
---
title: Инструменты
list:
- icon: nodejs
title: Node.js
- icon: docker
title: Docker
- icon: git-branch
title: Git/gitflow
- icon: sass
title: SASS
- icon: webpack
title: Webpack
- icon: vite
title: Vite.js
---

View File

@ -1,12 +0,0 @@
---
title: Программирование
list:
- icon: nodejs
title: Javascript/Typescript
- icon: language-c
title: C Language
- icon: mysql
title: SQL
- icon: avr
title: AVR instruction set
---

View File

@ -1,18 +0,0 @@
---
title: Фреймворки
list:
- icon: adonisjs
title: Adonis.js
- icon: alpinejs
title: Alpine.js
- icon: expressjs
title: Express.js
- icon: socket-io
title: Socket.io
- icon: astro_filled
title: Astro.js
- icon: reactjs
title: React.js
- icon: gatsbyjs
title: Gatsby.js
---

View File

@ -1,6 +0,0 @@
---
title: Языки
list:
- title: Русский (родной)
- title: Английский (A2)
---

View File

@ -1,3 +0,0 @@
---
title: AVR
---

View File

@ -1,3 +0,0 @@
---
title: Digital Electronics
---

View File

@ -1,3 +0,0 @@
---
title: JavaScript
---

View File

@ -1,3 +0,0 @@
---
title: Microcontrollers
---

View File

@ -1,3 +0,0 @@
---
title: Tutorial
---

View File

@ -1,3 +0,0 @@
---
title: Web Development
---

View File

@ -1,3 +0,0 @@
---
title: WebSerial API
---

27
src/graphql/execute.ts Normal file
View File

@ -0,0 +1,27 @@
import type { ExecutionResult } from "graphql";
import type { TypedDocumentString } from "./graphql";
import { DIRECTUS_URL_GRAPHQL, DIRECTUS_TOKEN } from "astro:env/server";
export default async function execute<TResult, TVariables>(
query: TypedDocumentString<TResult, TVariables>,
...[variables]: TVariables extends Record<string, never> ? [] : [TVariables]
) {
const response = await fetch(DIRECTUS_URL_GRAPHQL, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/graphql-response+json",
Authorization: `Bearer ${DIRECTUS_TOKEN}`,
},
body: JSON.stringify({
query,
variables,
}),
});
if (!response.ok) {
throw new Error(`Network response was not ok. ${response.statusText}`);
}
return response.json() as ExecutionResult<TResult>;
}

View File

@ -0,0 +1,83 @@
/* eslint-disable */
import { ResultOf, DocumentTypeDecoration } from '@graphql-typed-document-node/core';
import { Incremental, TypedDocumentString } from './graphql';
export type FragmentType<TDocumentType extends DocumentTypeDecoration<any, any>> = TDocumentType extends DocumentTypeDecoration<
infer TType,
any
>
? [TType] extends [{ ' $fragmentName'?: infer TKey }]
? TKey extends string
? { ' $fragmentRefs'?: { [key in TKey]: TType } }
: never
: never
: never;
// return non-nullable if `fragmentType` is non-nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>>
): TType;
// return nullable if `fragmentType` is undefined
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | undefined
): TType | undefined;
// return nullable if `fragmentType` is nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null
): TType | null;
// return nullable if `fragmentType` is nullable or undefined
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null | undefined
): TType | null | undefined;
// return array of non-nullable if `fragmentType` is array of non-nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: Array<FragmentType<DocumentTypeDecoration<TType, any>>>
): Array<TType>;
// return array of nullable if `fragmentType` is array of nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: Array<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
): Array<TType> | null | undefined;
// return readonly array of non-nullable if `fragmentType` is array of non-nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
): ReadonlyArray<TType>;
// return readonly array of nullable if `fragmentType` is array of nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
): ReadonlyArray<TType> | null | undefined;
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | Array<FragmentType<DocumentTypeDecoration<TType, any>>> | ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
): TType | Array<TType> | ReadonlyArray<TType> | null | undefined {
return fragmentType as any;
}
export function makeFragmentData<
F extends DocumentTypeDecoration<any, any>,
FT extends ResultOf<F>
>(data: FT, _fragment: F): FragmentType<F> {
return data as FragmentType<F>;
}
export function isFragmentReady<TQuery, TFrag>(
queryNode: TypedDocumentString<TQuery, any>,
fragmentNode: TypedDocumentString<TFrag, any>,
data: FragmentType<TypedDocumentString<Incremental<TFrag>, any>> | null | undefined
): data is FragmentType<typeof fragmentNode> {
const deferredFields = queryNode.__meta__?.deferredFields as Record<string, (keyof TFrag)[]>;
const fragName = fragmentNode.__meta__?.fragmentName as string | undefined;
if (!deferredFields || !fragName) return true;
const fields = deferredFields[fragName] ?? [];
return fields.length > 0 && fields.every(field => data && field in data);
}

98
src/graphql/gql.ts Normal file
View File

@ -0,0 +1,98 @@
/* eslint-disable */
import * as types from './graphql';
/**
* Map of all GraphQL operations in the project.
*
* This map has several performance disadvantages:
* 1. It is not tree-shakeable, so it will include all operations in the project.
* 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle.
* 3. It does not support dead code elimination, so it will add unused operations.
*
* Therefore it is highly recommended to use the babel or swc plugin for production.
* Learn more about it here: https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#reducing-bundle-size
*/
type Documents = {
"\n\t\tquery LatestAppealByClientIpQuery($client_ip: String!, $limit: Int!) {\n\t\t\tappeal(filter: { client_ip: { _eq: $client_ip } }, sort: [\"-date_created\"], limit: $limit) {\n\t\t\t\tclient_ip\n\t\t\t\tdate_created\n\t\t\t}\n\t\t}\n\t": typeof types.LatestAppealByClientIpQueryDocument,
"\n\t\tmutation CreateAppealQuery(\n\t\t\t$name: String!\n\t\t\t$email: String!\n\t\t\t$subject: String!\n\t\t\t$message: String!\n\t\t\t$client_ip: String!\n\t\t) {\n\t\t\tcreate_appeal_item(\n\t\t\t\tdata: { name: $name, email: $email, subject: $subject, message: $message, client_ip: $client_ip }\n\t\t\t) {\n\t\t\t\tid\n\t\t\t}\n\t\t}\n\t": typeof types.CreateAppealQueryDocument,
"\n\t\tquery ArticleRSS($lang: String!) {\n\t\t\tarticle_translations(filter: { languages_code: { code: { _contains: $lang } }, status: { _eq: \"published\" } }) {\n\t\t\t\ttitle\n\t\t\t\tslug\n\t\t\t\ttags {\n\t\t\t\t\ttag_id {\n\t\t\t\t\t\ttitle\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tauthor {\n\t\t\t\t\tfirst_name\n\t\t\t\t\tlast_name\n\t\t\t\t\temail\n\t\t\t\t}\n\t\t\t\tdescription\n\t\t\t\tpub_date\n\t\t\t\tlanguages_code {\n\t\t\t\t\tcode\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t": typeof types.ArticleRssDocument,
"\n\t\tquery Article {\n\t\t\tarticle {\n\t\t\t\ttranslations(filter: { status: { _eq: \"published\" } }) {\n\t\t\t\t\tid\n\t\t\t\t\ttitle\n\t\t\t\t\ttags {\n\t\t\t\t\t\ttag_id {\n\t\t\t\t\t\t\ttitle\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tdescription\n\t\t\t\t\tslug\n\t\t\t\t\tpub_date\n\t\t\t\t\tlanguages_code {\n\t\t\t\t\t\tcode\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t": typeof types.ArticleDocument,
"\n\t\tquery RecentArticle($limit: Int!) {\n\t\t\tarticle(limit: $limit, sort: [\"-translations.pub_date\"]) {\n\t\t\t\ttranslations(filter: { status: { _eq: \"published\" } }) {\n\t\t\t\t\ttitle\n\t\t\t\t\tslug\n\t\t\t\t\tpub_date\n\t\t\t\t\tlanguages_code {\n\t\t\t\t\t\tcode\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t": typeof types.RecentArticleDocument,
"\n\t\tquery Article_translations($slug: String!) {\n\t\t\tarticle_translations(filter: { slug: { _eq: $slug } }) {\n\t\t\t\tid\n\t\t\t\ttitle\n\t\t\t\ttags {\n\t\t\t\t\ttag_id {\n\t\t\t\t\t\ttitle\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tdescription\n\t\t\t\tslug\n\t\t\t\tcontent\n\t\t\t\tauthor {\n\t\t\t\t\tfirst_name\n\t\t\t\t\tlast_name\n\t\t\t\t}\n\t\t\t\tpub_date\n\t\t\t\tviews\n\t\t\t\tlanguages_code {\n\t\t\t\t\tcode\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t\tcover_image {\n\t\t\t\t\ttitle\n\t\t\t\t\ttype\n\t\t\t\t\tfilename_disk\n\t\t\t\t}\n\t\t\t\tarticle_id {\n\t\t\t\t\ttranslations {\n\t\t\t\t\t\tslug\n\t\t\t\t\t\tlanguages_code {\n\t\t\t\t\t\t\tcode\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t}\n\t\t\t\t\t\tpub_date\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t": typeof types.Article_TranslationsDocument,
"\n\t\tmutation Update_article_translations_item($id: ID!, $views: Int!) {\n\t\t\tupdate_article_translations_item(id: $id, data: { views: $views }) {\n\t\t\t\tviews\n\t\t\t}\n\t\t}\n\t": typeof types.Update_Article_Translations_ItemDocument,
"\n\t\tquery AboutMeQuery($pageLang: String!) {\n\t\t\tglobal_translations(filter: { languages_code: { code: { _contains: $pageLang } } }) {\n\t\t\t\tabout\n\t\t\t}\n\t\t}\n\t": typeof types.AboutMeQueryDocument,
"\n\t\tquery ContactsQuery {\n\t\t\tglobal {\n\t\t\t\temail\n\t\t\t\tgithub\n\t\t\t\ttelegram\n\t\t\t}\n\t\t}\n\t": typeof types.ContactsQueryDocument,
"\n\t\tquery SiteSettingsQuery {\n\t\t\tglobal {\n\t\t\t\tym_counter\n\t\t\t\trecent_articles_limit\n\t\t\t\tgithub\n\t\t\t}\n\t\t}\n\t": typeof types.SiteSettingsQueryDocument,
"\n\t\tquery Project_translations($pageLang: String!) {\n\t\t\tproject_translations(filter: { languages_code: { code: { _contains: $pageLang } } }, sort: [\"-year\"]) {\n\t\t\t\tid\n\t\t\t\ttitle\n\t\t\t\tyear\n\t\t\t\tdescription\n\t\t\t\tsource_url\n\t\t\t\tdemo_url\n\t\t\t\tarticle_url\n\t\t\t\ticon {\n\t\t\t\t\tfilename_disk\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t": typeof types.Project_TranslationsDocument,
"\n\t\tquery Tag {\n\t\t\ttag {\n\t\t\t\tid\n\t\t\t\ttitle\n\t\t\t}\n\t\t}\n\t": typeof types.TagDocument,
};
const documents: Documents = {
"\n\t\tquery LatestAppealByClientIpQuery($client_ip: String!, $limit: Int!) {\n\t\t\tappeal(filter: { client_ip: { _eq: $client_ip } }, sort: [\"-date_created\"], limit: $limit) {\n\t\t\t\tclient_ip\n\t\t\t\tdate_created\n\t\t\t}\n\t\t}\n\t": types.LatestAppealByClientIpQueryDocument,
"\n\t\tmutation CreateAppealQuery(\n\t\t\t$name: String!\n\t\t\t$email: String!\n\t\t\t$subject: String!\n\t\t\t$message: String!\n\t\t\t$client_ip: String!\n\t\t) {\n\t\t\tcreate_appeal_item(\n\t\t\t\tdata: { name: $name, email: $email, subject: $subject, message: $message, client_ip: $client_ip }\n\t\t\t) {\n\t\t\t\tid\n\t\t\t}\n\t\t}\n\t": types.CreateAppealQueryDocument,
"\n\t\tquery ArticleRSS($lang: String!) {\n\t\t\tarticle_translations(filter: { languages_code: { code: { _contains: $lang } }, status: { _eq: \"published\" } }) {\n\t\t\t\ttitle\n\t\t\t\tslug\n\t\t\t\ttags {\n\t\t\t\t\ttag_id {\n\t\t\t\t\t\ttitle\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tauthor {\n\t\t\t\t\tfirst_name\n\t\t\t\t\tlast_name\n\t\t\t\t\temail\n\t\t\t\t}\n\t\t\t\tdescription\n\t\t\t\tpub_date\n\t\t\t\tlanguages_code {\n\t\t\t\t\tcode\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t": types.ArticleRssDocument,
"\n\t\tquery Article {\n\t\t\tarticle {\n\t\t\t\ttranslations(filter: { status: { _eq: \"published\" } }) {\n\t\t\t\t\tid\n\t\t\t\t\ttitle\n\t\t\t\t\ttags {\n\t\t\t\t\t\ttag_id {\n\t\t\t\t\t\t\ttitle\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tdescription\n\t\t\t\t\tslug\n\t\t\t\t\tpub_date\n\t\t\t\t\tlanguages_code {\n\t\t\t\t\t\tcode\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t": types.ArticleDocument,
"\n\t\tquery RecentArticle($limit: Int!) {\n\t\t\tarticle(limit: $limit, sort: [\"-translations.pub_date\"]) {\n\t\t\t\ttranslations(filter: { status: { _eq: \"published\" } }) {\n\t\t\t\t\ttitle\n\t\t\t\t\tslug\n\t\t\t\t\tpub_date\n\t\t\t\t\tlanguages_code {\n\t\t\t\t\t\tcode\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t": types.RecentArticleDocument,
"\n\t\tquery Article_translations($slug: String!) {\n\t\t\tarticle_translations(filter: { slug: { _eq: $slug } }) {\n\t\t\t\tid\n\t\t\t\ttitle\n\t\t\t\ttags {\n\t\t\t\t\ttag_id {\n\t\t\t\t\t\ttitle\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tdescription\n\t\t\t\tslug\n\t\t\t\tcontent\n\t\t\t\tauthor {\n\t\t\t\t\tfirst_name\n\t\t\t\t\tlast_name\n\t\t\t\t}\n\t\t\t\tpub_date\n\t\t\t\tviews\n\t\t\t\tlanguages_code {\n\t\t\t\t\tcode\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t\tcover_image {\n\t\t\t\t\ttitle\n\t\t\t\t\ttype\n\t\t\t\t\tfilename_disk\n\t\t\t\t}\n\t\t\t\tarticle_id {\n\t\t\t\t\ttranslations {\n\t\t\t\t\t\tslug\n\t\t\t\t\t\tlanguages_code {\n\t\t\t\t\t\t\tcode\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t}\n\t\t\t\t\t\tpub_date\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t": types.Article_TranslationsDocument,
"\n\t\tmutation Update_article_translations_item($id: ID!, $views: Int!) {\n\t\t\tupdate_article_translations_item(id: $id, data: { views: $views }) {\n\t\t\t\tviews\n\t\t\t}\n\t\t}\n\t": types.Update_Article_Translations_ItemDocument,
"\n\t\tquery AboutMeQuery($pageLang: String!) {\n\t\t\tglobal_translations(filter: { languages_code: { code: { _contains: $pageLang } } }) {\n\t\t\t\tabout\n\t\t\t}\n\t\t}\n\t": types.AboutMeQueryDocument,
"\n\t\tquery ContactsQuery {\n\t\t\tglobal {\n\t\t\t\temail\n\t\t\t\tgithub\n\t\t\t\ttelegram\n\t\t\t}\n\t\t}\n\t": types.ContactsQueryDocument,
"\n\t\tquery SiteSettingsQuery {\n\t\t\tglobal {\n\t\t\t\tym_counter\n\t\t\t\trecent_articles_limit\n\t\t\t\tgithub\n\t\t\t}\n\t\t}\n\t": types.SiteSettingsQueryDocument,
"\n\t\tquery Project_translations($pageLang: String!) {\n\t\t\tproject_translations(filter: { languages_code: { code: { _contains: $pageLang } } }, sort: [\"-year\"]) {\n\t\t\t\tid\n\t\t\t\ttitle\n\t\t\t\tyear\n\t\t\t\tdescription\n\t\t\t\tsource_url\n\t\t\t\tdemo_url\n\t\t\t\tarticle_url\n\t\t\t\ticon {\n\t\t\t\t\tfilename_disk\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t": types.Project_TranslationsDocument,
"\n\t\tquery Tag {\n\t\t\ttag {\n\t\t\t\tid\n\t\t\t\ttitle\n\t\t\t}\n\t\t}\n\t": types.TagDocument,
};
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n\t\tquery LatestAppealByClientIpQuery($client_ip: String!, $limit: Int!) {\n\t\t\tappeal(filter: { client_ip: { _eq: $client_ip } }, sort: [\"-date_created\"], limit: $limit) {\n\t\t\t\tclient_ip\n\t\t\t\tdate_created\n\t\t\t}\n\t\t}\n\t"): typeof import('./graphql').LatestAppealByClientIpQueryDocument;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n\t\tmutation CreateAppealQuery(\n\t\t\t$name: String!\n\t\t\t$email: String!\n\t\t\t$subject: String!\n\t\t\t$message: String!\n\t\t\t$client_ip: String!\n\t\t) {\n\t\t\tcreate_appeal_item(\n\t\t\t\tdata: { name: $name, email: $email, subject: $subject, message: $message, client_ip: $client_ip }\n\t\t\t) {\n\t\t\t\tid\n\t\t\t}\n\t\t}\n\t"): typeof import('./graphql').CreateAppealQueryDocument;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n\t\tquery ArticleRSS($lang: String!) {\n\t\t\tarticle_translations(filter: { languages_code: { code: { _contains: $lang } }, status: { _eq: \"published\" } }) {\n\t\t\t\ttitle\n\t\t\t\tslug\n\t\t\t\ttags {\n\t\t\t\t\ttag_id {\n\t\t\t\t\t\ttitle\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tauthor {\n\t\t\t\t\tfirst_name\n\t\t\t\t\tlast_name\n\t\t\t\t\temail\n\t\t\t\t}\n\t\t\t\tdescription\n\t\t\t\tpub_date\n\t\t\t\tlanguages_code {\n\t\t\t\t\tcode\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t"): typeof import('./graphql').ArticleRssDocument;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n\t\tquery Article {\n\t\t\tarticle {\n\t\t\t\ttranslations(filter: { status: { _eq: \"published\" } }) {\n\t\t\t\t\tid\n\t\t\t\t\ttitle\n\t\t\t\t\ttags {\n\t\t\t\t\t\ttag_id {\n\t\t\t\t\t\t\ttitle\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tdescription\n\t\t\t\t\tslug\n\t\t\t\t\tpub_date\n\t\t\t\t\tlanguages_code {\n\t\t\t\t\t\tcode\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t"): typeof import('./graphql').ArticleDocument;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n\t\tquery RecentArticle($limit: Int!) {\n\t\t\tarticle(limit: $limit, sort: [\"-translations.pub_date\"]) {\n\t\t\t\ttranslations(filter: { status: { _eq: \"published\" } }) {\n\t\t\t\t\ttitle\n\t\t\t\t\tslug\n\t\t\t\t\tpub_date\n\t\t\t\t\tlanguages_code {\n\t\t\t\t\t\tcode\n\t\t\t\t\t\tname\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t"): typeof import('./graphql').RecentArticleDocument;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n\t\tquery Article_translations($slug: String!) {\n\t\t\tarticle_translations(filter: { slug: { _eq: $slug } }) {\n\t\t\t\tid\n\t\t\t\ttitle\n\t\t\t\ttags {\n\t\t\t\t\ttag_id {\n\t\t\t\t\t\ttitle\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tdescription\n\t\t\t\tslug\n\t\t\t\tcontent\n\t\t\t\tauthor {\n\t\t\t\t\tfirst_name\n\t\t\t\t\tlast_name\n\t\t\t\t}\n\t\t\t\tpub_date\n\t\t\t\tviews\n\t\t\t\tlanguages_code {\n\t\t\t\t\tcode\n\t\t\t\t\tname\n\t\t\t\t}\n\t\t\t\tcover_image {\n\t\t\t\t\ttitle\n\t\t\t\t\ttype\n\t\t\t\t\tfilename_disk\n\t\t\t\t}\n\t\t\t\tarticle_id {\n\t\t\t\t\ttranslations {\n\t\t\t\t\t\tslug\n\t\t\t\t\t\tlanguages_code {\n\t\t\t\t\t\t\tcode\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t}\n\t\t\t\t\t\tpub_date\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t"): typeof import('./graphql').Article_TranslationsDocument;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n\t\tmutation Update_article_translations_item($id: ID!, $views: Int!) {\n\t\t\tupdate_article_translations_item(id: $id, data: { views: $views }) {\n\t\t\t\tviews\n\t\t\t}\n\t\t}\n\t"): typeof import('./graphql').Update_Article_Translations_ItemDocument;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n\t\tquery AboutMeQuery($pageLang: String!) {\n\t\t\tglobal_translations(filter: { languages_code: { code: { _contains: $pageLang } } }) {\n\t\t\t\tabout\n\t\t\t}\n\t\t}\n\t"): typeof import('./graphql').AboutMeQueryDocument;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n\t\tquery ContactsQuery {\n\t\t\tglobal {\n\t\t\t\temail\n\t\t\t\tgithub\n\t\t\t\ttelegram\n\t\t\t}\n\t\t}\n\t"): typeof import('./graphql').ContactsQueryDocument;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n\t\tquery SiteSettingsQuery {\n\t\t\tglobal {\n\t\t\t\tym_counter\n\t\t\t\trecent_articles_limit\n\t\t\t\tgithub\n\t\t\t}\n\t\t}\n\t"): typeof import('./graphql').SiteSettingsQueryDocument;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n\t\tquery Project_translations($pageLang: String!) {\n\t\t\tproject_translations(filter: { languages_code: { code: { _contains: $pageLang } } }, sort: [\"-year\"]) {\n\t\t\t\tid\n\t\t\t\ttitle\n\t\t\t\tyear\n\t\t\t\tdescription\n\t\t\t\tsource_url\n\t\t\t\tdemo_url\n\t\t\t\tarticle_url\n\t\t\t\ticon {\n\t\t\t\t\tfilename_disk\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t"): typeof import('./graphql').Project_TranslationsDocument;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n\t\tquery Tag {\n\t\t\ttag {\n\t\t\t\tid\n\t\t\t\ttitle\n\t\t\t}\n\t\t}\n\t"): typeof import('./graphql').TagDocument;
export function graphql(source: string) {
return (documents as any)[source] ?? {};
}

4620
src/graphql/graphql.ts Normal file

File diff suppressed because it is too large Load Diff

2
src/graphql/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from "./fragment-masking";
export * from "./gql";

View File

@ -17,8 +17,7 @@ export default {
/* Page descriptions */ /* Page descriptions */
"pageDescription.articles": "pageDescription.articles":
"Technical articles, tutorials and various reference materials related to IT topics can be found here.", "Technical articles, tutorials and various reference materials related to IT topics can be found here.",
"pageDescription.projects": "pageDescription.projects": "A few of my main projects that have been developed and published in open access.",
"A few of my main projects that have been developed and published in open access.",
"pageDescription.contacts": "Use the feedback form or write to me on social networks.", "pageDescription.contacts": "Use the feedback form or write to me on social networks.",
/* Navigation */ /* Navigation */
"navigation.articles": "Articles", "navigation.articles": "Articles",
@ -31,7 +30,7 @@ export default {
"splash.description": "splash.description":
"I develop websites and web applications of varying complexity, and also publish articles and tutorials on various IT topics.", "I develop websites and web applications of varying complexity, and also publish articles and tutorials on various IT topics.",
"splash.about": "about me", "splash.about": "about me",
"splash.contact": "write me", "splash.contact": "text me",
/* Recent articles */ /* Recent articles */
"recentArticles.heading": "recent publications", "recentArticles.heading": "recent publications",
"recentArticles.all": "see all", "recentArticles.all": "see all",
@ -57,4 +56,9 @@ export default {
/* Footer */ /* Footer */
"footer.rssFeed": "RSS Feed", "footer.rssFeed": "RSS Feed",
"footer.developed": "Developed", "footer.developed": "Developed",
/* Submitted page */
"submitted.heading": "Message sent",
"submitted.description": "Your message has been sent, thank you.",
"submitted.error": "Error while sending a message. Try again later.",
"submitted.limit": "Send your message later.",
}; };

View File

@ -17,10 +17,8 @@ export default {
/* Page descriptions */ /* Page descriptions */
"pageDescription.articles": "pageDescription.articles":
"Технические статьи, туториалы и различные справочные материалы которые связаны с IT тематикой находятся здесь.", "Технические статьи, туториалы и различные справочные материалы которые связаны с IT тематикой находятся здесь.",
"pageDescription.projects": "pageDescription.projects": "Несколько моих основных проектов которые были разработаны и выложены в открытый доступ.",
"Несколько моих основных проектов которые были разработаны и выложены в открытый доступ.", "pageDescription.contacts": "Воспользуйтесь формой обратной связи или напишите мне в социальные сети.",
"pageDescription.contacts":
"Воспользуйтесь формой обратной связи или напишите мне в социальные сети.",
/* Navigation */ /* Navigation */
"navigation.articles": "Статьи", "navigation.articles": "Статьи",
"navigation.projects": "Проекты", "navigation.projects": "Проекты",
@ -58,4 +56,9 @@ export default {
/* Footer */ /* Footer */
"footer.rssFeed": "RSS Лента", "footer.rssFeed": "RSS Лента",
"footer.developed": "Разработка", "footer.developed": "Разработка",
/* Submitted page */
"submitted.heading": "Сообщение отправлено",
"submitted.description": "Ваше сообщение отправлено, спасибо.",
"submitted.error": "Ошибка отправки сообщения. Повторите позже.",
"submitted.limit": "Отправьте ваше сообщение позже.",
}; };

View File

@ -8,19 +8,30 @@ export function useTranslation(Astro: Readonly<AstroGlobal>): (key: UIDictionary
} }
export function getLanguageKeys(): Array<string> { export function getLanguageKeys(): Array<string> {
return Object.keys(languages).filter((lang) => return Object.keys(languages).filter((lang) => SUPPORTED_LANGUAGES.includes(lang as LanguageKeys));
SUPPORTED_LANGUAGES.includes(lang as LanguageKeys)
);
} }
export function getLanguageFromURL(pathname: string): LanguageKeys { export function getLanguageFromURL(pathname: string): LanguageKeys {
const lang = pathname.split("/")[1] as LanguageKeys; const splittedPathName = pathname.split("/");
let lang = splittedPathName[1] as LanguageKeys;
if ((lang as string) === "blog") lang = splittedPathName[2] as LanguageKeys;
if (Object.hasOwn(languages, lang)) return lang; if (Object.hasOwn(languages, lang)) return lang;
return DEFAULT_LANGUAGE; return DEFAULT_LANGUAGE;
} }
// Call only from ./src/[lang]/page.astro
export function isPageLangSupported(Astro: Readonly<AstroGlobal>) {
const params = Astro.params as { lang: string };
if (SUPPORTED_LANGUAGES.includes(params.lang as LanguageKeys)) return true;
return false;
}
export function removeLangFromURL(pathname: string): string { export function removeLangFromURL(pathname: string): string {
return pathname.split("/").slice(2).join("/"); const splittedPath = pathname.split("/");
if (splittedPath.length >= 3) return "/" + splittedPath.slice(2).join("/");
return pathname;
} }
function transformExports<T>(modules: Record<string, { default: T }>) { function transformExports<T>(modules: Record<string, { default: T }>) {
@ -32,9 +43,7 @@ function transformExports<T>(modules: Record<string, { default: T }>) {
return translations; return translations;
} }
const translations = transformExports<Record<string, UIDictionaryKeys>>( const translations = transformExports<Record<string, UIDictionaryKeys>>(import.meta.glob("./*/ui.ts", { eager: true }));
import.meta.glob("./*/ui.ts", { eager: true })
);
export function translate(key: UIDictionaryKeys, lang: LanguageKeys): string { export function translate(key: UIDictionaryKeys, lang: LanguageKeys): string {
const value = translations[lang]?.[key] || translations[DEFAULT_LANGUAGE][key]; const value = translations[lang]?.[key] || translations[DEFAULT_LANGUAGE][key];

View File

@ -1,27 +1,23 @@
--- ---
import type { MarkdownHeading } from "astro";
import type { CollectionEntry } from "astro:content";
import { DEFAULT_LANGUAGE, SITE_METADATA } from "@root/constants"; import { DEFAULT_LANGUAGE, SITE_METADATA } from "@root/constants";
import BaseHead from "@components/BaseHead.astro"; import BaseHead from "@components/BaseHead.astro";
import BaseLayout from "@components/BaseLayout.astro"; import BaseLayout from "@components/BaseLayout.astro";
import ArticleContent from "@components/ArticleContent.astro"; import ArticleContent from "@components/ArticleContent.astro";
import ArticlePost from "@components/ArticlePost.astro"; import ArticlePost from "@components/ArticlePost.astro";
import OpenGraph from "@components/OpenGraph.astro"; import OpenGraph from "@components/OpenGraph.astro";
import TableOfContents from "@root/components/TableOfContents.astro"; // import TableOfContents from "@root/components/TableOfContents.astro";
import { useTranslation } from "@root/i18n/utils"; import { markdownToHTML } from "@utils/markdown";
import { Article_TranslationsQuery } from "../graphql/graphql";
export interface Props { export interface Props {
lang?: string | undefined; lang?: string;
article: CollectionEntry<"blog">; article: Article_TranslationsQuery["article_translations"][0];
availableOnLangs?: CollectionEntry<"blog">[];
headings: MarkdownHeading[];
} }
const canonicalUrl = new URL(Astro.url.pathname, Astro.site); const canonicalUrl = new URL(Astro.url.pathname, Astro.site);
const { lang = DEFAULT_LANGUAGE, article, headings, availableOnLangs } = Astro.props as Props; const { lang = DEFAULT_LANGUAGE, article } = Astro.props as Props;
const { const { title, description, cover_image, content } = article;
data: { title, description, coverImage }, const htmlContent = await markdownToHTML(content);
} = article;
--- ---
<html lang={lang}> <html lang={lang}>
@ -32,8 +28,13 @@ const {
description={description} description={description}
type="article" type="article"
url={canonicalUrl.href} url={canonicalUrl.href}
image={coverImage image={cover_image
? { path: coverImage, width: "700", height: "400", type: coverImage } ? {
path: `${cover_image.filename_disk}`,
width: "700",
height: "400",
type: `${cover_image.type}`,
}
: undefined} : undefined}
/> />
<script is:inline defer src="/assets/js/load-code-highlight.js"></script> <script is:inline defer src="/assets/js/load-code-highlight.js"></script>
@ -41,11 +42,8 @@ const {
<BaseLayout> <BaseLayout>
<div class="container"> <div class="container">
<ArticlePost lang={lang} article={article} availableOnLangs={availableOnLangs}> <ArticlePost lang={lang} article={article}>
<TableOfContents headings={headings} /> <ArticleContent set:html={htmlContent} />
<ArticleContent>
<slot />
</ArticleContent>
</ArticlePost> </ArticlePost>
</div> </div>
</BaseLayout> </BaseLayout>

View File

@ -14,10 +14,7 @@ const { title, description, lang = DEFAULT_LANGUAGE, noIndex = false } = Astro.p
--- ---
<html lang={lang}> <html lang={lang}>
<BaseHead <BaseHead title={title ? `${title} | ${SITE_METADATA.title}` : SITE_METADATA.title} description={description}>
title={title ? `${title} | ${SITE_METADATA.title}` : SITE_METADATA.title}
description={description}
>
{noIndex && <meta name="robots" content="noindex" />} {noIndex && <meta name="robots" content="noindex" />}
</BaseHead> </BaseHead>
<BaseLayout> <BaseLayout>

View File

@ -1,5 +1,5 @@
--- ---
import NotFoundPage from "@templates/NotFound.astro"; import NotFoundTemplate from "@templates/NotFound.astro";
--- ---
<NotFoundPage /> <NotFoundTemplate />

View File

@ -1,45 +0,0 @@
---
import type { CollectionEntry } from "astro:content";
import { getCollection } from "astro:content";
import ArticleLayout from "@root/layouts/article.astro";
import { createRelativeArticleUrl, removeLanguageCodeFromPath } from "@root/utils";
/**
* Generate articles pages from `blog` collection.
* The final url will be `http://website.com/blog/2023-01-01/my-article` with default language
* and `http://website.com/{language-code}/blog/2023-01-01/my-article` with other languages.
*/
export async function getStaticPaths() {
const collection = await getCollection("blog", (e) => !e.data.draft);
return collection.map((article) => {
const {
id,
data: { title, pubDate },
} = article;
const url = createRelativeArticleUrl({ id, title, pubDate });
return {
params: { blog: url.slice(1) },
props: {
article,
availableOnLangs: collection.filter((e) =>
e.id.includes(removeLanguageCodeFromPath(article.id))
),
},
};
});
}
type Props = { article: CollectionEntry<"blog">; availableOnLangs: CollectionEntry<"blog">[] };
const { Content, headings } = await Astro.props.article.render();
const { article, availableOnLangs } = Astro.props;
---
<ArticleLayout
lang={article.id.split("/")[0]}
article={article}
headings={headings}
availableOnLangs={availableOnLangs}
>
<Content />
</ArticleLayout>

View File

@ -1,14 +1,8 @@
--- ---
import { getLanguageKeys } from "@i18n/utils";
import { DEFAULT_LANGUAGE } from "@root/constants";
import AboutTemplate from "@templates/About.astro"; import AboutTemplate from "@templates/About.astro";
import { isPageLangSupported } from "@i18n/utils";
// TODO: do not generate pages without required data if (!isPageLangSupported(Astro)) return Astro.redirect("/404");
export function getStaticPaths() {
return getLanguageKeys()
.filter((lang) => lang !== DEFAULT_LANGUAGE)
.map((lang) => ({ params: { lang } }));
}
--- ---
<AboutTemplate /> <AboutTemplate />

View File

@ -1,13 +1,8 @@
--- ---
import { getLanguageKeys } from "@i18n/utils";
import { DEFAULT_LANGUAGE } from "@root/constants";
import ArticlesTemplate from "@templates/Articles.astro"; import ArticlesTemplate from "@templates/Articles.astro";
import { isPageLangSupported } from "@i18n/utils";
export function getStaticPaths() { if (!isPageLangSupported(Astro)) return Astro.redirect("/404");
return getLanguageKeys()
.filter((lang) => lang !== DEFAULT_LANGUAGE)
.map((lang) => ({ params: { lang } }));
}
--- ---
<ArticlesTemplate /> <ArticlesTemplate />

Some files were not shown because too many files have changed in this diff Show More