Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ gem 'image_processing'
gem 'ancestry'
gem 'api-pagination'
gem 'api_cache'
gem 'attribute_normalizer'
gem 'awesome_print', require: false
gem 'aws-sdk-s3'
gem 'bcrypt'
Expand Down
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ GEM
api-pagination (5.0.0)
api_cache (0.3.0)
ast (2.4.2)
attribute_normalizer (1.2.0)
autoprefixer-rails (10.4.19.0)
execjs (~> 2)
awesome_print (1.9.2)
Expand Down Expand Up @@ -597,6 +598,7 @@ DEPENDENCIES
ancestry
api-pagination
api_cache
attribute_normalizer
autoprefixer-rails
awesome_print
aws-sdk-s3
Expand Down
1 change: 1 addition & 0 deletions app/controllers/ui/devices_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ def device_params
:notify_stopped_publishing,
:hardware_version_override,
:mac_address,
:meshtastic_id,
{ :tag_ids => [] },
{ :postprocessing_attributes => :hardware_url },
)
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/v0/measurements_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def update
private

def measurement_params
params.permit( :name, :description, :unit, :definition)
params.permit(:name, :description, :unit, :definition, :meshtastic_id, :meshtastic_default_sensor_id)
end

def set_measurement
Expand Down
36 changes: 36 additions & 0 deletions app/controllers/v0/meshtastic_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
module V0
class MeshtasticController < ApplicationController
before_action :verify_ingest_token
after_action :verify_authorized, only: []

def device_token
@device = Device.includes(
:owner, :tags, { sensors: :measurement }
).find_by(meshtastic_id: params[:meshtastic_id])
render json: { token: @device.device_token } if @device
end

def sensor_id
@sensor = Sensor.joins(
:devices, :measurement
).where(
measurement: { meshtastic_id: params[:measurement_meshtastic_id] },
devices: { meshtastic_id: params[:device_meshtastic_id] }
).first
@sensor ||= Measurement.find_by(meshtastic_id: params[:measurement_meshtastic_id])&.meshtastic_default_sensor
render json: { id: @sensor.id } if @sensor
end

private

def verify_ingest_token
return if ingest_token && params[:ingest_token] == ingest_token

render json: { error: "forbidden" }, status: 403
end

def ingest_token
ENV["MESHTASTIC_INGEST_TOKEN"]
end
end
end
3 changes: 3 additions & 0 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ def flash_class(level)
end
end

def map_base_url
ENV.fetch("MAP_BASE_URL", "https://smartcitizen.me/kits/")
end

def sc_nav_button_to(legend, path, opts={})
button_class = opts[:dark_buttons] ? "btn-dark" : "btn-secondary"
Expand Down
6 changes: 5 additions & 1 deletion app/lib/presenters/device_presenter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def default_options
end

def exposed_fields
%i{id uuid name description state system_tags user_tags last_reading_at created_at updated_at notify device_token mac_address postprocessing location data_policy hardware owner components}
%i{id uuid name description state system_tags user_tags last_reading_at created_at updated_at notify device_token mac_address meshtastic_id postprocessing location data_policy hardware owner components}
end

def notify
Expand Down Expand Up @@ -76,6 +76,10 @@ def mac_address
authorize!(:mac_address) { device.mac_address }
end

def meshtastic_id
authorize!(:meshtastic_id) { device.meshtastic_id }
end

def components
present(device.components, readings: options[:readings])
end
Expand Down
10 changes: 9 additions & 1 deletion app/models/device.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
class Device < ActiveRecord::Base

EXPOSURE_VALUES = %w{indoor outdoor}
HARDWARE_VERSION_OVERRIDE_VALUES = %w{1 1.1}
HARDWARE_VERSION_OVERRIDE_VALUES = ["1", "1.1", "Meshtastic"].freeze

default_scope { with_active_state }

Expand All @@ -34,11 +34,14 @@ class Device < ActiveRecord::Base

accepts_nested_attributes_for :postprocessing, update_only: true

normalize_attributes :mac_address, :meshtastic_id

validates_presence_of :name
validates_presence_of :owner, on: :create
#validates_uniqueness_of :name, scope: :owner_id, on: :create

validates_uniqueness_of :device_token, allow_nil: true
validates_uniqueness_of :meshtastic_id, allow_nil: true

validates_format_of :mac_address,
with: /\A([0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}\z/, allow_nil: true
Expand All @@ -47,6 +50,7 @@ class Device < ActiveRecord::Base

before_save :nullify_other_mac_addresses, if: :mac_address
before_save :truncate_and_fuzz_location!, if: :location_changed?
before_save :set_device_token, if: :meshtastic_id
before_save :calculate_geohash
after_validation :do_geocoding

Expand Down Expand Up @@ -386,4 +390,8 @@ def nullify_other_mac_addresses
end
end

def set_device_token
self.device_token ||= SecureRandom.alphanumeric(6).downcase
end

end
3 changes: 3 additions & 0 deletions app/models/measurement.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# Measurements are descriptions of what sensors do.
class Measurement < ActiveRecord::Base
has_many :sensors
belongs_to :meshtastic_default_sensor, class_name: "Sensor", optional: true

validates_presence_of :name, :description
validates_uniqueness_of :name
validates_uniqueness_of :meshtastic_id, allow_nil: true

def for_sensor_json
attributes.except(*%w{created_at updated_at})
Expand Down
2 changes: 1 addition & 1 deletion app/views/layouts/_nav.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
</a>
</li>
<li class="nav-item">
<a class="nav-link w-100 w-md-auto text-center text-lg-end ms-auto" target="_blank" href="<%= t :map_link_url %>">
<a class="nav-link w-100 w-md-auto text-center text-lg-end ms-auto" target="_blank" href="<%= map_base_url %>">
<span data-bs-toggle="collapse" data-bs-target="#navbarMenuToggle" >
<%= t :map_link_text %>
</span>
Expand Down
2 changes: 1 addition & 1 deletion app/views/ui/devices/_actions.html.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<p class="mb-0">
<%= sc_nav_button_to(t(:show_device_on_map_cta), "https://smartcitizen.me/kits/#{device.id}", local_assigns.merge(external: true)) %>
<%= sc_nav_button_to(t(:show_device_on_map_cta), "#{map_base_url}#{device.id}", local_assigns.merge(external: true)) %>
<% if authorize? device, :update? %>
<%= sc_nav_button_to(t(:show_device_edit_cta, name: device.name), edit_ui_device_path(device, goto: request.path), local_assigns) %>
<% end %>
Expand Down
3 changes: 2 additions & 1 deletion app/views/ui/devices/_fields.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
device.hardware_version_override
),
required: true, label: t(:device_form_hardware_version_override_label) %>
<%= form.text_field :mac_address, required: true, label: t(:device_form_mac_address_label), pattern: "^([0-9A-Fa-f]{2}[:\\-]){5}([0-9A-Fa-f]{2})$", help: t(:device_form_mac_address_help) %>
<%= form.text_field :mac_address, required: !current_user.is_admin_or_researcher?, label: t(:device_form_mac_address_label), pattern: "(?:^([0-9A-Fa-f]{2}[:\\-]){5}([0-9A-Fa-f]{2})$)?", help: t(:device_form_mac_address_help) %>
</div>
<% end %>

Expand Down Expand Up @@ -49,6 +49,7 @@
<% if device.owner.forward_device_readings? %>
<%= form.check_box :enable_forwarding, label: t(:device_form_enable_forwarding_label) %>
<% end %>
<%= form.text_field :meshtastic_id, label: t(:device_form_meshtastic_id_label) %>
<%= form.fields_for :postprocessing, device.postprocessing || Postprocessing.new do |fp| %>
<%= fp.text_field :hardware_url, label: t(:device_form_hardware_url_label), help: t(:device_form_postprocessing_blurb_html) %>
<% end %>
Expand Down
2 changes: 1 addition & 1 deletion app/views/v0/devices/_device.jbuilder
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ authorized = (current_user && (current_user.is_admin? || (device.owner_id && cur
if authorized
json.merge! device_token: device.device_token
json.merge! mac_address: device.mac_address if device.mac_address
json.merge! meshtastic_id: device.meshtastic_id if device.meshtastic_id
else
json.merge! device_token: '[FILTERED]'
end
json.merge!(postprocessing: device.postprocessing)
json.merge!(location: device.formatted_location) if local_assigns[:with_location]
json.merge!(data_policy: device.data_policy(authorized))
json.merge!(hardware: device.hardware(authorized))

if local_assigns[:with_owner] && device.owner
json.owner do
json.id device.owner.id
Expand Down
2 changes: 1 addition & 1 deletion app/views/v0/measurements/_measurement.jbuilder
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
json.(measurement,
:id, :uuid, :name, :description, :definition, :created_at, :updated_at
:id, :uuid, :name, :description, :definition, :meshtastic_id, :meshtastic_default_sensor_id, :created_at, :updated_at
# :is_childless?,
)

Expand Down
18 changes: 18 additions & 0 deletions compose/meshtastic-bridge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
services:
meshtastic-bridge:
build:
context: ../meshtastic-bridge
env_file: ../.env
depends_on:
- mqtt-task-main-1
- mqtt-task-main-2
- mqtt-task-secondary
restart: always
deploy:
resources:
limits:
memory: 2gb
volumes:
- "../meshtastic-bridge:/app"
environment:
PYTHON_UNBUFFERED: 1
2 changes: 2 additions & 0 deletions config/locales/views/devices/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ en:
device_form_hardware_version_override_label: "Hardware version"
device_form_hardware_version_override_option_1: "Smart Citizen Kit 1.0"
device_form_hardware_version_override_option_1_1: "Smart Citizen Kit 1.1"
device_form_hardware_version_override_option_Meshtastic: "Smart Citizen Meshtastic Prototype"
device_form_mac_address_label: "MAC address"
device_form_mac_address_help: "This will be six pairs of letters and numbers separated by colons, for example: 80:e7:a5:47:09:da."
device_form_meshtastic_id_label: "Meshtastic ID"
device_form_details_subhead: "Basic information"
device_form_location_subhead: "Location"
device_form_location_blurb: "You can adjust the location by dragging the marker on the map."
Expand Down
1 change: 0 additions & 1 deletion config/locales/views/nav/en.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
en:
logo_alt: "SmartCitizen"
hamburger_label: "Toggle navigation"
map_link_url: "https://smartcitizen.me/kits/"
map_link_text: "Map"
documentation_link_url: "https://docs.smartcitizen.me"
documentation_link_text: "Documentation"
Expand Down
3 changes: 3 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@
get "/500" => "errors#exception"
get "/test_error" => "errors#test_error"

get "meshtastic/device_token", to: "meshtastic#device_token"
get "meshtastic/sensor_id", to: "meshtastic#sensor_id"

# Active Storage cannot show the correct url if using catchall matchers
# https://github.com/rails/rails/issues/31228
# Disable until a solution is found
Expand Down
6 changes: 6 additions & 0 deletions db/migrate/20250704115254_add_meshtastic_ids_to_devices.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class AddMeshtasticIdsToDevices < ActiveRecord::Migration[6.1]
def change
add_column :devices, :meshtastic_id, :string, null: true
add_index :devices, :meshtastic_id, unique: true
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class AddMeshtasticIdsToMeasurements < ActiveRecord::Migration[6.1]
def change
add_column :measurements, :meshtastic_id, :string, null: true
add_index :measurements, :meshtastic_id, unique: true
add_reference :measurements, :meshtastic_default_sensor, foreign_key: { to_table: :sensors }
end
end
9 changes: 8 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 2025_05_05_081245) do
ActiveRecord::Schema.define(version: 2025_07_06_133001) do

# These are extensions that must be enabled in order to support this database
enable_extension "adminpack"
Expand Down Expand Up @@ -109,9 +109,11 @@
t.string "hardware_slug_override"
t.boolean "precise_location", default: true, null: false
t.boolean "enable_forwarding", default: false, null: false
t.string "meshtastic_id"
t.index ["device_token"], name: "index_devices_on_device_token", unique: true
t.index ["geohash"], name: "index_devices_on_geohash"
t.index ["last_reading_at"], name: "index_devices_on_last_reading_at"
t.index ["meshtastic_id"], name: "index_devices_on_meshtastic_id", unique: true
t.index ["owner_id"], name: "index_devices_on_owner_id"
t.index ["state"], name: "index_devices_on_state"
t.index ["workflow_state", "is_test", "last_reading_at", "latitude"], name: "world_map_request"
Expand Down Expand Up @@ -182,6 +184,10 @@
t.datetime "updated_at", null: false
t.uuid "uuid", default: -> { "uuid_generate_v4()" }
t.string "definition"
t.string "meshtastic_id"
t.bigint "meshtastic_default_sensor_id"
t.index ["meshtastic_default_sensor_id"], name: "index_measurements_on_meshtastic_default_sensor_id"
t.index ["meshtastic_id"], name: "index_measurements_on_meshtastic_id", unique: true
end

create_table "oauth_access_grants", id: :serial, force: :cascade do |t|
Expand Down Expand Up @@ -352,6 +358,7 @@
add_foreign_key "devices_tags", "tags"
add_foreign_key "experiments", "users", column: "owner_id"
add_foreign_key "ingest_errors", "devices"
add_foreign_key "measurements", "sensors", column: "meshtastic_default_sensor_id"
add_foreign_key "postprocessings", "devices"
add_foreign_key "sensors", "measurements"
add_foreign_key "uploads", "users"
Expand Down
1 change: 1 addition & 0 deletions meshtastic-bridge/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.venv
34 changes: 34 additions & 0 deletions meshtastic-bridge/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Use a Python image with uv pre-installed
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim

# Install the project into `/app`
WORKDIR /app

# Enable bytecode compilation
ENV UV_COMPILE_BYTECODE=1

# Copy from the cache instead of linking since it's a mounted volume
ENV UV_LINK_MODE=copy

# Install the project's dependencies using the lockfile and settings
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --locked --no-install-project --no-dev

# Then, add the rest of the project source code and install it
# Installing separately from its dependencies allows optimal layer caching
COPY . /app
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-dev

# Place executables in the environment at the front of the path
ENV PATH="/app/.venv/bin:$PATH"

# Reset the entrypoint, don't invoke `uv`
ENTRYPOINT []

# Run the FastAPI application by default
# Uses `fastapi dev` to enable hot-reloading when the `watch` sync occurs
# Uses `--host 0.0.0.0` to allow access from outside the container
CMD ["uv", "run", "meshtastic-bridge.py"]
Loading